Invitation security
The invite landing page needs to render "You've been invited to Acme by Jane" before the recipient signs in. That means one small piece of the database has to be reachable by anonymous callers. This page documents exactly what that surface is, why it's safe, and who is expected to call it.
The function
public.peek_invitation(_raw_token text) is a SECURITY DEFINER, STABLE SQL function with EXECUTE granted to both anon and authenticated. It is the only privileged invitation function exposed to anon — create_invitation, redeem_invitation, and revoke_invitation are authenticated-only.
How the token is protected
- The raw token is generated server-side when the invite is created and delivered only in the invitation email link.
- The database never stores the raw token. It stores
token_hash, a SHA-256 digest computed viaencode(extensions.digest(_raw_token, 'sha256'), 'hex'). peek_invitationrecomputes the same hash from the caller's input and looks up a single row bytoken_hash. A leaked hash from the database cannot be replayed — you need the original token from the email.- Tokens are single-lookup, time-bound (
expires_at) and tracked with astatusthat flips toredeemed/revokedonce used.
What the function returns
Only the fields the invite landing page needs to render context — no IDs, no emails of other members, no role/permission data, no workspace internals:
kind invitation_kind
email text -- the invited recipient's own email
workspace_name text
inviter_display_name text
client_name text
collaborator_company_name text
group_name text
new_company_name text
expires_at timestamptz
status invitation_statusNotably absent: workspace_id, invited_by_user_id, role, token_hash, and every other column on public.invitations. An attacker who guesses a valid token learns only what the legitimate recipient would already see in their email.
Expected callers
- Invite landing page (
/invite/$token) — the sole intended caller. Runs pre-auth so the recipient can see who invited them before signing up or signing in. - Server function
peekInvitationinsrc/lib/invitations.functions.tswraps the RPC. All other invitation server functions requirerequireSupabaseAuth.
Nothing else in the app calls peek_invitation, and no client code should. Redemption always goes through the authenticated redeem_invitation path, which enforces the recipient's session identity and marks the token consumed atomically.
Regression coverage
The anon-exposure surface is pinned by two automated checks that run in CI on every migration change:
tests/security-definer-exec-grants.test.tsasserts the exact allow-list ofSECURITY DEFINERfunctions thatanonmay execute. Adding a new anon-callable privileged function fails the build until the allow-list is updated deliberately.tests/rls-cross-workspace-admin.test.tsverifies that no role — anonymous or authenticated — can pivot from apeek_invitationresult into cross-workspace reads or admin escalation.