v0.22.0¶
Release Date: 2026-10-25
Invite-gated sign-up lets a deployment grow on purpose: first the people its
operators know by name, then cohorts invited with shareable codes, then
people early members vouch for. It builds on invite-only sign-up and adds no
breaking changes. Existing databases need migration
0011.
Highlights¶
- Invite codes. An operator creates a code with a use limit, an optional
expiry and a label naming the cohort, and shares it as a link
(
/signup?code=…). A code with N uses admits at most N accounts however many sign-ups race for it. Only a hash is stored, so a code is shown once. - Member invites. An operator gives a member an allowance, or every member a default one; members create single-use codes up to it. Codes already used, and codes still usable, count against it.
- Attribution. Every account admitted by a code records which code and who created it; email invites record the principal who created them.
- An invite API and commands.
signupgate/signupapiserves operator routes (email invites, codes, the waitlist, allowances, joined accounts) and member routes (a person's own codes), behind SystemAuth's session or a host's own. New commands create codes, set allowances, invite from the waitlist, and grant the first platform administrator.
Invite codes¶
signupgate.CodeStore:CreateCode,ListCodes,RevokeCode,Lookup,Redemptions. Codes are 16 characters of Crockford base32 (80 bits), shown asXXXX-XXXX-XXXX-XXXX;NormalizeCodereads them back forgivingly.- The gate admits a verified email with a pending email invite, or else one
bringing a usable code. It reserves a use with one conditional update
before the account is created and gives it back through the new optional
systemauth.SignupGateAborterif creation fails, for example for the sign-up age question. An email invite wins over a code. systemauth.WithSignupCodesserves the sign-up page at/signup, keeps a usable code in an HttpOnly cookie for an hour, and acceptsinvite_codeon/login/{provider}. The page is rate-limited per client address, sends no referrer, and never says why a code cannot be used.
Member invites¶
CodeStore.SetAllowance,Allowances,MemberInvites,CreateMemberCodeandRevokeMemberCode, withsignupgate.MemberOptions{DefaultAllowance, CodeTTL}(defaults 0 and 14 days). Creating a member code locks the member's allowance row, so concurrent requests cannot exceed it.
Invite API¶
signupapi.New(Options) takes the stores, how to find the signed-in person,
and how to tell a platform administrator. JSON is snake_case, responses are
not cached, cross-origin writes are refused, and every change is audited:
signup_invite.created, signup_invite.revoked, signup_code.created,
signup_code.revoked, signup_code.redeemed, signup_allowance.set,
signup_waitlist.invited. See the
guide for the routes.
SystemAuth service¶
New systemauthsvc.Options: SignupCodes (needs InviteOnly),
SignupPage, MemberInvites, and SignupAPI (serves the API at
/signup-invites/api for the SystemAuth session). Service.SignupCodes()
returns the code store and Service.SignupAPI(principal) the API for a host
to serve behind its own session.
Commands¶
systemauth code create|list|revoke|joinedsystemauth allowance set <email> <count|default>,allowance listsystemauth waitlist invite <email>... | --allsystemauth admin grant|revoke|list(principal/admincmd), which creates the first platform administrator without editing the database
The waitlist CSV export gains an invited_at column.
Upgrading¶
Run migrations/0011_signup_invite_attribution.sql
against an existing Postgres database: it adds
sf_signup_invites.invited_by_principal_id and
sf_signup_waitlist.invited_at. The new tables (sf_signup_codes,
sf_signup_code_redemptions, sf_signup_allowances) are created by the
schema migration and are never granted to application database logins.
Nothing else changes for existing deployments: codes, member invites and the API are off until their options are set.