Droplet
A single-file HTML or Markdown app served on Drop. Versioned content, access policy, optional Space membership.
The Droplet is Drop's core unit. Every Droplet has a stable slug that lives in its URL (/view/{slug}), a current published version, an access policy (PRIVATE / SPACE / SPECIFIC / EVERYONE), and optional membership in one Space. Skills (appType: SKILL) and Drops (appType: DROP) share this same shape; the only difference is rendering and feature gates.
The Droplet object
Every Droplet carries these attributes.
Attributes
/view/{slug} and every API path. Minimum 2 characters, lowercase with hyphens.DROPLET (static HTML/Markdown). DROP is feature-flagged Node.js apps with databases; SKILL is a Claude Code skill recipe rendered with markdown preview.SPACE is live-bound to the Droplet's owning Space membership. SPECIFIC uses a named-people grant list. Set via the Set access method.internalAccess via the share-link layer.currentVersion if the latest version is a draft.null for personal-workspace Droplets.tag:retro) and Space featured-tags curation. Lowercase-normalized server-side.null when active.More attributes+ Show 12− Hide
slug in API paths.{slug}/index.html. Internal — rarely needed by API consumers.ARCHIVED here means *destructively* removed (S3 + edge cleanup done) — distinct from the non-destructive archivedAt./view/{slug} shows the comments side panel. Default true.false./view/{slug}. Default false.{
"id": "clx1abc...",
"slug": "q2-retro-board",
"name": "Q2 retro",
"description": "Quarterly retrospective board for the platform team",
"appType": "DROPLET",
"internalAccess": "SPACE",
"accessMode": "LINK",
"currentVersion": 3,
"publishedVersion": 3,
"s3Key": "q2-retro-board/index.html",
"fileSize": 45200,
"sourceFormat": "HTML",
"searchText": "Q2 platform retro board with cards for went well…",
"tags": [
"retro",
"platform"
],
"spaceId": "spc_platform_team",
"ownerId": "usr_alex",
"status": "ACTIVE",
"archivedAt": null,
"commentsEnabled": true,
"allowEmbedding": false,
"inlineEditingEnabled": false,
"createdAt": "2026-04-12T14:22:00.000Z",
"updatedAt": "2026-05-31T09:15:00.000Z"
}Methods
Endpoints to operate on a Droplet. The example switches between curl, drop CLI, and MCP via the dropdown — your choice persists across the page.
Create a Droplet
Deploy a new Droplet from a single HTML or Markdown file. Auto-creates the slug if not provided. Pass ?draft=true to land in your private workspace instead of publishing. Two-step: this returns a presigned S3 upload URL; PUT the file to that URL, then POST to confirm.
drop create app.html \
--slug q2-retro-board \
--name "Q2 retro" \
--description "Quarterly retrospective board"
# Draft (lands in your private workspace)
drop create app.html --slug q2-retro-board --draft{
"app": {
"id": "clx1abc...",
"slug": "q2-retro-board",
"name": "Q2 retro",
"description": "Quarterly retrospective board",
"appType": "DROPLET",
"accessMode": "LINK",
"sourceFormat": "HTML",
"status": "ACTIVE",
"createdAt": "2026-04-12T14:22:00.000Z"
},
"uploadUrl": "https://drop-apps.s3.amazonaws.com/q2-retro-board/v1/index.html?X-Amz-...",
"isNew": true
}Archive
Non-destructively archive a Droplet — sets archivedAt, hides it from default lists, but preserves all content and audit history. Restore with the unarchive endpoint. Idempotent: a second call returns 204 with no body.
drop archive q2-retro-board{
"slug": "q2-retro-board",
"archivedAt": "2026-05-31T09:15:00.000Z"
}Unarchive
Clear archivedAt and return the Droplet to the active lane. Idempotent: a second call returns 204 with no body.
drop unarchive q2-retro-board{
"slug": "q2-retro-board",
"archivedAt": null
}List pipeline stages
Return the latest-attempt PipelineStage row for each of the droplet's 7 background stages (upload, text-extract, summary, KB ingest, edge sync, CDN invalidation, vector indexing). Stages that have never run are absent. Caller must be able to read the droplet.
drop pipeline q2-retro-board{
"slug": "q2-retro-board",
"stages": [
{
"stage": "UPLOAD",
"status": "SUCCEEDED",
"attempt": 1,
"startedAt": "2026-06-07T15:00:00.000Z",
"endedAt": "2026-06-07T15:00:01.230Z",
"lastError": null,
"metadata": {
"version": 3,
"fileSize": 84321
}
}
]
}Retrieve a Droplet
Return the full Droplet object by slug. Includes Space context (live member count when internalAccess is SPACE) and the effective audience (computed from access mode + Space policy). 404 to anyone who can't see it (privacy by indistinguishability).
Parameters
drop get q2-retro-board{
"id": "clx1abc",
"slug": "q2-retro-board",
"name": "Q2 retro",
"description": "Quarterly retrospective board for the platform team",
"appType": "DROPLET",
"internalAccess": "SPACE",
"accessMode": "LINK",
"currentVersion": 3,
"publishedVersion": 3,
"spaceId": "spc_platform_team",
"tags": [
"retro",
"platform"
],
"archivedAt": null,
"ownerId": "usr_alex",
"status": "ACTIVE",
"s3Key": "q2-retro-board/index.html",
"fileSize": 45200,
"searchText": "Q2 platform retro…",
"sourceFormat": "HTML",
"commentsEnabled": true,
"allowEmbedding": false,
"inlineEditingEnabled": false,
"createdAt": "2026-04-12T14:22:00.000Z",
"updatedAt": "2026-05-31T09:15:00.000Z"
}Update a Droplet
Patch Droplet metadata: name, description, tags, allowEmbedding, commentsEnabled, inlineEditingEnabled, accessMode, password, publicName, spaceSlug. Use the dedicated Set access endpoint to change internalAccess (audience expansions need an explicit confirm). Tags are server-side normalized: trim → lowercase → drop empty → cap at 50 chars each → dedup → cap at 20 total. Only the owner can change accessMode, password, publicName, spaceSlug, and inlineEditingEnabled; editors can change the other metadata.
Parameters
drop set q2-retro-board --name "Q2 platform retro" --tags retro,platform,2026q2{
"id": "clx1abc",
"slug": "q2-retro-board",
"name": "Q2 platform retro",
"description": "Quarterly retrospective board for the platform team",
"appType": "DROPLET",
"internalAccess": "SPACE",
"accessMode": "LINK",
"currentVersion": 3,
"publishedVersion": 3,
"spaceId": "spc_platform_team",
"tags": [
"retro",
"platform",
"2026q2"
],
"archivedAt": null,
"ownerId": "usr_alex",
"status": "ACTIVE",
"s3Key": "q2-retro-board/index.html",
"fileSize": 45200,
"searchText": "Q2 platform retro…",
"sourceFormat": "HTML",
"commentsEnabled": true,
"allowEmbedding": false,
"inlineEditingEnabled": false,
"createdAt": "2026-04-12T14:22:00.000Z",
"updatedAt": "2026-05-31T09:15:00.000Z"
}Delete
Irreversible hard-delete (029-freshness FR-028). Gated by the clean-slate rule (caller is sole owner AND zero comments AND zero view events AND no AppAccess grants). On gate failure returns 409 NOT_CLEAN_SLATE with { code, reason, message }. On success: removes the App row (Prisma cascade-deletes versions, comments, share links, access rows, stars, view events), removes the S3 prefix {slug}/, removes the DynamoDB edge record, and triggers a CloudFront invalidation for /{slug}/*. Use Archive for recoverable removal.
Parameters
drop delete q2-retro-board{
"success": true,
"hardDeleted": true
}Set access
Change internalAccess (PRIVATE / SPACE / SPECIFIC / EVERYONE). Audience expansions return 409 CONFIRMATION_REQUIRED until { confirmed: true } is included; reductions are immediate. See POST /api/apps/{slug}/access/preview for the audience diff. The full request body supports people (email grant list) and grants (mixed audience: email + User Group grants); see the route source for the complete schema.
Parameters
drop set-access q2-retro-board --internal-access EVERYONE --confirmed{
"internalAccess": "EVERYONE",
"changed": [
"mode"
]
}Move into a Space
Move a Droplet into / between / out of a Space. Flips internalAccess to SPACE on move-in; resets to PRIVATE on move-out (FR-015). Preserves prior SPECIFIC grants as supplemental rows on move-in (FR-013) so a later move-out can recover them. Owner-only. Returns 409 CONFIRMATION_REQUIRED with the access preview for audience-expanding moves; never for reductions.
Parameters
drop move q2-retro-board --to-space-id spc_platform_team --confirmed{
"app": {
"slug": "q2-retro-board",
"spaceId": "spc_platform_team",
"internalAccess": "SPACE"
},
"changed": [
"space",
"visibility",
"preserved_specific"
],
"preservedSpecificCount": 2
}Rollback to a version
Roll back to a previous version. The target version's content is copied to the live path and the CDN cache is invalidated. The previous live version stays in history as a separate row.
Parameters
drop rollback q2-retro-board 5{
"success": true,
"version": {
"id": "clxver5",
"version": 5,
"fileSize": 45200,
"deployedBy": {
"name": "Alex Chen",
"email": "alex@himarley.com"
},
"notes": null,
"createdAt": "2026-04-12T14:22:00.000Z"
}
}List versions
Return the full version history for a Droplet — newest first. Each row includes the deployer, the file size, optional notes, the conversion status (for Markdown Droplets), and whether the version is the current published one.
Parameters
drop versions q2-retro-board{
"versions": [
{
"id": "clxver3",
"version": 3,
"fileSize": 45200,
"deployedBy": {
"name": "Alex Chen",
"email": "alex@himarley.com"
},
"notes": "Fixed chart colors",
"isCurrent": true,
"createdAt": "2026-05-31T09:15:00.000Z",
"sourceFormat": "HTML"
}
],
"currentVersion": 3,
"publishedVersion": 3
}Redeploy
Provision the next published version of a Droplet. Returns a presigned S3 upload URL — PUT the new HTML/Markdown content to that URL, and the version is live as soon as the upload completes. Owner/editor only; baseVersion provides optimistic concurrency.
Parameters
drop redeploy q2-retro-board --notes "Updated team list" --file-size 45200{
"uploadUrl": "https://drop-apps.s3.amazonaws.com/q2-retro-board/v4/index.html?X-Amz-...",
"version": 4,
"app": {
"id": "clx1abc",
"slug": "q2-retro-board",
"name": "Q2 retro",
"deployCount": 4,
"lastDeployedAt": "2026-05-31T09:15:00.000Z"
}
}Duplicate
Create a new Droplet seeded from this one's latest version. The new Droplet starts in PRIVATE mode under the caller's ownership; pass an optional name and newSlug. Returns the new Droplet record. Slug collisions auto-resolve by appending -2, -3, etc.
Parameters
drop duplicate q2-retro-board --name "Q3 retro template"{
"id": "clxnew",
"slug": "q2-retro-board-copy",
"name": "Q3 retro template",
"description": "Quarterly retrospective board for the platform team",
"appType": "DROPLET",
"internalAccess": "PRIVATE",
"accessMode": "LINK",
"currentVersion": 1,
"publishedVersion": 1,
"spaceId": null,
"tags": [],
"archivedAt": null,
"ownerId": "usr_alex",
"status": "ACTIVE",
"s3Key": "q2-retro-board-copy/index.html",
"fileSize": 45200,
"searchText": null,
"commentsEnabled": true,
"allowEmbedding": false,
"inlineEditingEnabled": false,
"createdAt": "2026-05-31T09:15:00.000Z",
"updatedAt": "2026-05-31T09:15:00.000Z"
}Star a Droplet
Mark a Droplet as starred for the calling user. Stars are per-user — they pin the Droplet to the top of the dashboard and surface it in drop list --starred. Idempotent: re-starring returns the original starredAt timestamp.
Parameters
drop star q2-retro-board{
"slug": "q2-retro-board",
"starredAt": "2026-05-31T09:15:00.000Z"
}Unstar a Droplet
Remove the caller's star on a Droplet. Idempotent: returns removed: false if there was nothing to remove.
Parameters
drop unstar q2-retro-board{
"slug": "q2-retro-board",
"removed": true
}Pin to Space
Pin a Droplet to the top of its Space. Only valid for Droplets that belong to a Space. Pinning is per-Droplet (not per-user — every Space member sees the same pinned set). Owner/editor only.
Parameters
drop pin q2-retro-board{
"slug": "q2-retro-board",
"pinnedAt": "2026-05-31T09:15:00.000Z"
}Unpin from Space
Remove a Droplet's pin within its Space. Owner/editor only. Idempotent.
Parameters
drop unpin q2-retro-board{
"slug": "q2-retro-board",
"pinnedAt": null
}Publish a draft
Promote one of the caller's drafts to a new published version. Squashes any sibling drafts in the workspace (only the chosen draft survives, the rest are discarded). With no draftId, publishes the latest draft. If the Droplet was previously archived, publish clears archivedAt and the response includes wokeFromArchive: true (029-freshness FR-018 wake-on-publish). Owner/editor only.
Parameters
drop publish q2-retro-board{
"version": 4,
"publishedAt": "2026-05-31T09:15:00.000Z",
"siblingsDiscarded": 2
}Unpublish
Take a published Droplet offline. Sets publishedVersion = null (preserves currentVersion so republish can restore the same version). Viewers see an "unavailable" placeholder. Drafts are unaffected. Owner/editor only.
Parameters
drop unpublish q2-retro-board{
"ok": true
}Republish
Restore a previously-unpublished Droplet by setting publishedVersion = currentVersion. Idempotent on already-published Droplets. Owner/editor only.
Parameters
drop republish q2-retro-board{
"version": 4
}List drafts
List the calling user's drafts on this Droplet. Drafts are private per-caller — co-editors cannot see them. Returns 404 (not 403) when the caller lacks edit rights (privacy by indistinguishability).
Parameters
drop drafts q2-retro-board{
"drafts": [
{
"id": "clxdr1",
"shortId": 1,
"fileSize": 46123,
"sourceFormat": "HTML",
"notes": "WIP: refactor cards",
"createdAt": "2026-05-31T09:15:00.000Z",
"commentCount": 0
}
]
}Retrieve a draft
Get metadata for one of the caller's drafts on a Droplet. draftId may be the CUID or the workspace-local short ID (e.g. 1, 2). Returns 404 when not owned by the caller (privacy by indistinguishability).
Parameters
drop draft get q2-retro-board 1{
"id": "clxdr1",
"shortId": 1,
"fileSize": 46123,
"sourceFormat": "HTML",
"notes": "WIP: refactor cards",
"createdAt": "2026-05-31T09:15:00.000Z",
"commentCount": 0
}Discard a draft
Discard a single draft from the caller's workspace. Returns 204 on success. 404 when the draft is gone or never existed (the CLI / MCP layer treats 404 as idempotent success).
Parameters
drop draft discard q2-retro-board 1{
"discarded": true
}Discard all drafts
Discard every draft in the caller's workspace on this Droplet. Always returns 204 (idempotent — re-running on an empty workspace is a no-op).
Parameters
drop drafts discard-all q2-retro-board{
"discarded": true
}Set Droplet Type
Set the Droplet's Type slot (App.typeId) by resolving a Type by name. Resolution honors scope (USER | SPACE | GLOBAL) when provided, otherwise falls back to most-specific-wins from the viewer's context (user → space → global). NON-DESTRUCTIVE: existing typeFields are PRESERVED across a Type change, and any sign-off — votes and audit log — survives it. Values the new Type does not declare simply do not render, and are still there if you switch back. A topic-shaped Type seeds its draft status for keys that are absent, so a returning decision keeps the status it left with. To genuinely discard a decision, use droplet.clearType, which is the explicit act and does purge. Owner/editor only.
Parameters
drop droplets set-type q2-retro-board --type-name recommendation --scope SPACE --space-slug platform-team{
"slug": "q2-retro-board",
"typeId": "typ_recommendation_platform",
"typeName": "recommendation",
"scope": "SPACE",
"typeFields": {}
}Clear Droplet Type
Clear the Droplet's Type slot — sets App.typeId = null and resets App.typeFields = {}. On a decision this is the un-decision path: the sign-offs and the recorded call go with the Type, reported as purgedSignoff. Idempotent: returns cleared: false when there was nothing to clear. Owner/editor only.
Parameters
drop droplets clear-type q2-retro-board{
"slug": "q2-retro-board",
"cleared": true,
"purgedSignoff": {
"responses": 3,
"hadVerdict": true
}
}Set a typed field on a Droplet
Write a single value into App.typeFields[fieldKey]. The Droplet must have a Type slot set (App.typeId != null); the server validates fieldKey against the Type's schema and coerces / rejects value per the field definition. Per-field errors return 400 TYPE_FIELD_* with { field } extras. Archived Droplets reject writes with 409 APP_ARCHIVED. Owner/editor only.
Parameters
drop droplets set-field q2-retro-board status --value in-flight{
"slug": "q2-retro-board",
"fieldKey": "status",
"value": "in-flight",
"typeFields": {
"status": "in-flight",
"priority": "high"
}
}Clear a typed field on a Droplet
Remove a single key from App.typeFields. Idempotent: returns cleared: false when the key was already absent. Owner/editor only.
Parameters
drop droplets clear-field q2-retro-board priority{
"slug": "q2-retro-board",
"fieldKey": "priority",
"cleared": true,
"typeFields": {
"status": "in-flight"
}
}Transfer ownership
Hand a Droplet to another person. Callable by the current owner or a site admin — the admin path exists because the common case (the owner is on PTO or has left) is exactly when the owner cannot act. The prior owner keeps an EDITOR grant unless keepPriorOwnerAsEditor is false.
Ownership is not an audience change: the recipient must already be able to see the Droplet, or the call returns 409 RECIPIENT_NO_ACCESS. Grant them access first, then transfer. internalAccess, accessMode, Space membership, and every draft workspace are left alone — drafts are keyed per-user and stay with their author.
Transferring to the current owner is a no-op (204). Archived Droplets are rejected.
Parameters
drop transfer forum-video-review --to jackie.booth@himarley.com --keep-prior-owner-as-editor{
"id": "clx1abc",
"slug": "forum-video-review",
"name": "Forum Video Review",
"description": null,
"appType": "DROPLET",
"internalAccess": "EVERYONE",
"accessMode": "LINK",
"currentVersion": 42,
"publishedVersion": 42,
"spaceId": null,
"tags": [
"forum-video"
],
"archivedAt": null,
"ownerId": "usr_jackie",
"status": "ACTIVE",
"s3Key": "forum-video-review/index.html",
"fileSize": 107111,
"searchText": "Forum video review…",
"sourceFormat": "HTML",
"commentsEnabled": true,
"allowEmbedding": false,
"inlineEditingEnabled": true,
"createdAt": "2026-08-12T16:18:33.352Z",
"updatedAt": "2026-09-11T10:02:00.000Z"
}Transfer everything one person owns
Move every Droplet a person owns to someone else — the offboarding path. Restricted to the @drop-owner-transfer role; being a site admin is not sufficient.
Deliberately returns a count, never a list: total plus a breakdown by access mode. That is enough to judge whether you named the right person (fourteen is plausible, four hundred is a typo) without displaying titles. Archived Droplets are excluded.
The count is recipient-aware: total is what will actually move, and blocked is what the recipient cannot read (a Space they are not in, a SPECIFIC list they are not on). Transfer never widens an audience, so those stay put.
One call moves at most 50, chosen from the movable set rather than the most recent, so batch slots are never spent on Droplets the recipient cannot read. remaining counts movable leftovers only — it reaches zero, where a raw owned-count would stall forever on blocked rows. Counting stops at 500; past that truncated is set and the numbers are a floor. Re-running is safe: a Droplet already owned by the recipient is a no-op.
Call with dryRun: true first — the UI does. Partial success is the contract: each Droplet goes through the same per-Droplet rules, and anything that could not move comes back in skipped with a code.
Bulk emits ONE notification per party rather than one per Droplet, while still writing a per-Droplet audit row.
Parameters
drop transfer-all --from allison.paraskevas@himarley.com --to jackie.booth@himarley.com --reason "Allison on PTO through 9/22; Jackie running the forum review." --dry-run{
"dryRun": true,
"total": 12,
"byAccess": {
"EVERYONE": 9,
"PRIVATE": 3
},
"archived": 1,
"blocked": 2,
"truncated": false
}Retry pipeline stage
Re-run a single pipeline stage on this droplet. Creates a new PipelineStage row at attempt = (prior + 1) and returns the terminal status. Synchronous — the caller waits for the stage to complete. Caller must be droplet owner or sys admin.
drop pipeline retry q2-retro-board KB_INGEST{
"slug": "q2-retro-board",
"stage": "KB_INGEST",
"status": "SUCCEEDED",
"attempt": 2,
"lastError": null
}