Space
A named container of Droplets with its own membership and join policy. The unit of team-level scope on Drop.
A Space groups related Droplets and answers the question "who is this group of work for?" Each Space has an owner, a roster of members with roles (OWNER / EDITOR / READ_ONLY, default EDITOR when added or joined), and a joinPolicy that controls whether outsiders can self-join. Droplets in a Space can use internalAccess: SPACE to bind their audience live to the current member list — add a member, they immediately see every SPACE-mode Droplet.
Asking to join an invite-only Space. INVITE_ONLY Spaces can't be self-joined, but anyone at Hi Marley can ask: request-access records the ask (with an optional note) and notifies every member with the OWNER role — in Drop, by email, and by Slack DM with Add / Dismiss buttons, per each owner's Access notification setting. Owners review with requests list / grant (EDITOR by default, or READ_ONLY) / dismiss, or from Space settings → Members. The requester is told when they're added; a dismissal is silent, and they can ask again after 24 hours. Someone added through any other path (the member picker, a User Group, auto-member) has their pending request closed automatically.
The Space object
Every Space carries these attributes.
Attributes
/spaces/{slug} and every API path. Lowercase alphanumeric + hyphens, min 2 characters, globally unique.INVITE_ONLY — members are added explicitly; non-members see the row but cannot self-join (POST /join returns 403 CANNOT_SELF_JOIN). ANYONE_HIMARLEY — any @himarley.com user can self-join with one click. EVERYONE_AUTOMEMBER (admin-only) — every @himarley user is added at signup; an enable triggers a one-time backfill.archivedAt — ARCHIVED here means the Space is tombstoned; the lifecycle archive uses archivedAt./spaces and from search by default; member Droplets cascade-archive too (029-freshness). Restore with Unarchive.tags @>). Lowercase-normalized, deduped, soft cap of 12; order preserved.rocket, users, briefcase). Rendered in the sidebar + Space header.engineering Space links to the @engineering Group). Pure cross-reference — there is no membership sync between the two.More attributes+ Show 7− Hide
slug in API paths and URLs; the id shows up in audit events and the toSpaceId argument of `POST /move`.SpaceMember join row.slackChannelId. Re-synced lazily.hr/, legal/) whose tagged Droplets render tags only to members. See feature 024-tags-ux. Empty by default.{
"id": "spc_platform_team",
"slug": "platform-team",
"name": "Platform Team",
"description": "Roadmaps, retros, and decision logs for the platform team",
"icon": "rocket",
"joinPolicy": "INVITE_ONLY",
"status": "ACTIVE",
"featuredTags": [
"roadmap",
"retro"
],
"privateTagPrefixes": [],
"slackChannelId": null,
"slackChannelName": null,
"linkedGroupId": null,
"archivedAt": null,
"createdById": "usr_alex",
"createdAt": "2026-04-20T10:00:00.000Z",
"updatedAt": "2026-05-31T09:00:00.000Z"
}Methods
Endpoints to operate on a Space. The example switches between curl, drop CLI, and MCP via the dropdown — your choice persists across the page.
List Spaces
Return Spaces the caller belongs to. Pass ?include=all to browse the directory — every Space the caller can see (including INVITE_ONLY, with the member roster suppressed). Each row carries joinPolicy, memberCount, appCount, ownerEmail/ownerName, and a computed canSelfJoin flag.
Parameters
all to browse every Space (directory mode). Default: only Spaces the caller belongs to.drop spaces list[
{
"id": "spc_platform_team",
"name": "Platform Team",
"slug": "platform-team",
"description": "Roadmaps, retros, and decision logs",
"icon": "rocket",
"joinPolicy": "INVITE_ONLY",
"internalAccess": "PRIVATE",
"status": "ACTIVE",
"memberCount": 12,
"appCount": 8,
"isMember": true,
"canSelfJoin": false,
"role": "OWNER",
"createdAt": "2026-04-20T10:00:00.000Z",
"createdBy": {
"name": "Alex Chen",
"email": "alex@himarley.com"
}
}
]Create a Space
Create a new Space. The caller becomes the owner. Slug rules match Droplets (lowercase alphanumeric + hyphens, min 2 chars, globally unique). joinPolicy sets the membership policy at create time. Setting joinPolicy: EVERYONE_AUTOMEMBER requires admin privilege and triggers a backfill of every @himarley user as a MEMBER. If the slug collides with an existing User Group slug, the response includes a non-blocking collisionWarning for the UI to suggest linking.
Parameters
drop spaces create --name "Platform Team" --slug platform-team --description "Roadmaps, retros, and decision logs" --join-policy ANYONE_HIMARLEY{
"space": {
"id": "spc_platform_team",
"name": "Platform Team",
"slug": "platform-team",
"description": "Roadmaps, retros, and decision logs",
"icon": null,
"joinPolicy": "ANYONE_HIMARLEY",
"internalAccess": "EVERYONE",
"memberCount": 1,
"appCount": 0,
"isMember": true,
"role": "OWNER",
"createdAt": "2026-04-20T10:00:00.000Z",
"createdBy": {
"name": "Alex Chen",
"email": "alex@himarley.com"
}
},
"collisionWarning": null
}Retrieve a Space
Return full Space detail by slug — metadata, join policy, owner, current member roster. Members see the full roster; non-members of INVITE_ONLY Spaces get a 404 to hide existence. Open Spaces (ANYONE_HIMARLEY / EVERYONE_AUTOMEMBER) are visible to every @himarley user.
Parameters
drop spaces get platform-team{
"id": "spc_platform_team",
"name": "Platform Team",
"slug": "platform-team",
"description": "Roadmaps, retros, and decision logs for the platform team",
"icon": "rocket",
"joinPolicy": "INVITE_ONLY",
"internalAccess": "PRIVATE",
"status": "ACTIVE",
"slackChannelId": null,
"slackChannelName": null,
"featuredTags": [
"roadmap",
"retro"
],
"linkedGroup": null,
"memberCount": 12,
"appCount": 8,
"isMember": true,
"role": "OWNER",
"createdAt": "2026-04-20T10:00:00.000Z",
"createdBy": {
"name": "Alex Chen",
"email": "alex@himarley.com"
},
"members": [
{
"id": "spm_alex",
"userId": "usr_alex",
"name": "Alex Chen",
"email": "alex@himarley.com",
"role": "OWNER",
"createdAt": "2026-04-20T10:00:00.000Z"
}
]
}Update a Space
Patch Space metadata — name, description, icon, joinPolicy, featuredTags, linkedGroupId. Owner-only for most fields; OWNER or EDITOR can patch featuredTags (curator-level). Setting joinPolicy: EVERYONE_AUTOMEMBER is admin-gated and triggers a member backfill. Reverting from auto-member never silently revokes members. The legacy internalAccess field is accepted as an alias by the server but SPECIFIC is rejected with 400 INVALID_POLICY.
Parameters
drop spaces update platform-team --description "Engineering platform team — owns CI, infra, and Drop." --featured-tags dashboard,retro{
"id": "spc_platform_team",
"name": "Platform Team",
"slug": "platform-team",
"description": "Engineering platform team — owns CI, infra, and Drop.",
"icon": "rocket",
"joinPolicy": "INVITE_ONLY",
"internalAccess": "PRIVATE",
"status": "ACTIVE",
"featuredTags": [
"dashboard",
"retro"
],
"linkedGroup": null,
"memberCount": 12,
"appCount": 8,
"isMember": true,
"role": "OWNER",
"createdAt": "2026-04-20T10:00:00.000Z",
"createdBy": {
"name": "Alex Chen",
"email": "alex@himarley.com"
}
}Delete a Space
Irreversible hard-delete (029-freshness FR-031). Gated by the clean-slate rule: caller must be the sole OWNER, the Space must have zero member Droplets, and zero non-owner SpaceMember rows. On gate failure returns 409 NOT_CLEAN_SLATE with { code, reason, message }. Otherwise removes the Space row (SpaceMember cascade-deletes).
Parameters
drop spaces delete old-experimental-space{
"success": true,
"hardDeleted": true
}List members
Return every member of the Space with role + join timestamp. Visible to members; for INVITE_ONLY Spaces non-members get 403.
Parameters
drop spaces members list platform-team[
{
"id": "spm_alex",
"userId": "usr_alex",
"name": "Alex Chen",
"email": "alex@himarley.com",
"role": "OWNER",
"createdAt": "2026-04-20T10:00:00.000Z"
}
]Add member(s)
Add one or more people to a Space by email. Owner-only. Idempotent — already-members are returned in alreadyMembers; unknown emails are skipped silently. New members immediately inherit access to every SPACE-mode Droplet in the Space (edge cache resynced fire-and-forget). New members are added as EDITOR by default; pass READ_ONLY for view-only access. OWNER is not accepted via this endpoint — use update-member to promote.
Parameters
drop spaces members add platform-team --emails kai@himarley.com,jane@himarley.com{
"added": [
{
"email": "kai@himarley.com",
"role": "EDITOR"
},
{
"email": "jane@himarley.com",
"role": "EDITOR"
}
],
"alreadyMembers": []
}Change a member's role
Owner-only. Promote or demote a Space member. Promoting to OWNER does not transfer ownership exclusively — multiple OWNERs are allowed. Demoting the last OWNER is blocked via the leave / remove-member last-owner guard.
Parameters
drop spaces members set-role platform-team usr_kai --role EDITOR{
"success": true,
"role": "EDITOR"
}Remove member
Remove a single member. OWNER can remove anyone; members can only remove themselves (self-removal == leave). Cannot remove the last OWNER — 400 returned.
Parameters
drop spaces members remove platform-team usr_kai{
"success": true
}Self-join a Space
Self-join a Space. Returns 201 with the new MEMBER row for ANYONE_HIMARLEY. For EVERYONE_AUTOMEMBER users are added at signup; calling join when already a member returns 409 (the client treats it as idempotent success). For INVITE_ONLY returns 403 CANNOT_SELF_JOIN with ownerEmail / ownerName so the client can render "ask the owner for an invite".
Parameters
drop spaces join platform-team{
"membership": {
"id": "spm_kai",
"spaceId": "spc_platform_team",
"userId": "usr_kai",
"role": "EDITOR",
"createdAt": "2026-05-31T09:15:00.000Z"
}
}Leave a Space
Leave a Space. Your Droplets in the Space stay in the Space. Cannot leave as the last OWNER — transfer ownership first (400 LAST_OWNER).
Parameters
drop spaces leave platform-team{
"success": true
}Get audit log
Return the Space-scoped audit log — membership adds/removes, role changes, join-policy changes, automember toggles, bulk backfills, featured-tag changes, and Droplet moves into / between this Space. Caller must be a MEMBER; non-members get 404 to avoid leaking existence. Cursor-paginated, newest-first.
Parameters
drop spaces audit platform-team{
"events": [
{
"id": "ae_1",
"event": "space.member_added",
"actorEmail": "alex@himarley.com",
"actorName": "Alex Chen",
"details": {
"memberEmail": "kai@himarley.com",
"role": "EDITOR"
},
"ipAddress": null,
"createdAt": "2026-05-31T09:15:00.000Z"
}
],
"nextCursor": null,
"hasMore": false,
"pagination": {
"limit": 50,
"total": 1
}
}Archive
Non-destructive archive. Sets archivedAt, hides the Space from default lists, and cascade-archives every Droplet in the Space (029-freshness). Returns the count of cascaded Droplets + notified owners + a correlationId shared by all the cascaded audit events. Owner-only. Idempotent — calling on an already-archived Space returns 204.
Parameters
drop spaces archive platform-team{
"archivedAt": "2026-05-31T09:15:00.000Z",
"cascadedDropletCount": 7,
"notifiedOwnerCount": 3,
"correlationId": "space-archive-clx7"
}Preview archive cascade
Return the blast radius of archiving the Space — total Droplet count, how many are yours, how many belong to other owners, plus up to 5 of those owners' emails. Visible to anyone who can GET the Space (non-members of INVITE_ONLY Spaces get 404).
Parameters
drop spaces archive-preview platform-team{
"totalDropletCount": 7,
"ownedDropletCount": 5,
"otherDropletCount": 2,
"affectedOwnerCount": 2,
"otherOwnerEmails": [
"kai@himarley.com",
"jane@himarley.com"
]
}Unarchive
Clear archivedAt and return the Space to the active directory. Cascaded Droplets stay archived — owners restore them individually with drop unarchive. Owner-only. Idempotent — calling on an active Space returns 204.
Parameters
drop spaces unarchive platform-team{
"slug": "platform-team",
"archivedAt": null
}Bootstrap a User Group
Convenience: create a new User Group seeded from this Space's current roster, then set linkedSpaceId / linkedGroupId on both sides so the cross-reference is in place. People Admin only. No ongoing membership sync — the seed is one-shot.
Parameters
drop spaces bootstrap-group platform-team{
"groupSlug": "platform-team",
"memberCount": 12
}Add a User Group's roster
Copy a User Group's current roster into an existing Space. One-time copy — future changes to the Group's roster do NOT propagate. Authorization: Space owner OR People Admin OR site admin.
Parameters
drop spaces members add-from-group platform-team --group-slug engineering{
"added": 8,
"alreadyMembers": 2,
"totalGroupMembers": 10
}Mark a Space visit
Internal endpoint — upsert the viewer's UserSpaceVisit row with lastVisitAt = now() for this Space and return the PREVIOUS value (null on first visit). The Space Agent panel POSTs this on open; the returned timestamp becomes the since cursor on the immediately-following GET /api/changes?subject=space:S&since=… so the "what's new since I was here" pane renders in one round-trip. Caller must be a Space member (non-members get 404 to hide existence on INVITE_ONLY).
Parameters
drop spaces _visit platform-team{
"lastVisitAt": "2026-06-07T09:15:00.000Z"
}Star a Space
Star a Space to pin it to the top of your sidebar. Personal to you — other members are unaffected. Idempotent: starring an already-starred Space returns the existing starredAt. Caller must be a member (404 SPACE_NOT_FOUND otherwise).
Parameters
drop spaces star platform-team{
"success": true,
"starredAt": "2026-06-19T09:15:00.000Z"
}Unstar a Space
Remove your star from a Space. Personal to you. Idempotent: unstarring a Space that is not starred returns success with starredAt: null. Caller must be a member (404 SPACE_NOT_FOUND otherwise).
Parameters
drop spaces unstar platform-team{
"success": true,
"starredAt": null
}Hide a Space
Hide a Space so it drops out of your active sidebar (it moves to Inactive). Personal to you — other members are unaffected; the Space is NOT archived, and your notifications are unchanged. Idempotent. Caller must be a member (404 SPACE_NOT_FOUND otherwise).
Parameters
drop spaces hide platform-team{
"success": true,
"mutedAt": "2026-06-19T09:15:00.000Z"
}Unhide a Space
Unhide a Space so it returns to its relationship group in your sidebar. Personal to you. Idempotent: unhiding a Space that is not hidden returns success with mutedAt: null. Caller must be a member (404 SPACE_NOT_FOUND otherwise).
Parameters
drop spaces unhide platform-team{
"success": true,
"mutedAt": null
}Request access
Ask to join an invite-only Space. Every Space owner is notified on the channels they have on for "Access" (bell, email, Slack DM). Repeat requests are de-duplicated — the response reports the existing request (outcome: EXISTING) and nobody is notified again. A request an owner turned down also reads as EXISTING for 24 hours (dismissals are never announced); after that, asking again re-opens it. Errors: 409 ALREADY_MEMBER, 409 OPEN_SPACE (join it instead), 404 SPACE_NOT_FOUND (missing or archived).
Parameters
drop spaces request-access brand-hub --message "Picking up the logo audit from Mia this sprint."{
"request": {
"id": "sar_123",
"status": "PENDING",
"createdAt": "2026-10-15T14:12:00.000Z"
},
"outcome": "CREATED"
}List access requests
Owner-only. Pending requests to join the Space, most recent first.
Parameters
drop spaces requests list brand-hub[
{
"id": "sar_123",
"message": "Picking up the logo audit from Mia this sprint.",
"createdAt": "2026-10-15T14:12:00.000Z",
"requester": {
"id": "usr_sam",
"name": "Sam Chen",
"email": "sam@himarley.com"
}
}
]Grant an access request
Owner-only. Adds the requester to the Space (EDITOR by default, or READ_ONLY) and tells them they're in. If they already joined another way their existing role is kept. Idempotent: a request another owner already handled returns outcome: ALREADY_RESOLVED with its current status.
Parameters
requests list).drop spaces requests grant brand-hub sar_123 --role EDITOR{
"outcome": "GRANTED",
"status": "GRANTED",
"role": "EDITOR"
}Dismiss an access request
Owner-only. Turns the request down without adding the requester. They aren't told; they can ask again after 24 hours. Idempotent like grant.
Parameters
requests list).drop spaces requests dismiss brand-hub sar_123{
"outcome": "DISMISSED",
"status": "DISMISSED"
}