-- Self-service sign-up, and the invite links that make it useful. -- -- Until now the only way to get an account was for somebody who already had one -- to create it, and the login page told people to "ask an admin". That is a -- workable arrangement for one operator and an impossible one for a team. -- -- An invite is a link, not an email: this server has no SMTP and adding it to -- send one message would be a new subsystem to run, secure and monitor. The -- person inviting sends the link however they already talk to the person they -- are inviting. CREATE TABLE invites ( id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, -- SHA-256 of the raw token, like api_keys, the integration keys and the -- acknowledgement tokens. A leaked database hands nobody an account. token_hash TEXT NOT NULL UNIQUE, -- Which team the invitee lands in, and as what. An invite always names a -- team: an account in no team sees an empty queue and can be paged by -- nobody, which is not a state to invite somebody into. team_id BIGINT NOT NULL REFERENCES teams(id) ON DELETE CASCADE, role TEXT NOT NULL CHECK (role IN ('owner', 'member')), created_by BIGINT REFERENCES users(id) ON DELETE SET NULL, created_at BIGINT NOT NULL DEFAULT FLOOR(EXTRACT(EPOCH FROM now()))::bigint, -- Invites expire. A link that works forever is a credential nobody -- remembers issuing, sitting in a chat log. expires_at BIGINT NOT NULL, -- Single-use by default: max_uses 1. A team onboarding six people at once -- can raise it rather than minting six links. max_uses BIGINT NOT NULL DEFAULT 1 CHECK (max_uses > 0 AND max_uses <= 100), uses BIGINT NOT NULL DEFAULT 0, -- Revoked by hand, separately from expiry, so "this link is no longer -- wanted" and "this link timed out" stay distinguishable in the listing. revoked_at BIGINT ); CREATE INDEX invites_team_idx ON invites(team_id); -- Who redeemed which invite. Kept after the invite is gone — the answer to "how -- did this account get here" should outlive the link that made it. ALTER TABLE users ADD COLUMN invited_via BIGINT REFERENCES invites(id) ON DELETE SET NULL; -- Where a person is in the first-run checklist, so it can be resumed and -- dismissed rather than nagging forever. One row per user, created on demand. ALTER TABLE users ADD COLUMN onboarding_dismissed_at BIGINT;