---
title: "Your first droplet, DroppyKit"
description: "Scaffold a droplet, run it in the harness, and see it on the shelf."
canonical: "https://getdroppy.app/docs/droppykit/first-droplet"
last-updated: "2026-09-09"
---

# Your first droplet

Scaffold a droplet, run it in the harness, and see it on the shelf.

This walks through a droplet that shows the time in another timezone on Droppy's shelf. It is the shortest thing that is still a real droplet: an entry point, a surface, and a manifest.

The finished version ships with the SDK as `Examples/WorldClock`, including its Icon Composer document and its harness target.

### 1. Scaffold

`droppykit` is the SDK repository's `Scripts/droppykit`, on your PATH as [Add the package](https://getdroppy.app/docs/droppykit/add-spm) sets up.

```
droppykit new worldclock --name "World Clock"
cd worldclock
```

`droppykit new` writes the package, a placeholder Icon Composer document (leave it: Jordy designs every Store droplet's icon, see [The icon](https://getdroppy.app/docs/droppykit/icons)), a placeholder creator avatar, `droplet.json`, a `CHANGELOG.md` and a `README.md` to fill in, and the harness target. The avatar and the words are placeholders that build and validate; the command's output lists what to replace before you submit. If you would rather start from `Examples/WorldClock`, copy that directory instead.

It also writes `AGENTS.md`, `CLAUDE.md` and the MCP wiring for Claude Code, Codex and Cursor. If a coding agent is going to write most of this droplet, open the folder in it now and let it read the brief; see [Build with a coding agent](https://getdroppy.app/docs/droppykit/coding-agents). The rest of this walkthrough is the same either way.

### 2. The entry point

Every droplet has two types. The **principal** is what Droppy's loader instantiates through `NSPrincipalClass`; it is `@objc` and does nothing but make the droplet. The **droplet** is the object that lives for the session.

```
import DroppyKit
import SwiftUI

@objc(WorldClockPrincipal)
public final class WorldClockPrincipal: NSObject, DropletPrincipal {
    public override init() { super.init() }
    @MainActor public func makeDroplet() -> AnyObject { WorldClockDroplet() }
}

@MainActor
public final class WorldClockDroplet: NSObject, ObservableObject, Droplet {
    public nonisolated static let id: DropletID = "worldclock"

    private var host: DropletHost?
    @Published private var now = Date()
    private var ticker: AnyCancellable?

    public func activate(host: DropletHost) throws {
        self.host = host
        ticker = Timer.publish(every: 1, on: .main, in: .common)
            .autoconnect()
            .sink { [weak self] date in self?.now = date }
    }

    public func deactivate() {
        ticker?.cancel()
        ticker = nil
        host = nil
    }
}
```

**Important.** Everything `activate(host:)` starts must be torn down in `deactivate()`. Swift cannot unload code, so a droplet that leaves a timer running keeps running until Droppy relaunches.

### 3. A surface

Conform to [`ShelfWidgetProviding`](https://getdroppy.app/docs/droppykit/api/shelf-widget-providing), declare the widget, and build its view.

```
extension WorldClockDroplet: ShelfWidgetProviding {
    public var widgetDescriptors: [ShelfWidgetDescriptor] {
        [
            ShelfWidgetDescriptor(
                id: "clocks",
                title: "World Clock",
                systemImage: "globe",
                layoutTraits: ShelfWidgetLayoutTraits(
                    preferredSoloWidth: 420,
                    preferredPairedWidth: 210,
                    contentHeight: .fixed(150)
                )
            )
        ]
    }

    public func makeWidgetView(_ id: ShelfWidgetID, context: ShelfWidgetContext) -> AnyView {
        AnyView(WorldClockWidget(droplet: self, context: context))
    }

    public func makeWidgetSettingsPopover(_ id: ShelfWidgetID) -> AnyView? { nil }
}
```

Both widths are required. Droppy refuses a descriptor that leaves either to a host fallback, because the shelf's layout is one algorithm over supplied numbers and a missing number has no correct default.

### 4. The manifest

`droplet.json` describes the droplet to everything that is not the droplet: the build script, the validator, the Store, and getdroppy.app.

```
{
  "id": "worldclock",
  "name": "World Clock",
  "version": "1.0.0",
  "summary": "Every timezone you care about, on the shelf",
  "category": "Productivity",
  "kit": { "abi": 1, "minAPI": "1.0.0" },
  "minAppVersion": "15.3.0",
  "capabilities": ["shelf-read"],
  "surfaces": ["shelf-widget"],
  "icon": "WorldClock.icon",
  "creator": {
    "name": "Jane Doe",
    "kind": "community",
    "url": "https://jane.example",
    "avatar": "Creator.png"
  },
  "source": {
    "repository": "https://gitlab.com/droppyformac1/droplets/-/tree/main/droplets/worldclock",
    "license": "MIT"
  }
}
```

`surfaces` and your conformances must agree. The harness reports the difference, because the two disagreeing is the most common reason a droplet validates and then does nothing.

### 5. Run it

```
droppykit run
```

The harness opens on your droplet's Overview page. Switch to **Shelf widgets** to see it solo and paired, side by side.

### 6. Build and validate

```
droppykit build      # produces WorldClock.droplet
droppykit validate   # the same checks the Store repository runs
droppykit submit     # fork the Store repository, push a branch, open the merge request
```

### 7. Submit

The Store is a repository, [gitlab.com/droppyformac1/droplets](https://gitlab.com/droppyformac1/droplets), one folder per droplet. `droppykit submit` opens the merge request that adds yours; when it merges, the pipeline builds, signs and publishes it, and every Mac running Droppy can install it within minutes. Replace the scaffold's placeholders first, all but the icon, and read [Good citizen](https://getdroppy.app/docs/droppykit/good-citizen) for what review checks beyond the build. See [Submitting](https://getdroppy.app/docs/droppykit/submitting).

## See also

[The manifestdroplet.json, the bundle it becomes, and the capabilities you declare.](https://getdroppy.app/docs/droppykit/droplet-setup) [Design guidelinesHow a droplet looks like part of Droppy instead of a guest in it.](https://getdroppy.app/docs/droppykit/design) [Shelf widgetsA card on the expanded shelf, alone or sharing the row.](https://getdroppy.app/docs/droppykit/shelf-widgets) [The harnessDroppy's real Settings panel, running your droplet, with nothing installed.](https://getdroppy.app/docs/droppykit/harness)
