An AI coding agent can draft a waitlist app, but a signup form alone cannot control who later sees a private release. This guide gives an owner a complete build specification: public signup, a durable queue, manual one-use invitations, verified-account redemption and a private resource tied to the right program. The specification is based on current official component documentation checked on 22 October 2026. We have not generated, run or deployed the app.
The big change
- What changed: The build brief covers the path from an exact-notice signup through owner selection to a current, program-specific private access grant.
- Why it matters: An applicant's checkbox, a sent code and a signed-in account are different facts. The app must check each relevant fact before admitting an entry or serving private content.
- What to watch: Generated auth routes, database lock behavior and the public host need hands-on checks with synthetic accounts before the owner collects real data.
Before you build
This is for an owner who has authority to decide what applicant information to collect, who qualifies for early access and how invitations will be sent. Define the actual product or resource, program purpose, eligibility, queue and seat capacities, hourly intake cap, notice, retention period, invitation lifetime and a human owner. Separately establish the applicable authority to collect data and contact applicants through the chosen channel. An affirmative checkbox records a choice for the exact notice and purpose; it alone does not establish legal authority to contact an applicant or marketing consent.
The proposed stack is Replit Agent, React, Express, Replit-managed Clerk, PostgreSQL and Node's crypto module. The app sends no email or text and provisions no external product. The owner sends invitations through an independently approved channel and records only an attestation of that attempt. Start with synthetic addresses and accounts. Check the services and their actual account prices, the public host, and the installed project before using real data. Use Node 22 or 24 only if the actual Development and published runtimes and locked dependencies support that version.
Build and check the complete flow
- Plan the whole app. Give the coding agent the full brief and contracts below. Ask it to plan the React interface, Express routes, SQL migration, managed identity integration and private resource gate before generating code. Review the generated dependency lockfile, migrations and route order. The installed managed Clerk proxy must be mounted on its actual generated paths before body parsers. Do not invent a callback or proxy path when the installed project differs from the documentation.
- Set up separate environments and ownership. Keep Development and Production identities and databases separate. Seed the first verified owner's Clerk ID into the SQL owner table through an audited administrative step. A public Clerk signup must never create a staff row. Publish a program's immutable notice and private content, then deliberately open signup. The real host must let prospects reach the public form and recipients reach sign-in; an invite-only host that blocks them defeats this design.
- Check public signup with synthetic addresses. Submit a current notice version, affirmative choice and UUID request key. Confirm one entry, its exact notice snapshot and a generic response that does not disclose whether an email was already present. Try a duplicate email with a new key, a same-key retry, a changed binding, an outdated notice and a missing choice. At both the queue and hourly cap, a genuinely new key for a known address and an unknown address must get the same generic 429 without a denied receipt, entry, event or quota charge. A previously committed same-key 202 must replay after a later cap, closure or notice change. Test a lock wait crossing a UTC hour against the bucket captured after the wait. A fresh-key duplicate at a full cap cannot confirm enrollment; the applicant checks their own status after sign-in with a currently verified matching email.
- Check selection and a manual invitation. An owner reviews the private queue against the written eligibility rule, selects an entry and issues one expiring code while reserving a seat. The server returns the raw code once, after commit, in an authenticated no-store response. It retains only a digest. The owner sends it outside the app through the approved channel and records a send attempt; that record does not prove delivery. A lost code is revoked and reissued with a new request key. Check expiry, seat capacity, revocation and history.
- Check recipient access without a staff row. A synthetic recipient signs in with a currently verified email matching their entry, checks their own status, pastes the code into the protected redemption form and reaches only their granted program's private page, API and assets. They must have no SQL staff row, and every owner route must deny them. Test an unverified or different email, a code replay, a missing delivery record, a signed-in account with no grant and two programs with different content. A grant to program A must not open program B. Withdraw or revoke a grant and check subsequent reads. Already delivered content cannot be recalled from a device.
- Check persistence and release. Inspect restricted history after restart, restore a Development backup to an isolated target and verify the notice, choice and grant links. Repeat public reachability and private denial checks with synthetic accounts on the actual Production host. The owner then reviews the generated implementation, applicable collection/contact authority, channel, retention, backups and release. A failed required check holds the whole app for repair and retest.
The expected result is a persistent app whose public and private routes pass those checks in the owner's environment. None of the signup, SQL, identity, concurrency, security, message delivery, backup or host results has been observed for this guide. Hosting, Agent, database and identity services may charge under the owner's account terms; there is no measured cost estimate here. Email addresses, Clerk IDs, token digests, notice snapshots and events remain restricted, linkable data even after withdrawal. Retention and backup expiry need an owner-approved procedure; withdrawal is not a claim of complete anonymization or legal erasure.
Appendix A: Copyable app brief
Give the following full brief, SQL and HTTP contract to the coding agent. The rules describe the app to build and test; they are not claims that a generated app already satisfies them.
Build brief: a waitlist and controlled early-access app
This is an original documentation-only brief for an owner using Replit Agent. Build the whole app: public signup and an affirmative choice tied to the exact contact notice; durable, deduplicated queue and status; private owner selection; bounded, one-use invitations with expiry, revocation and reissue; verified-recipient redemption; and a program-scoped private resource gated by a current grant. The owner must supply the real product purpose, eligibility policy, capacity, invitation wording, contact channel, retention terms and actual people. Begin with synthetic addresses and accounts. No app, message, provider account, race, bill or deployment was operated for this packet.
Give Agent this brief plus Appendices B and C and ask for a React UI, Express server, Replit-managed Clerk, and PostgreSQL. Inspect generated code, dependency lockfile, migration and host route behavior. Use Node 22 or 24 only when the actual development and published runtime and locked packages support that active/maintenance LTS line. Use separate Replit Development and Production identities and databases. Hosting and auth may cost money under the reader's actual account terms; no cost estimate is measured.
Reader-controlled purpose and public queue
An owner may create separate programs in one workspace. For each program the owner publishes an immutable notice version with an exact purpose and invitation-contact text; a fresh checkbox records the applicant's affirmative choice for that notice and purpose at signup. The checkbox alone does not establish legal authority to contact anyone: the owner separately determines and documents applicable collection and invitation-contact authority before real use. It supplies no marketing consent. A changed purpose requires a new program and a new choice, not quiet reuse of old entries. No automatic email or campaign is built. The public landing explains what is collected, contact purpose, waitlist status process, withdrawal route, and owner-approved retention period. Owner authorizes any real signup collection and invitations before release.
The applicant submits a bounded ASCII email (3–254 bytes, conservative local/domain grammar specified in the API), notice version, affirmative checkbox and UUID request key. Preserve local-part spelling and lowercase the domain only; treat that normalized string as the exact identity key, without dot/plus stripping or unsupported claims of mailbox ownership. Owner-defined queue capacity and UTC-hour intake cap use serialized database admission. For a new request key, check both caps before email dedupe: if either is full, every new-key attempt receives the same generic 429, including an address already on the waitlist; no receipt, queue entry or quota increment is recorded for the denied attempt. When capacity is available, duplicate email and new entry receive the same generic 202, and the duplicate consumes no new-entry quota. A committed retry with the same key and exact binding returns its prior 202 even if notice, cap or active state later changes; a key rebound to other fields receives 409. A fresh-key duplicate at capacity cannot confirm enrollment through signup; a signed-in applicant with a currently verified matching email checks their own status instead. Submission creates no Clerk account, staff membership, verified mailbox, invitation or access grant.
The owner may view bounded private queue pages, filter by status/time, inspect a record and its limited audit history, select an eligible waiting entry under the written policy, decline, or withdraw on documented applicant request. Applicant self-service status/withdrawal requires sign-in with a current Clerk verified email matching the exact queue email; avoid a public email lookup that reveals enrollment. A withdrawal immediately prevents pending invitation redemption and revokes an active grant. Owner records the request basis and handles real data rights under the approved retention policy; this app does not certify legal deletion.
Invitations and grant
The owner issues an invitation only to a selected entry with a recorded affirmative choice for its exact notice, after separately establishing applicable contact authority. The server reserves one seat under the program lock, produces 32 cryptographically random bytes, shows the base64url token once in a private no-store owner response, and saves only its SHA-256 digest. The owner uses an independently approved recipient channel to send that code to the queue address, then records a manual delivery attempt with timestamp and channel reference. That record means the owner says they sent it, not that a provider delivered it or the recipient read it. No outbound service, API, email, SMS or invitation is sent by this reference app. If a copy is lost, revoke and reissue; never expose the old code in a list, URL, log, event or analytics. The code expires at the owner-configured instant. Reissue revokes the previous issued invitation before reserving/replacing it; a revoked or expired code cannot redeem. The owner can cancel an invitation and free its reserved seat.
The recipient opens a token-free /redeem page, signs in through managed Clerk, and pastes the code into a bounded, CSRF-protected POST body. The server reads the backend Clerk user and requires a currently verified EmailAddress whose exact normalized value equals the entry email. Token possession or a Clerk signup alone is insufficient. Under the program lock it rechecks current invitation state, delivery record, entry, grant, expiry and capacity using the database clock, then consumes the invitation and creates one active grant bound to that Clerk user in one transaction. Every /early-access/:programSlug page/asset and /api/me/early-access/:programSlug resource request resolves that exact program on the server and checks the authenticated user ID, a matching currently verified email, an active grant and entry for the same program, its current access gate and workspace. A grant to another program cannot open this resource. The protected resource is owner-edited private launch content stored in this app. This does not provision an external product. Owner revocation immediately blocks subsequent reads; already delivered content cannot be recalled from a user's device.
Security, release and evidence limit
Mount the exact installed Replit-managed Clerk proxy paths before body parsers. Keep document-free public landing, signup, recipient sign-in/sign-up/callback, redeem shell, and required provider bootstrap/static transport outside SQL membership and custom app CSRF, while retaining Clerk's own protection. A signed-in public applicant still has no staff membership and no grant. Use separate path-scoped routers: owner queue, notice, invite, audit, content editor and admin HTML/API/assets/fallback require a current Clerk session and current SQL owner membership; recipient own-status, withdrawal, redemption and program-scoped private page/API/assets require a current Clerk session, matching backend-verified email and their route-specific own-entry or same-program current grant, without a staff-membership requirement. Recipient and owner mutations both keep narrow allowed Origin and session-bound CSRF. An absent, foreign, inactive-workspace or revoked owner gets a consistent non-enumerating response; unmatched HTML/API/asset paths deny by default rather than falling through between routers. The host must allow prospects to reach signup and recipients to authenticate; an invite-only host that blocks them fails this workflow. Protect app writes by bounded JSON, parameterized queries, escaped text, server-only secrets and Cache-Control: private, no-store on sensitive responses. Use a short fixed-format code only in a POST body, never a URL or query string. Avoid response/body logging, analytics and browser persistence of it. Do not use private source-provenance names in the app.
The owner sets data retention and backup expiry. Queue email, Clerk IDs, exact notice/choice snapshots, token digests and event metadata remain personal or linkable restricted data; an app redaction or withdrawal is not a complete anonymization or legal-erasure claim. No real collection starts until the owner approves the notice, applicable collection/contact authority, channel and retention. Human acceptance checks the source implementation and actual Development/Production host with synthetic accounts first: a matching verified recipient with no SQL staff row can view own status, withdraw, redeem and read only their granted program while owner routes deny; no-grant and cross-program private reads deny; known and unknown addresses get the same new-key response at both queue and hourly caps, committed same-key 202 survives a later cap, denied attempts store nothing, lock waits crossing UTC hours use the post-lock bucket, duplicate/retry, wrong identity, one-use and expired/revoked code, access revocation, immutable history and restore. If managed auth routing, verified-email lookup, locks or host exposure cannot be reproduced, hold the whole app rather than reducing it to a form.
Appendix B: Full PostgreSQL reference schema
Apply this as a reviewed migration to separate Development and Production databases. Inspect actual database privileges and migration behavior before relying on it.
-- PostgreSQL reference schema. Apply in a reviewed migration to separate Development and Production DBs.
-- App role cannot bypass API guards; revoke direct table access from browser/public roles.
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE TABLE workspaces (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name text NOT NULL CHECK (char_length(name) BETWEEN 1 AND 120),
active boolean NOT NULL DEFAULT true,
created_at timestamptz NOT NULL DEFAULT clock_timestamp()
);
CREATE TABLE staff_members (
workspace_id uuid NOT NULL REFERENCES workspaces(id),
clerk_user_id text NOT NULL CHECK (length(clerk_user_id) BETWEEN 1 AND 128),
role text NOT NULL CHECK (role IN ('owner')),
active boolean NOT NULL DEFAULT true,
PRIMARY KEY (workspace_id, clerk_user_id)
);
-- Seed the first owner only through an audited out-of-band owner-controlled bootstrap;
-- public Clerk registration NEVER inserts staff_members.
CREATE TABLE programs (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
workspace_id uuid NOT NULL REFERENCES workspaces(id),
public_slug text NOT NULL UNIQUE CHECK (public_slug ~ '^[a-z0-9-]{3,60}$'),
purpose text NOT NULL CHECK (char_length(purpose) BETWEEN 1 AND 1000),
signup_open boolean NOT NULL DEFAULT false,
access_enabled boolean NOT NULL DEFAULT false,
queue_cap integer NOT NULL CHECK (queue_cap BETWEEN 1 AND 100000),
hourly_cap integer NOT NULL CHECK (hourly_cap BETWEEN 1 AND 100000),
seat_cap integer NOT NULL CHECK (seat_cap BETWEEN 1 AND 100000),
revision bigint NOT NULL DEFAULT 1 CHECK (revision>0),
current_notice_id uuid,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
UNIQUE (id, workspace_id)
);
CREATE TABLE notices (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
program_id uuid NOT NULL REFERENCES programs(id),
version integer NOT NULL CHECK (version > 0),
purpose text NOT NULL CHECK (char_length(purpose) BETWEEN 1 AND 1000),
contact_text text NOT NULL CHECK (char_length(contact_text) BETWEEN 1 AND 2000),
retention_text text NOT NULL CHECK (char_length(retention_text) BETWEEN 1 AND 1000),
published_at timestamptz NOT NULL DEFAULT clock_timestamp(),
UNIQUE (program_id, version), UNIQUE (id, program_id)
);
ALTER TABLE programs ADD CONSTRAINT current_notice_same_program
FOREIGN KEY (current_notice_id, id) REFERENCES notices(id, program_id);
CREATE TABLE entries (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
program_id uuid NOT NULL REFERENCES programs(id),
email_key text NOT NULL CHECK (length(email_key) BETWEEN 3 AND 254),
notice_id uuid NOT NULL,
permission_at timestamptz NOT NULL,
state text NOT NULL CHECK (state IN ('waiting','selected','invited','active','declined','withdrawn','revoked')),
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
updated_at timestamptz NOT NULL DEFAULT clock_timestamp(),
UNIQUE (program_id,email_key), UNIQUE (id,program_id),
FOREIGN KEY (notice_id,program_id) REFERENCES notices(id,program_id)
);
CREATE TABLE signup_receipts (
program_id uuid NOT NULL REFERENCES programs(id),
request_key uuid NOT NULL,
binding_hmac bytea NOT NULL CHECK (octet_length(binding_hmac)=32),
response_code integer NOT NULL DEFAULT 202 CHECK (response_code=202),
accepted_at timestamptz NOT NULL DEFAULT clock_timestamp(),
PRIMARY KEY (program_id,request_key)
);
CREATE TABLE intake_hours (
program_id uuid NOT NULL REFERENCES programs(id),
bucket_start timestamptz NOT NULL,
admitted_count integer NOT NULL CHECK (admitted_count>=0),
PRIMARY KEY (program_id,bucket_start)
);
CREATE TABLE invitations (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
program_id uuid NOT NULL,
entry_id uuid NOT NULL,
token_sha256 bytea NOT NULL UNIQUE CHECK (octet_length(token_sha256)=32),
state text NOT NULL CHECK (state IN ('issued','redeemed','revoked','expired')),
issued_at timestamptz NOT NULL,
expires_at timestamptz NOT NULL CHECK (expires_at>issued_at),
delivery_recorded_at timestamptz,
delivery_channel text CHECK (delivery_channel IS NULL OR char_length(delivery_channel) BETWEEN 1 AND 80),
delivery_reference text CHECK (delivery_reference IS NULL OR char_length(delivery_reference) BETWEEN 1 AND 160),
terminal_at timestamptz,
FOREIGN KEY (entry_id,program_id) REFERENCES entries(id,program_id),
UNIQUE (id,entry_id,program_id),
CHECK ((delivery_recorded_at IS NULL) = (delivery_channel IS NULL)),
CHECK (delivery_reference IS NULL OR delivery_recorded_at IS NOT NULL)
);
CREATE UNIQUE INDEX one_issued_invite_per_entry ON invitations(entry_id) WHERE state='issued';
CREATE TABLE owner_action_receipts (
program_id uuid NOT NULL REFERENCES programs(id),
request_key uuid NOT NULL,
action text NOT NULL CHECK (action IN ('issue_invitation','record_delivery')),
binding_hmac bytea NOT NULL CHECK (octet_length(binding_hmac)=32),
target_id uuid NOT NULL,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
PRIMARY KEY (program_id,request_key)
);
CREATE TABLE grants (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
program_id uuid NOT NULL,
entry_id uuid NOT NULL,
invitation_id uuid NOT NULL UNIQUE,
clerk_user_id text NOT NULL CHECK (length(clerk_user_id) BETWEEN 1 AND 128),
active boolean NOT NULL DEFAULT true,
granted_at timestamptz NOT NULL,
revoked_at timestamptz,
FOREIGN KEY (entry_id,program_id) REFERENCES entries(id,program_id),
FOREIGN KEY (invitation_id,entry_id,program_id) REFERENCES invitations(id,entry_id,program_id),
UNIQUE (entry_id), UNIQUE (program_id,clerk_user_id),
CHECK ((active AND revoked_at IS NULL) OR (NOT active AND revoked_at IS NOT NULL))
);
CREATE TABLE early_access_content (
program_id uuid PRIMARY KEY REFERENCES programs(id),
revision bigint NOT NULL DEFAULT 1 CHECK (revision>0),
body text NOT NULL CHECK (char_length(body) BETWEEN 1 AND 20000),
updated_at timestamptz NOT NULL DEFAULT clock_timestamp()
);
CREATE TABLE audit_events (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
program_id uuid NOT NULL REFERENCES programs(id),
entry_id uuid,
actor_kind text NOT NULL CHECK (actor_kind IN ('owner','recipient','public','system')),
actor_clerk_id text CHECK (actor_clerk_id IS NULL OR length(actor_clerk_id) BETWEEN 1 AND 128),
event_type text NOT NULL CHECK (event_type IN ('notice_published','signup','selected','declined','invite_issued','delivery_recorded','invite_revoked','invite_expired','redeemed','grant_revoked','withdrawn','content_updated','program_changed')),
event_at timestamptz NOT NULL,
details jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(details)='object' AND pg_column_size(details)<=512),
FOREIGN KEY (entry_id,program_id) REFERENCES entries(id,program_id)
);
CREATE INDEX entries_queue_idx ON entries(program_id,state,created_at,id);
CREATE INDEX audit_program_idx ON audit_events(program_id,event_at,id);
CREATE INDEX invite_entry_idx ON invitations(entry_id,issued_at DESC);
CREATE FUNCTION block_immutable_reference_edits() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN RAISE EXCEPTION 'immutable reference row'; END $$;
CREATE TRIGGER notices_immutable BEFORE UPDATE OR DELETE ON notices
FOR EACH ROW EXECUTE FUNCTION block_immutable_reference_edits();
CREATE TRIGGER audit_immutable BEFORE UPDATE OR DELETE ON audit_events
FOR EACH ROW EXECUTE FUNCTION block_immutable_reference_edits();
-- DB permissions also deny direct UPDATE/DELETE of immutable rows to the application role.
Appendix C: Full HTTP, transaction and interface contract
HTTP, authorization and transaction contract
Implement these exact semantics in the generated Express server; UI is an untrusted client. Use parameterized PostgreSQL queries. All writes use READ COMMITTED, one transaction, and abort on a failed predicate; do not do provider calls inside a DB transaction. Every sensitive response is Cache-Control: private, no-store; responses and access logs must omit email, token, Clerk ID, raw request body and contact content. No app secret reaches React. Store request-binding HMAC secret server-side, separate from the token generator. Keep one session-bound custom CSRF token and exact allowed Origin on authenticated app writes. The unauthenticated signup uses a narrow public Origin and same-site form protection/rate control, but not staff membership/custom staff CSRF. Limit JSON to 32 KiB UTF-8 bytes; independently enforce each listed field bound. Reject malformed JSON before route logic.
Mount order and identity
Mount the installed Replit-managed Clerk proxy on its generated paths before any body parser, then Clerk middleware and exact public auth/bootstrap/static transport. Retain provider security. Public HTML /, /waitlist/:slug, /redeem and provider sign-in/sign-up/callbacks contain no private data. Mount narrowly allowed public signup outside app membership. Then mount two separate, path-scoped authenticated routers, never a global SQL staff-membership wrapper: (1) /api/me/waitlist/:slug own-status/withdrawal, /api/me/invitations/redeem, /early-access/:programSlug, /api/me/early-access/:programSlug and their private assets require a current Clerk session plus the route-specific backend verified-email and own-entry or same-program grant checks below, without any SQL staff row; apply exact allowed Origin and session-bound custom CSRF to their mutations; (2) every /api/owner/..., owner HTML/content editor, admin asset and owner SPA fallback requires current SQL owner membership in the same active workspace, with Origin/CSRF on writes. Route-specific guards run before any private body/content is served. After these explicit routes, an unmatched HTML/API/asset fallback denies by default with generic 404; do not expose an unprotected wildcard or let a recipient route fall through to owner middleware. Replit public host is necessary for public signup and recipient authentication; verify actual Development and Production host routing. A Clerk signup is identity only: it creates neither staff membership nor a grant.
For owner router only, getAuth(req) must yield a verified session/user ID. Start transaction, lock workspace FOR SHARE, then read current active staff row with role owner in a fresh statement. Missing/foreign/inactive workspace or revoked membership returns the same generic 404; do not reveal whether an ID exists. Owner-only policy denial inside a valid workspace is 403. Every owner writer locks workspaces FOR UPDATE, then programs FOR UPDATE, then target entry/invitation/grant in that order and rechecks membership after acquiring workspace lock. Public signup locks the workspace and program in that order, without requiring staff membership. Recipient router never queries staff_members as an admission requirement. It authenticates Clerk and calls backend getUser(userId); match an email address whose backend verification.status === 'verified' and whose exact normalized value equals entries.email_key for the recipient's own record or current same-program grant. A missing backend response, unverified address, mismatch or invalid token denies without fallback to session claims or token possession. Cache no verification across a later access request; if Clerk is unavailable, fail closed and offer retry. Do not treat backend verification as proof of a specific human beyond the provider's account controls.
Email grammar: trim surrounding ASCII space only; reject internal whitespace, control/non-ASCII, length outside 3–254 UTF-8 bytes, more/less than one @, or a local part outside this character class:
[A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]Also reject a local part >64 bytes, leading/trailing/consecutive dot, or a domain outside ASCII DNS labels of 1–63 [A-Za-z0-9-] with no edge hyphen, no dot, or >253 bytes. Preserve local part exactly; lowercase domain with ASCII rules. This intentionally rejects some valid email forms; disclose that restriction rather than silently changing addresses. Use the same function for public input and Clerk backend value. No plus/dot/case folding of local part, no claimed mailbox ownership at signup.
Public signup and status
GET /waitlist/:slug returns only current public notice and form version. POST /api/public/programs/:slug/signups body {requestKey:UUID,email:string,noticeVersion:int,permission:true}. This boolean records the applicant's affirmative choice for the specified notice and purpose; it does not establish legal contact authority or marketing consent. The owner separately determines applicable collection and invitation-contact authority. Canonicalize validated fields as length-delimited bytes (program id, UUID, normalized email, notice version, boolean) and compute server HMAC-SHA256; never store a plain email-derived digest in the request receipt. Start transaction and lock workspace FOR UPDATE, then program FOR UPDATE. After any lock wait and before active/version/cap/dedupe denials, read (program_id,request_key) from signup_receipts; same binding returns stored generic 202, different binding 409. This repeat is required even if a preflight key read saw nothing. Only for a genuinely new key, capture one database clock_timestamp() after locks and key read, and derive date_trunc('hour', clock_timestamp_value AT TIME ZONE 'UTC') AT TIME ZONE 'UTC' for the UTC bucket. Check workspace active, signup_open, current notice version and affirmative choice; closed/outdated form gives generic 409/reload independent of whether that email exists. Before looking up (program,email_key), read queue occupancy (all non-withdrawn entries; declined records remain counted) and the current UTC-hour admitted_count. If either cap is exhausted, roll back and return the same generic 429 for every genuinely new request key, whether the email already exists or not. Do not insert a receipt, entry, event or hour increment for this denial. Only below both caps check existing (program,email_key) under the program lock: duplicate email gets generic 202 and a committed request receipt without altering its original notice, choice snapshot or state or consuming new-entry quota. New email inserts entries with current notice and exact choice time, increments the hour bucket using INSERT ... ON CONFLICT DO UPDATE ... RETURNING with count still <= cap, inserts signup receipt and audit event, then commits. On unique conflict caused by an unforeseen writer, rollback and retry once from lock and key lookup; never treat a conflict as permission to bypass cap. A server crash before commit leaves no receipt/entry/count; after commit same key returns stored 202 even after a later cap, closure or notice change. A fresh-key duplicate at a full cap also gets 429, so it cannot use signup as an enrollment confirmation; the applicant signs in with a matching currently verified email to check only their own status. When capacity later opens, an unrecorded denied key is a new attempt. No 202 is returned for a dropped new signup.
GET /api/me/waitlist/:slug and POST /api/me/waitlist/:slug/withdraw require Clerk authentication and backend verified email. Return only matching entry/status, with no global lookup; no match is generic 404. Withdrawal is CSRF/Origin protected. Lock workspace/program/entry; recheck verified identity and current state. Set withdrawn, revoke any issued invite and active grant in the same transaction, append restricted event. Repeated own withdrawal yields stable withdrawn status. If mailbox verification changes, fail closed. It does not delete audit or backup copies; follow owner-approved retention handling.
Owner program, selection and invitation
POST /api/owner/programs creates a new program in an active owned workspace with a unique validated slug, immutable purpose and positive queue/hour/seat caps, all under the workspace lock. The exact workspace ID is taken from current owner membership/body and checked, never inferred from signup. The new program is closed and has no current notice or private content until owner publishes both; purpose change requires a separately reviewed new program. GET /api/owner/programs/:id returns bounded program/notice/cap summary. POST /api/owner/programs/:id/notices publishes sequential immutable notice versions with owner-supplied purpose/contact/retention; purpose must equal the program purpose, otherwise require a new program. PATCH /api/owner/programs/:id updates signupOpen, accessEnabled, queueCap, hourlyCap, seatCap with a bigint expected revision under locks; seatCap may not fall below active grants plus unexpired issued invitations. Open signup only with current notice; enable access only with private content. Lowering queue/hour cap does not retroactively remove entries. GET /api/owner/programs/:id/entries?state=&cursor=&limit= and /entries/:id return only owner-scoped bounded lists and exact notice/history; use an opaque cursor containing only IDs/time in the query, never email. Keep private emails out of logging. POST /entries/:id/select requires waiting and records an explicit owner eligibility reason code in a bounded private event, not arbitrary free text. POST /entries/:id/decline requires waiting/selected, no issued invite or grant. If owner acts on requested withdrawal, use the same withdrawal transition.
POST /api/owner/programs/:id/entries/:entryId/invitations body {requestKey:UUID,expiresAt:ISO8601}. Require selected entry or an invited entry whose sole issued invitation has expired; require its original affirmative choice for the exact notice/purpose, plus the owner's separately established contact authority, future expiry within an owner-set maximum of 1–30 days and no active grant. Store an owner_action_receipts row with HMAC binding of action, program ID, entry ID and expiry. After workspace/program lock waits, check this key before mutable status/cap/expiry checks; same key and binding returns existing invitation metadata only, changed binding 409. Never re-display the token. To recover an unrecorded lost token, owner revokes/reissues with a new key. Lock workspace→program→entry→current invitation. Capture DB clock after waits. Change an expired issued invite to expired and entry to selected (event); a still-live one needs a separate explicit revoke before reissue. Count active grants plus unexpired issued invitations, including this entry only once, and require < seatCap for a new reservation. Generate 32 bytes via Node crypto.randomBytes, encode base64url without padding, compute createHash('sha256') over the decoded bytes, insert digest, set entry invited, issued/expiry times, event and idempotency receipt, commit; only then return raw code once to the owner over an authenticated no-store response. If the response is lost, the code is unrecoverable. Do not generate a second code by retrying the same key. Any unique conflict rolls back and re-reads the key/current state under locks before deciding; never double-reserve.
POST /api/owner/invitations/:id/delivery-record body {requestKey:UUID,channel:string,reference?:string} records a human-attested approved send attempt once; do not accept a token or recipient address in this request. Store the owner_action_receipts HMAC binding of action, program ID, invitation ID, channel and reference; repeat key lookup after waits before mutable state checks. A repeat same key/binding returns metadata, changed binding 409; another delivery claim is rejected absent a new reviewed policy. This does not send or verify delivery. POST /api/owner/invitations/:id/revoke revokes an issued unredeemed invite and returns entry to selected; do not revoke a redeemed invitation to revoke access—use grant revoke. Reissue is a fresh issue with a new key after revocation/expiry. All actions require current owner and scoped program/entry checks; do not trust nested IDs from path alone. Record actor/time/state in audit.
Redeem, private content and revocation
POST /api/me/invitations/redeem body {code:base64url} where decoded length is exactly 32 bytes (43 unpadded base64url characters). Authenticated Clerk session and CSRF/Origin required. Do backend verified-email lookup before DB transaction. Hash decoded bytes, start transaction, find invitation by digest only for lock routing, lock workspace→program→entry→invitation→possible grant, then recheck digest, current user verified email against entry, delivery record, invite issued, entry invited, program/workspace active and DB clock_timestamp() < expires_at. Keep error text generic for absent/foreign/unverified/revoked/used/expired codes; rate-limit attempts by account/network without exposing counts. The issued invitation already reserves its seat; the transaction changes it to redeemed, inserts exactly one active grant for the same program+entry+user, changes entry active and appends event. Uniqueness enforces one grant per entry and one account per program. Same code replay cannot create another grant and returns generic denial. A unique conflict rolls back and re-reads the committed state; it never issues a second grant. If owner revokes/withdraws first, redemption fails; if redemption commits first, owner grant revocation is next.
GET /early-access/:programSlug and GET /api/me/early-access/:programSlug serve owner-edited private content for only the named program. Validate slug grammar; resolve its program ID and workspace ID server-side, never from a client-supplied workspace or grant ID. Require current Clerk user plus backend matching verified email. In a READ COMMITTED transaction lock that workspace FOR SHARE, then that program FOR SHARE; after any wait, recheck the same slug→program→workspace binding and current active workspace/program. In a fresh scoped statement join early_access_content.program_id = programs.id, entries.program_id = programs.id, and grants.program_id = programs.id AND grants.entry_id = entries.id; require programs.id = resolvedProgramId, programs.public_slug = :programSlug, programs.workspace_id = resolvedWorkspaceId, programs.access_enabled, entries.state = 'active', grants.active, grants.clerk_user_id = authenticatedUserId, and entries.email_key equal to one of that user's currently verified normalized Clerk emails. Select content only from that same program. Never fall back to another program's grant or content, even if the same user belongs to both. All writer transitions take workspace/program FOR UPDATE; if reader gets lock first, it may finish the already-authorized response before revocation; if writer commits first, reader is denied. Hold shared locks through bounded content materialization/serialization; do not stream after unlock. Scope any private page assets to the same program gate; public static bundles contain no private content. Do not use a public service-worker cache. Any failed check gives generic 404, no private content. Owner PUT /api/owner/programs/:id/content changes text with expected revision under locks; render escaped text, do not execute HTML. Owner POST /api/owner/grants/:id/revoke atomically marks grant inactive, entry revoked, appends history, and blocks subsequent reads. Reissue after a redeemed grant is not allowed in this design; owner must define a reviewed new program for a new eligibility cycle.
Global invariants, errors and acceptance
All program-changing writers lock workspace then program FOR UPDATE, followed by entry, invitation, grant; never acquire in reverse order. Owner membership/revocation writer also takes workspace FOR UPDATE before changing staff. No physical deletion of rows referenced by permission/history; use bounded owner retention/redaction procedure with separate reviewed policy and backup expiry. notices and audit_events are append-only via DB trigger and app grants. Events contain IDs/status/timestamps and controlled reason codes only, no token, code, email, message body, raw IP or user agent; constrain JSON to the event-type allowlist in server code and inspect serialized events before commit. IDs/digests remain linkable and restricted. Owner UI shows notice snapshot, status, delivery-record meaning and append-only changes; recipient UI shows only own status, manual paste and granted private page. Every mutation produces a checked response and fresh readback. 400 malformed/bounds, 401 signed out, 403 authenticated valid workspace but wrong owner action, uniform 404 absent/foreign/revoked private object, 409 revision/key binding/state, 429 cap/attempt rate, 503 provider unavailable. No endpoint's status/body identifies whether a public email joined.
Human acceptance with synthetic accounts: no staff from Clerk signup; an authenticated recipient with matching verified email and no staff_members row can view only their own status, withdraw, redeem when eligible and read only their granted program's page/API/private asset, while every owner route denies them; a signed-in no-grant recipient and cross-program grant are denied private content; current owner and revoked owner; public host signup while owner APIs denied; exact notice/affirmative-choice snapshot and separately owner-checked contact authority; duplicate address and same/different-key retry after closed/version/cap change; concurrent new signups and UTC-hour-crossing wait; selection/decline/withdraw; one seat and concurrent invite issue; one-time display and lost-response recovery; recorded-send vs actual-delivery distinction; wrong/unverified email, token replay, expired/revoked/reissued invitation; concurrent owner revocation vs redeem; current verified email removed after grant; program gate off; two programs with different private content and grants: program A's grant is denied at program B's page/API/private asset and vice versa, with no client workspace/grant selector; content not in public assets; restart and backup restore; Development and Production isolation. These are tests for the reader to run, not observed results of this packet.
Sources & further reading
These official references document components and syntax, not a completed waitlist app. The checks above remain for the reader to run.
- Replit Agent overview: Agent can plan and generate a project. It does not guarantee that this app or its access rules will be generated correctly.
- Replit managed Clerk authentication: Managed proxy and identity integration. The installed proxy paths, mount order and recipient flow need inspection in the generated project.
- Replit Development and Production data: Separate environments are documented. The reader must check actual database and identity bindings.
- Replit private deployments: Host access controls exist. A host that blocks anonymous prospects or recipient sign-in would fail this workflow; actual access is untested.
- Clerk Express getAuth: Reads request authentication state. It does not confer SQL owner membership or an access grant.
- Clerk Express clerkMiddleware: Provides auth context to Express. Route-specific SQL and email checks remain app responsibilities.
- Clerk backend getUser: Fetches the current user by ID for a fresh server-side email check. No particular human identity is proven by the guide.
- Clerk backend User: Documents backend user email address fields; the generated integration and failures remain to check.
- Clerk backend EmailAddress: Documents email address records used for exact matching. Signup input by itself does not verify a mailbox.
- Clerk backend Verification: Documents verification status; the app must check it on each protected recipient request.
- PostgreSQL INSERT: Supports insert and conflict operations used in the proposed transaction contract. No migration was executed.
- PostgreSQL explicit locking: Supports row lock primitives. The proposed writer and reader race behavior requires reader testing.
- PostgreSQL constraints: Supports composite foreign keys, uniqueness and checks used in the reference schema. It does not prove generated route authorization.
- PostgreSQL date/time functions: Supports database clock and UTC-hour operations. The cap rule is a proposed app policy.
- PostgreSQL transaction isolation: Supports the fresh statement requirement under READ COMMITTED after a lock wait. Concurrency results remain unobserved.
- Node.js crypto: Documents random bytes and SHA-256 primitives. Token handling and log exclusion remain to inspect.
- Node.js release status: At this check, Node 22 and 24 were LTS lines. Actual Replit runtime and locked dependency support require verification.



