---
title: "The manifest, DroppyKit"
description: "The lifecycle, the host, and the capabilities a droplet runs under."
canonical: "https://getdroppy.app/docs/droppykit/droplet-setup"
last-updated: "2026-09-09"
---

# The manifest

The lifecycle, the host, and the capabilities a droplet runs under.

A droplet is created once per Droppy launch and activated when it is installed, enabled, and not removed. It runs **in-process, on the main actor**, and shares fate with the app.

That is why the Store is review-and-sign rather than open loading: a droplet has Droppy's permissions, so a droplet is read before it is published.

### Lifecycle

[`activate(host:)`](https://getdroppy.app/docs/droppykit/api/droplet#activate-host) is called once, after the host's own managers are up. Store the host — there is no global accessor, and if you did not keep it you cannot reach it. Keep activation fast; the host runs it under a watchdog.

Throwing marks the droplet failed for the session. The user sees it as unavailable on its Store page, with the error.

[`deactivate()`](https://getdroppy.app/docs/droppykit/api/droplet#deactivate) is called when the user disables or removes the droplet. Tear down everything: timers, observers, windows, registered shortcuts. After it returns the droplet must be inert.

[`prepareForRemoval()`](https://getdroppy.app/docs/droppykit/api/droplet#prepare-for-removal) is a veto. Return `false` to cancel a removal that would leave something in a bad state — a transfer in flight, an unsaved document. It defaults to `true`.

### The host

[`DropletHost`](https://getdroppy.app/docs/droppykit/api/droplet-host) is the whole surface. Every service is present whether or not you may use it; the *effect* is gated. A call you lack the capability for returns `false` or `nil` and is logged, so a missing grant never crashes.

```
func activate(host: DropletHost) throws {
    self.host = host
    if host.isGranted(.globalShortcuts) {
        host.shortcuts.register(id: "toggle", title: "Toggle World Clock", defaultShortcut: nil) {
            // …
        }
    }
}
```

Check [`isGranted(_:)`](https://getdroppy.app/docs/droppykit/api/droplet-host#is-granted) to hide an affordance you cannot honour, not as a substitute for handling refusal — capabilities can be revoked while you run.

### Capabilities

Declare what you need in `droplet.json`. The user sees the list before installing, so ask for what you use and nothing more. A long list is the single most common reason a submission is sent back.

| Capability | Grants |
| --- | --- |
| `hud` | Presenting transient HUDs on the notch |
| `shelf-read` | Observing the shelf's state |
| `shelf-write` | Opening and closing the shelf |
| `expanded-surface` | Presenting takeover surfaces |
| `global-shortcuts` | Registering shortcuts the user can rebind |
| `clipboard-read` / `clipboard-write` | Reading and writing the pasteboard |
| `network-client` | Outbound network requests |
| `menu-bar` | A menu bar extra |
| `lock-screen` | A row on the lock surface |

The full list is [`DropletPermission`](https://getdroppy.app/docs/droppykit/api/droplet-permission).

### Storage

[`DropletPreferencesService`](https://getdroppy.app/docs/droppykit/api/droplet-preferences-service) namespaces every key to `droplet.<your-id>.<key>`, so two droplets can both store `"interval"` without colliding and neither can read the other's. That namespacing is why [`DropletID`](https://getdroppy.app/docs/droppykit/api/droplet-id) forbids `.` — without the rule, droplet `a` writing `b.c` and droplet `a.b` writing `c` are the same key.

Settings survive disable, re-enable, update, and removal. A user who removes and reinstalls finds their configuration intact.

For files, [`containerDirectory`](https://getdroppy.app/docs/droppykit/api/droplet-environment-service#container-directory) is a private per-droplet directory that persists across updates.

### Logging

Use [`DropletLogService`](https://getdroppy.app/docs/droppykit/api/droplet-log-service), never `print`. A droplet runs inside an installed app, and `print` from there goes nowhere a user or a reviewer can find.

```
host.log.info("activated for \(zone)")
```

## See also

[Design guidelinesHow a droplet looks like part of Droppy instead of a guest in it.](https://getdroppy.app/docs/droppykit/design)
