---
title: Drop SDK
category: concept
slug: sdk
summary: "Every Droplet auto-loads a JavaScript API as `window.Drop` — read other Droplets, write typed fields, run polls and surveys, persist state."
---
# Drop SDK

Every Droplet auto-loads a JavaScript API as `window.Drop` — read other Droplets, write typed fields, run polls and surveys, persist state.

Every [Droplet](/docs/droplets) auto-loads a JavaScript API at `window.Drop`. From inside your HTML you can read other Droplets, write to your own typed fields, identify the current viewer, run polls and surveys, and persist state between sessions. There is no install step and no `<script>` tag to add — the SDK is already there when your code runs.

## Quickstart

Wait for the SDK to bootstrap, then call methods. Anything beyond the State API needs a capability declaration in the `<head>` (see [Capabilities](#capabilities)):

```html
<!DOCTYPE html>
<html>
<head>
  <meta name="drop-sdk-capabilities" content="viewer:read">
</head>
<body>
  <h1></h1>
  <script>
    await Drop.ready;
    const me = await Drop.viewer.me();
    document.querySelector('h1').textContent = `Hello, ${me.name}!`;
  </script>
</body>
</html>
```

## Surfaces

| Surface | What you can do |
|---|---|
| `Drop.viewer` | Get the current viewer's name, email, and Space memberships. |
| `Drop.droplets` | List or fetch other Droplets the viewer can read; optionally fold in their shared state for aggregation. |
| `Drop.types` | Read the Type registry — every Type and its field schema. |
| `Drop.fields` | Read typed field values; write to your own Droplet. |
| `Drop.state` | Per-viewer key-value storage. Other viewers never see your values. |
| `Drop.shared` | One value per Droplet, visible to everyone. Read your own or, with `state:read`, another Droplet's. Optimistic concurrency built in. |
| `Drop.poll` | One vote per viewer with aggregated counts. |
| `Drop.survey` | Structured submissions, one per viewer, overwrite on resubmit. |
| `Drop.access` | Names and emails of everyone with read access. |

The [operations table](#operations) below has signatures and short examples for each.

## Calls run as the viewer

Every SDK call is evaluated against the **current viewer**, not against you. `Drop.droplets.list()` returns the subset of Droplets the viewer can read; `Drop.fields.set()` succeeds only if the viewer would be allowed to edit that field from the UI. The author of a Droplet has no runtime presence — it is just HTML someone uploaded.

In practice this means two things:

- You don't write permission checks in Droplet code. If a viewer should not see a row, they don't see it.
- If a Droplet you expect to load is missing for some viewer, check whether they have access to it — not whether the SDK is misconfigured.

## Capabilities

Meta-SDK methods require an opt-in token in the Droplet's HTML head:

```html
<meta name="drop-sdk-capabilities" content="droplets:read fields:write">
```

Multiple tokens are space-separated. Without the right token, the call rejects with `capability_not_declared` *before* any request leaves the iframe — so the failure shows up in dev tools immediately, not as a confusing server response.

The State API (`Drop.state`, `Drop.shared`, `Drop.poll`, `Drop.survey`, `Drop.access`) is always available for the **current** Droplet; no declaration needed. Reading *another* Droplet's shared state — `Drop.shared.get(key, { droplet })`, `Drop.shared.list`, or `includeSharedState` on `Drop.droplets.list` — is the one exception: it needs `state:read`.

| Token | Grants |
|---|---|
| `droplets:read` | `Drop.droplets.list`, `Drop.droplets.get` |
| `viewer:read` | `Drop.viewer.me` |
| `types:read` | `Drop.types.list`, `Drop.types.get` |
| `fields:read` | `Drop.fields.get` |
| `fields:write` | `Drop.fields.set` (this Droplet only) |
| `state:read` | `Drop.shared.get(key, { droplet })`, `Drop.shared.list`, `Drop.droplets.list({ includeSharedState })` — reading *another* Droplet's shared state |
| `csp:wasm` | Browser WebAssembly — adds `'wasm-unsafe-eval'` to the Droplet's CSP (gates no SDK method) |

The capability set is frozen for the life of a deploy. Adding a capability means deploying a new version.

### `csp:wasm` — running WebAssembly

`csp:wasm` is unlike the tokens above: it gates no SDK method. It relaxes the Droplet's Content-Security-Policy by exactly one source — `'wasm-unsafe-eval'` on `script-src` — so the browser will compile WebAssembly, which in-browser ML runtimes (ONNX Runtime Web, Transformers.js) need. It does **not** enable `eval()` or `new Function()`.

It only allows *compiling* WASM, not downloading it. The library, the `.wasm` binary, and any model weights are still fetched — declare their hosts with `<link rel="preconnect">` so they land in `connect-src`:

```html
<meta name="drop-sdk-capabilities" content="csp:wasm">
<link rel="preconnect" href="https://cdn.jsdelivr.net">
<link rel="preconnect" href="https://huggingface.co">
```

## Example: a dashboard of Droplets by tag

A Droplet that lists every other Droplet tagged `#dashboard` and renders each one's description as a card. About fifteen lines of code; the [reference implementation](https://drop.himarley.ai/view/meta-dashboard-reference) is open to fork.

```javascript
await Drop.ready;
const { items } = await Drop.droplets.list({
  tags: ['dashboard'],
  sort: '-updatedAt',
  includeFields: true, // fold each Droplet's typed fields inline — no N+1
});

for (const d of items) {
  const description = d.typeFields?.description ?? d.description ?? '';
  // …render d.name + description into a card
}
```

`includeFields: true` is the pattern for dashboards: each item arrives with its `typeFields` already populated, so you don't need a follow-up `Drop.fields.get` per row.

**Linking a card to its Droplet.** Droplets render inside a sandboxed iframe that allows popups but blocks top-frame navigation, so an outbound link to another Drop page must open a new tab — use `target="_blank"` (with `rel="noopener"`). `target="_top"` and `target="_self"` are silently blocked by the sandbox (you'll see an *"Unsafe attempt to initiate navigation… sandboxed"* console error and the click does nothing).

## Example: rolling up templated Droplets

Teams each deploy a copy of a status template that writes a few `Drop.shared` values — `status`, `confidence`, `blockers`. A rollup Droplet reads them all in one call with `includeSharedState`, no per-Droplet round-trip. Reading other Droplets' shared state needs `state:read` (plus `droplets:read` to list them):

```html
<meta name="drop-sdk-capabilities" content="droplets:read state:read">
```

```javascript
await Drop.ready;
const { items } = await Drop.droplets.list({
  type: 'team-status',
  includeSharedState: ['status', 'confidence', 'blockers'],
});

for (const d of items) {
  const { status, confidence, blockers } = d.sharedState ?? {};
  // …render one row per team
}
```

When you don't know the keys ahead of time, discover them per Droplet with `Drop.shared.list({ droplet })`, or pass `includeSharedState: true` to fold in every (non-reserved) key. And to read a single value from one known Droplet, `Drop.shared.get(key, { droplet })` is the point-read. All three are read-only and scoped to what the viewer can already see — a Droplet the viewer can't read comes back `not_found`.

## Example: a RAPID decision Droplet

Five viewers vote their RAPID role — Recommend, Agree, Perform, Input, Decide — using `Drop.shared` for the running tally. When all five roles have voted and at least one is *Decide*, the Droplet advances its own `status` field:

```javascript
await Drop.fields.set('status', 'decided');
```

Viewers see a toast — *"Status set to Decided by this droplet."* — naming the Droplet as the source, so the change is distinguishable from a manual edit. A viewer without write access on the Droplet gets `forbidden`; you don't gate the call yourself. The full implementation is at the [RAPID reference Droplet](https://drop.himarley.ai/view/rapid-decision-reference).

## Errors

Calls reject with a `DropStateError` carrying a `code` and a human-readable `message`. For most UIs, surfacing `err.message` directly is enough.

| `err.code` | What it means |
|---|---|
| `capability_not_declared` | You called a method without declaring its capability token. Add the token and redeploy. |
| `forbidden` | The viewer lacks permission for this action. |
| `not_found` | The Droplet or field doesn't exist *or* the viewer can't see it (these are intentionally indistinguishable). |
| `validation_failed` | A `Drop.fields.set` value didn't match the field's type. `err.fieldErrors` lists specifics. |
| `rate_limited` | Too many requests. `err.retryAfterMs` says when to retry. |

Narrower codes exist for archived Droplets, unknown fields, missing Type configurations, and expired share-link sessions — the full set is in the [SDK source](https://github.com/himarley/drop/blob/main/packages/web/sdk-src/types.ts).

## Limits

- **List results**: 100 max per page, 25 default. Use the returned `nextCursor` for pagination.
- **Field values**: 256 KB max per JSON value.
- **State keys**: 1,000 distinct keys per Droplet.
- **Cross-Droplet state reads**: `Drop.shared.get(key, { droplet })` and `includeSharedState: [...]` cap at 100 keys per call. `includeSharedState: true` folds up to 10,000 total entries across the page — for exhaustive enumeration of a large store use `Drop.shared.list` (per-Droplet, paged).
- **Rate**: 60 calls/minute per (viewer, Droplet) shared across every surface.
- **External viewers** (share-link, no SSO): can read polls and shared state; writes are no-ops; meta-SDK methods reject with `auth_required`. Gate UI on `Drop.viewer.me()`.
- **Realtime**: `onChange` handlers update within ~5 seconds via background polling — good for collaboration, not chat.
- **Outbound links**: the Droplet iframe sandbox blocks top-frame navigation. Link to other Drop pages with `target="_blank"`, not `_top`/`_self`.

## Where the data lives

A Droplet's detail page has a **State** tab showing aggregate poll counts (with a voter expand), survey responses (with CSV export), and any `Drop.shared` values you've written. Per-viewer state from `Drop.state` is never shown — not even to the owner. Drafts share state with the published Droplet; the owner's *Reset all state* button is the escape hatch.

Owners can also read and write those shared values from **outside** a Droplet — `drop state get` / `set` / `import` on the [CLI](/docs/cli), or the `drop_state_get` / `drop_state_set` [MCP](/docs/mcp) tools — handy for seeding a Droplet's state from a script or having Claude update it directly.



## Operations

### Wait for the SDK

Always `await Drop.ready` before the first SDK call. The SDK bootstraps via a handshake with the parent page; until `ready` resolves, methods may not be wired up.

**State SDK**

```javascript
await Drop.ready;
```

### Drop.viewer.me — who is looking

Returns the current viewer's identity. Use it for personalization, role-gating, or filtering a list of items to "mine." Requires `viewer:read`.

**State SDK**

```javascript
const me = await Drop.viewer.me();
// { id, name, email, isAdmin, spaceMemberships: [...], ... }
```

### Drop.droplets.list / .get — query the catalog

List Droplets the viewer can read. Filters compose: by Type, by tag, by Space, by archived status, sort order. Use the returned `nextCursor` for pagination. `get(slug)` fetches a single Droplet by slug; both return `not_found` for Droplets the viewer can't see. Requires `droplets:read`. Add `includeFields: true` to fold each result's typed fields inline, or `includeSharedState` to fold in shared state (also needs `state:read`) — both avoid an N+1 round-trip.

**State SDK**

```javascript
const { items, nextCursor } = await Drop.droplets.list({
  tags: ['dashboard'],
  sort: '-updatedAt',
  limit: 25,
});

const detail = await Drop.droplets.get('q3-pricing-decision');

// Aggregate shared state across templated Droplets (needs state:read):
const { items: teams } = await Drop.droplets.list({
  type: 'team-status',
  includeSharedState: ['status', 'confidence'], // or true for all keys
});
teams[0].sharedState; // { status: 'green', confidence: 0.8 }
```

### Drop.fields.get / .set — typed field values

Read or write [typed field](/docs/types) values. `get` defaults to the current Droplet; pass a slug to read another (viewer must have access). `set` writes to the current Droplet only — cross-Droplet writes reject. Viewers see a confirmation toast when an SDK write succeeds, naming the Droplet as the source. Requires `fields:read` / `fields:write`.

**State SDK**

```javascript
// Read this Droplet's fields:
const here = await Drop.fields.get();
console.log(here.typeFields.status);

// Read another Droplet's fields (read access required):
const other = await Drop.fields.get('q3-pricing-decision');

// Write a single field — this Droplet only:
await Drop.fields.set('status', 'decided');
// → viewer sees: "Status set to Decided by this droplet"
```

### Drop.types — the Type registry

Read [Types](/docs/types) and their field schemas. Useful when a Droplet needs to render a form for the active Type, or look up the values a select field allows. Requires `types:read`.

**State SDK**

```javascript
const all = await Drop.types.list();
const decision = await Drop.types.get('decision');
// { name, schema: { primitives, customFields }, ... }
```

### Drop.state — per-viewer key-value

Private to the viewer. Use for personal checkboxes, "I read this" markers, personal notes, per-viewer UI prefs. `onChange` returns an unsubscribe function. No capability required.

**State SDK**

```javascript
await Drop.state.set('action-3', true);
const value = await Drop.state.get('action-3');
// → true for this viewer, null for everyone else

const stop = Drop.state.onChange('action-3', (next) => {
  console.log('changed:', next);
});
```

### Drop.shared — one value visible to all viewers

One value per Droplet, visible to all viewers. Pass `expectedVersion` for optimistic concurrency — the call rejects with `version_mismatch` if someone else wrote first, so concurrent writes never silently overwrite. Reading and writing your own Droplet needs no capability. To read *another* Droplet's shared state, pass `{ droplet }` to `get` or use `Drop.shared.list({ droplet })` to enumerate its keys — both need `state:read` and read access to the target (a Droplet the viewer can't see returns `not_found`). Cross-Droplet writes are not supported.

**State SDK**

```javascript
const { value, version } = await Drop.shared.get('chosen-date');
await Drop.shared.set('chosen-date', '2026-06-12', { expectedVersion: version });

Drop.shared.onChange('chosen-date', (value, version) => {
  // fires when any viewer updates the value
});

// Read another Droplet's value (needs state:read):
const { value: theirStatus } = await Drop.shared.get('status', {
  droplet: 'q3-eng-plan',
});

// Discover another Droplet's keys when you don't know them up front:
const { entries } = await Drop.shared.list({ droplet: 'q3-eng-plan' });
// entries: [{ key, value, version, updatedAt }, ...]
```

### Drop.poll — one vote per viewer

Aggregated counts; voting again replaces the previous vote (no double-counting). The owner sees who voted for what on the State tab; viewers see only the totals. No capability required.

**State SDK**

```javascript
await Drop.poll.vote('offsite-date', 'option-a');

const result = await Drop.poll.results('offsite-date');
// { counts: { 'option-a': 7, 'option-b': 12 }, myVote: 'option-a', totalVotes: 19 }
```

### Drop.survey — structured submissions

Each viewer submits a structured response. Re-submitting overwrites the previous answer. The owner sees all responses with email attribution and can export to CSV from the State tab. No capability required.

**State SDK**

```javascript
await Drop.survey.submit('onboarding', {
  rating: 4,
  notes: 'Day one was great, day two was confusing.',
});
```

### Drop.access — who has access

Returns the list of people with read access. Useful for assignment dropdowns, @-mention pickers, or "send to" controls inside the Droplet. No capability required.

**State SDK**

```javascript
const people = await Drop.access.list();
// [{ name, email }, ...]
```

