workspaces module in @sublay/js is the client-side surface of the Workspaces bundle. It covers all 21 endpoints — workspace CRUD, membership management, the full invitation lifecycle, and the authority read — so a members-management UI needs no server-side code.
The actor is always derived from the bearer token. No actor field is ever sent from the client — the props types simply do not carry one, and a call that hand-writes actingUserId past the types to name a user other than the token’s own is rejected by the server with a 403. Acting on behalf of a different user is the node SDK service-key capability, and it is the only difference in reach between the two SDKs for this bundle — not a restriction on which operations a client may perform.
Requires the
workspaces bundle. See Bundles to install it.fetchWorkspace is the one read that also works without a token, returning the workspace when visibility permits.) Read the concepts first: capabilities vs permissions, rank, reach & ownership, and the identity-matched, no-token invite model.
Workspace lifecycle
createWorkspace
The bearer-token user becomes the owner. Root creation requires a verified email; child creation requirescreate-sub-workspace on the parent.
string
required
Display name (1–100 characters).
object
Opaque developer data.
string
Parent for child creation.
fetchWorkspace
string
required
string
Comma-separated include flags.
memberCount adds the workspace’s direct member count; omit it and the field is absent. An unrecognized flag value is ignored.fetchManyWorkspaces
Lists the bearer-token user’s direct-membership + owned workspaces (paginated). Self-service.number
number
string
Comma-separated include flags, sent as a query param.
memberCount adds a memberCount to every row — the workspace’s direct member count, resolved in one grouped query over the returned page rather than one request per row. Omit it and the field is absent from each row.updateWorkspace
Editsname/metadata. Requires the edit-workspace capability (or ownership). Does not flip the inherit flag.
string
required
string
object
updateWorkspaceInheritFlag
Flips whether authority held on an ancestor reaches into this node. Owner-only in both directions (own owner or an ancestor owner) — not a capability. Blocked with403 workspace/inherit-enforced when the project enforces its default.
string
required
boolean
required
true = open/door, false = sealed/wall.deleteWorkspace
Owner-only (own owner or an ancestor owner). Cascades the whole subtree transactionally and firesworkspace.deleted per deleted workspace — and no workspace.member.removed events for the memberships it tears down (see Webhooks).
string
required
transferWorkspaceOwnership
Owner-only (own owner or an ancestor owner).newOwnerId addresses the target — any verified user in the tenant, who need not already be a member.
string
required
string
required
The new owner (any verified user).
string
"demote" or "remove". Defaults to "remove" when omitted.number
On demote, the ex-owner’s rank (absolute only). Defaults to one rung below the acting user’s anchor —
0 for the usual apex actor, or one below their own row if they sit in this workspace’s ladder.string[]
Membership
fetchWorkspaceMembers
The unified roster —WorkspaceRosterResponse, or WorkspaceRosterCountsResponse with countOnly. Never paginated. Requires roster visibility — any relation to the workspace.
string
required
string
ancestorOwners, reachHolders, descendants.boolean
fetchWorkspaceMemberStanding
One user’s resolved standing, addressed by user id — works for any relation, including users with no direct member row. Requires roster visibility — any relation to the workspace.string
required
string
required
The target whose standing to read (path param, not an actor).
WorkspaceMemberStanding. On its user field only id is guaranteed (WorkspaceStandingUser): the server returns the full user record when the user row still exists and falls back to { id } alone when it does not — a deleted user with a lingering membership row is a reachable case. Read the rest defensively (standing.user.username ?? "Deleted user").
reasons is an array of structured { type, viaWorkspaceId? } entries — the same shape a roster entry’s reasons carries — so you learn which ancestor grants an ancestor-owner / reach-holder standing.
updateWorkspaceMember
Two capability tiers: powerful fields (capabilities, permissions, and rank as either rank or relativeRank) need edit-member-access plus the rank rule and no-escalation; cosmetic fields (title, metadata) need only edit-member-profile, and a member editing their own title needs nothing.
Rank moves in either coordinate, never both; omitting both means rank unchanged. See choosing a rank.
string
required
string
required
The member being edited (path param, not an actor).
string[]
string[]
number
Absolute. Mutually exclusive with
relativeRank; omit both to leave rank unchanged.number
An offset from you —
1 = one rung below me. Must be >= 1. Resolved to an absolute rank at write time and frozen. No default.string
object
removeWorkspaceMember
Removes a member from this node only. Requiresremove-member and is rank-bounded; sole-owner protected.
string
required
string
required
The member to remove (path param, not an actor).
removeWorkspaceMemberFromSubtree
Removes the target’s direct memberships on this workspace and every descendant you can reach. Requiresremove-member, rank-bounded per node. Blocks with 409 workspace/owns-descendants (and reports them) if the target owns any descendant workspace.
string
required
string
required
The user to offboard (path param, not an actor).
leaveWorkspace
Self-service — removes the bearer-token user’s own direct membership on this node only. No capability required. An owner cannot leave (409 workspace/sole-owner); transfer or delete first.
string
required
Invitations
createWorkspaceInvite
Requires theinvite capability (or ownership); the inviter is the bearer-token user. Address the invitee by exactly one of email, userId, or username — here userId/username address the invite target, not an actor. Email matching against existing accounts is case-insensitive.
string
required
string
One of
email/userId/username.string
Invite target user id.
string
Invite target username.
string[]
string[]
number
Absolute. Not required — mutually exclusive with
relativeRank, and omitting both applies the relativeRank: 1 default.number
default:"1"
An offset from the inviter —
1 = one rung below me. Must be >= 1. Anchored on your own rank if you hold a member row here, apex otherwise. A snapshot: resolved to an absolute rank at invite time and frozen. Relying on the default puts everyone on the same rung, and equals cannot manage each other. Sharper case: an inviter with no member row here (the owner, an ancestor owner, a reach holder) anchors at apex, so their default mints rank 0 — name a coordinate if that is not what you want. See choosing a rank.string
fetchWorkspaceInvites
The workspace’s outbox — the live pending invites it has issued (status='pending' AND expiresAt > now). Requires the invite capability (or ownership). Always returned in full; deliberately unpaginated.
string
required
revokeWorkspaceInvite
Marks a pending invitationrevoked. Requires the invite capability (or ownership). Note the verb-suffixed POST .../revoke route — this is not a DELETE.
string
required
string
required
resendWorkspaceInvite
Refreshes a pending invite — resets a 14-day expiry and resends the email — valid even on one already pastexpiresAt. Terminal invites → 409. Requires the invite capability (or ownership). Also a verb-suffixed POST .../resend. Returns the refreshed WorkspaceInvitation, carrying the new expiresAt. Like createWorkspaceInvite, it requires workspaces.inviteAcceptUrl on the project → otherwise 409 workspace/missing-invite-accept-url with expiresAt untouched.
string
required
string
required
fetchMyWorkspaceInvites
The caller’s inbox — the bearer-token user’s live pending invites across every workspace, matched byuserId from the token. Self-service, and surfacing is not verification-gated — a user sees pending invites right after signup (which nudges them to verify). The verified check applies when they act on one (accept or decline).
acceptWorkspaceInvite
Self-service — identity-matched to the bearer-token user, and a verified email is required. TheinviteId is non-secret. Idempotent when already a member/owner.
string
required
declineWorkspaceInvite
Self-service — identity-matched, and a verified email is required (the same gate as accept, so an unverified squatter on someone else’s address cannot burn their invite). Returns403 workspace/email-not-verified otherwise.
string
required
Authority-as-a-service
fetchWorkspaceAuthority
The bearer-token user’s resolved standing on a workspace —{ reasons, capabilities, permissions, rank }. Self-service, so nothing is fenced here. (No relativeRank: it is an offset from you, and here the subject is you, so rank is the coordinate this read reports.) Drive your UI off this rather than guessing: a permission check is a one-line .includes() on the result.
reasons is an array of structured { type, viaWorkspaceId? } entries (viaWorkspaceId on ancestor-owner / reach-holder only). capabilities is the fully-resolved set and view is implied by every other capability — any user with standing carries it, while a stranger gets reasons: [] and an empty set.
string
required
permissions — there is deliberately no ?permission= check or can() helper.
