Properties
Lazy expiry
Expiry is lazy — there is no background sweep and noexpired 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
userIdor normalizedemail) 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-verifiedcode 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 terminaldeclinedstate either. (Squatters can still see the invite: surfacing is deliberately not verification-gated.)
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 byuserIdlets 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
userIdworkaround), so root-creation is effectively unavailable on a purely external-auth project in v1.

