The hardest part of running an invite-only beta isn't deciding who to let in. It's deciding what to do when the wrong person tries to sign up — gracefully enough that you don't burn them, firmly enough that the gate isn't a sieve.
Our gate is three booleans and a lookup table. It's been running for ~three months at the time of writing. Some thousand emails have hit it. The false-positive rate is low; the false-negative rate is zero (no one we wanted to keep out has gotten in). This piece is the gate, the false positives we've seen, and the one rule we follow that keeps the gate honest.
The shape
When someone enters their email at /login, we run canSignUp(email). The function returns one of three states:
- Allow — let them in.
- Waitlist — accept the email, return
{ waitlisted: true }, and add them to awaitlist_signupstable for later promotion. - Reject — never; we don't have this state. We don't reject anyone outright. Even unrecognized emails get added to the waitlist.
The function is implemented as:
function canSignUp(email: string, opts: { inviteToken?: string }): SignUpResult {
// 1. Existing user? Always allow login.
if (await prisma.user.findUnique({ where: { email } })) {
return { allow: true };
}
// 2. Allowlisted email? Allow.
if (allowlist.has(email)) {
return { allow: true };
}
// 3. Carries a valid invite token? Allow + accept token.
if (opts.inviteToken && await isValidInvite(opts.inviteToken, email)) {
return { allow: true };
}
// 4. Otherwise → waitlist.
await waitlistSignup(email);
return { waitlisted: true };
}
That's the whole gate. Three checks, then waitlist by default.
The three booleans
The first check is whether the email is already a user. We always let users back in. We never accidentally lock someone out.
The second check is the allowlist. The allowlist is a Postgres table with one column: email. Anyone an admin has invited goes in here. We have a small admin UI for adding emails; mostly we paste them in batch when a friend asks "can my five teammates sign up?"
The third check is the invite token. Tokens are signed strings carrying an inviter's user ID + a timestamp + a nonce. They expire after 14 days. They're single-use (consumed on signup). We hand them out via /invite/[code] shareable URLs. The /login page reads the token from the URL and forwards it to canSignUp.
The combination is enough. Existing users always get in. People we know get in. People with a friend's invite token get in. Everyone else waits.
The waitlist tier
The fourth state — waitlist — is the most important part of the gate. Most invite-only systems treat unknown signups as a hostile event. They show an error, a captcha, a "you can't sign up." This burns the user.
We accept the signup. We tell them they're waitlisted. We add their email to a waitlist_signups table with a timestamp. From their perspective, they've signed up and now they wait. From our perspective, we have their email, the moment they tried to sign up, and a clean record we can promote later.
When we want to let waitlist signups in, an admin runs:
UPDATE waitlist_signups SET invited_at = now() WHERE email IN (...);
The invited_at non-null is the promotion. Next time the user enters their email, the allowlist check finds them and lets them in.
This pattern has been net positive. We've promoted roughly 40% of the waitlist signups so far, in batches based on whether they fit our beta priorities. The other 60% are still waiting; they're not angry because they were told they're waiting.
The false positives
A few patterns of false positive we've seen:
The friend forwarded the invite link. Alice gets an invite. Alice forwards the link to Bob. Bob clicks. The invite token is valid. Bob signs up. We didn't intend Bob — we intended Alice — but Bob is in.
The fix here is making invite tokens email-bound. We do this for invites that target a specific email (the email is part of the signed payload). For open-link invites (the kind you generate to share with anyone), the gate is intentionally permissive. We accept that any open-link invite can be forwarded.
The corporate email auto-replies cancel the magic link. When we email a magic link, some corporate mailservers auto-reply with "out of office" or scrape the URL for previewing. The preview consumes the magic link. The user clicks the link in their inbox; it's already used.
The fix is making magic links idempotent until first authentication. We let the link be "consumed" by the GET request that the auto-replier makes, but only fully consumed when the user actually authenticates from the resulting session. This is a small change with outsized impact on the corporate-user funnel.
The waitlist-signup typo. A user types goivnd@vector.build instead of govind@vector.build. They're now on the waitlist under a typo. Later they try to sign up correctly; they're treated as a new user, get waitlisted again. We catch this only via the support thread.
The fix is fuzzy matching when a user complains. We don't auto-merge typo emails — too risky. But our support tooling shows recent waitlist signups whose emails are within 2 edit distance of the requesting email, and we manually reconcile.
The one rule that keeps us honest
The rule: the waitlist must be a real waitlist. We must actually promote people from it. If we waitlist 1000 people and never look at them, the gate has stopped being honest.
Internally, we have a recurring weekly task: "review the waitlist." Two of us look at the new signups, scan for known names / domains we recognize, promote in batches. The cadence isn't optional. If it slips, the waitlist becomes a graveyard, and our claim that "we're letting people in over time" stops being true.
This is the rule because the alternative is the dark pattern: gate everyone, never promote, hide behind "invite-only" forever. Plenty of products do this. We don't want to.
What we'd do differently
A few things we'd build differently if we shipped the gate fresh:
Make the allowlist hierarchical. "Allow this domain" + "specific email overrides" would help with company onboarding. We currently have the email-level allowlist only.
Put the waitlist position visible. We tell users "you're waitlisted" but not "you're number 412 in line." Some products show position; we could too. The trade-off is creating a perceived queue when our promotion logic isn't actually FIFO.
Tighten open-link invite scope. Open-link invites can be forwarded indefinitely. We could add max-uses defaults (the field exists but isn't required) so an open link decays to "spent" after a small N.
These aren't urgent. The gate works. But they'd reduce the load on the support team.
What this composes with
The magic-link gate is one piece of how we handle agent and human authentication. It composes with:
- Agent identity. Agent users bypass the gate entirely — they're created by their human owners, not by signup.
- Org invites. A separate flow for adding teammates to an existing org. Org invites have their own token shape (longer-lived, max-uses configurable, email-bound or open-link).
- Workspace invites. A third flow for sharing a single workspace. The narrowest scope.
Each gate has its own table, its own token shape, its own expiration semantics. Centralizing them would have been a fun refactor; keeping them separate is the right call because their failure modes are different.
Cross-links
- AI-agent-first primitives — the broader pattern of building access into the substrate.
- OAuth scopes for agents — how agent permissions work after they get past this gate.
FAQ
What is a magic-link gate?
A signup flow that uses a one-time email-delivered link instead of a password. The gate is the logic that decides whether an unknown email gets in, gets waitlisted, or gets a different response. Our gate is allow-existing-users, allow-allowlist, allow-with-invite-token, otherwise waitlist.
Why waitlist instead of reject?
Rejecting unknown signups burns users — they think you don't want them. Waitlisting accepts them gracefully, captures their email, and gives you a way to invite them later when you have capacity. The waitlist becomes a pipeline of real interest.
Are invite tokens single-use?
Yes. Once a token is used to sign someone up, it's marked consumed. We don't allow re-use. For open-link invites (no email binding), the token can have a max-uses cap so it decays to spent after N signups.
How do you handle invite-link forwarding?
Email-bound invites only let the targeted email sign up — forwarding doesn't help, the email check fails. Open-link invites are intentionally permissive; we trade tighter security for the ergonomic of "share this link with your team."
How do you decide who to promote off the waitlist?
We don't auto-promote. A weekly review batch scans new signups, identifies known domains / referrers, and promotes in groups. The cadence is the discipline that keeps the waitlist honest — without it, the gate would silently become a wall.
{
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": "Inside the magic-link gate",
"description": "Every invite-only beta has a gate. Ours is three bools, a lookup table, and one rule that keeps the team honest.",
"datePublished": "2026-04-26",
"author": { "@type": "Person", "name": "Govind" },
"publisher": { "@type": "Organization", "name": "Dock", "url": "https://trydock.ai" },
"image": "https://trydock.ai/blog-mockups/style-d-dreamscape/inside-the-magic-link-gate.webp",
"mainEntityOfPage": "https://trydock.ai/blog/inside-the-magic-link-gate"
}