Skip to main content
The 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 userId is ever sent from the client, and the wrappers strip one if a caller smuggles it in. 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.
Every call is authorized against the signed-in user’s own standing on the workspace: owner-only, capability-gated, rank-bounded, visibility-gated, or self-service. Each function below states which. (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.
A userId that names a target is fully available on the client — the invitee userId on createWorkspaceInvite, and the targetUserId path param on the member functions. Only the actor userId is node-SDK-only.

Workspace lifecycle

createWorkspace

The bearer-token user becomes the owner. Root creation requires a verified email; child creation requires create-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
memberCount adds the direct member count.

fetchManyWorkspaces

Lists the bearer-token user’s direct-membership + owned workspaces (paginated). Self-service.
number
number
string

updateWorkspace

Edits name/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 with 403 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 fires workspace.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".
number
On demote; defaults to 0.
string[]

Membership

fetchWorkspaceMembers

The unified rosterWorkspaceRosterResponse, or WorkspaceRosterCountsResponse with countOnly. Never paginated. Requires roster visibility — any relation to the workspace.
rank, capabilities and permissions are fenced on other users’ entries. They are omitted (absent — not null) unless the bearer-token user holds one of the four people-operating capabilities (invite, remove-member, edit-member-access, edit-member-profile) or is the owner / an ancestor-owner. The caller’s own entry always carries them. Who is on the roster — user, reason types, title, metadata, viaWorkspaceId — stays visible to any relation, so read the fenced fields defensively (reason.capabilities ?? []).
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.
capabilities, permissions and rank are fenced. They are omitted unless the bearer-token user holds one of the four people-operating capabilities (invite, remove-member, edit-member-access, edit-member-profile), is the owner / an ancestor-owner, or is reading their own standing. user, reasons, title and metadata are always returned.
string
required
string
required
The target whose standing to read (path param, not an actor).
Returns 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, rank) 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.
string
required
string
required
The member being edited (path param, not an actor).
string[]
string[]
number
string
object

removeWorkspaceMember

Removes a member from this node only. Requires remove-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. Requires remove-member, rank-bounded per node. Blocks with 409 workspace/owns-descendants (and reports them) if the target owns any descendant workspace.
A non-owner’s sweep may be partial. It stops at sealed sub-workspaces (inheritsFromParent = false), so the target can keep a membership you never saw. Always check skippedCount / skippedremovedCount alone does not mean the user is gone from the subtree. An owner or ancestor owner always gets skippedCount: 0. Where you have no standing on a skipped workspace its id and name come back null: you learn that a membership survived, not where — escalate to an owner of that branch.
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 the invite 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.
The project must have workspaces.inviteAcceptUrl configured, or this call fails with 409 workspace/missing-invite-accept-url and persists nothing — the invitation email would have nowhere to deep-link. It has no default. Applies even when the invitee already has an account. See Project settings.
string
required
string
One of email/userId/username.
string
Invite target user id.
string
Invite target username.
string[]
string[]
number
required
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
Do not confuse this with fetchMyWorkspaceInvites. fetchWorkspaceInvites is scoped to one workspace and lists what it sent out (needs invite). fetchMyWorkspaceInvites is scoped to the caller, spans every workspace, and lists what was addressed to them (needs nothing).

revokeWorkspaceInvite

Marks a pending invitation revoked. 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 past expiresAt. 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 by userId 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. The inviteId 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). Returns 403 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. 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
Sublay never consumes your opaque permissions — there is deliberately no ?permission= check or can() helper.