Skip to main content
A WorkspaceInvitation is a persistent record inviting a person into a workspace with a pre-chosen grant (capabilities, permissions, rank, title) applied atomically on accept. Every invite always sends an email (there is no developer toggle). Acceptance is identity-matched + verified-email, with no token — see Acceptance model.

Properties

Lazy expiry

Expiry is lazy — there is no background sweep and no expired status or webhook. A pending invite past expiresAt is effectively expired: it won’t accept, and “live” reads filter on status = 'pending' AND expiresAt > now. A capability-holder can resend to refresh a lapsed invite (resets a fresh 14-day expiry).

Acceptance model

Acceptance is an authenticated, in-app action gated by identity, not a token:
  • No secret token is minted or emailed. The email link is a non-secret pointer to your app (a per-project accept URL) with a ?workspaceInviteId=<id> appended for deep-linking.
  • On accept and on decline, Sublay verifies the caller is the invite’s target (by userId or normalized email) and has a verified email.
  • A verified email is required to accept any invite — userId- or email-addressed. An unverified accept returns the distinct workspace/email-not-verified code so your app can prompt verification specifically. A squatter who signed up with someone else’s address can never accept (they can’t verify it) — and decline is gated the same way, so they cannot burn the real target’s invite by moving it to the terminal declined state either. (Squatters can still see the invite: surfacing is deliberately not verification-gated.)
Combined with the verified-email gate on root-workspace creation — which service and master keys do not bypass either — every workspace participant (member or owner) has a confirmed email.
Delivery requires one project setting. Invitation emails deep-link to your app via workspaces.inviteAcceptUrl, which has no default. Until it is set, creating or resending an invitation is refused with 409 workspace/missing-invite-accept-url — including for invitees who already have an account. The link itself carries nothing secret; acceptance is identity-matched.

Binding to userId

Invitations bind to a userId: at invite time if the user already exists, and via a signup-attach step for new users (on signup, pending invites matching the new user’s normalized email get their userId). So surfacing and matching are always by userId, and a deleted user never leaves an orphaned email-invite.

v1 external-auth limitation

External / developer-managed-auth users have no verified-email signal in v1. Consequently, on a purely external-auth project:
  • They cannot act on any invite — email- or userId-addressed — because both acceptance and decline are hard-gated on a verified email, which they lack. Inviting by userId lets the invite bind to the user and surface in their pending list, but they cannot accept or decline it in v1 until a verified-email signal exists.
  • They cannot create a root workspace at all (root creation is hard-gated on a verified email, with no userId workaround), so root-creation is effectively unavailable on a purely external-auth project in v1.
This is a documented v1 limitation. A future dev-flippable project flag (treat the project’s asserted emails as verified) would lift both restrictions.