From e73c9e96ed915794d139668afe5cb1dff838f06e Mon Sep 17 00:00:00 2001 From: Daniel Schemann Date: Mon, 11 May 2026 12:16:49 +0200 Subject: [PATCH] feat: add USESEND_INVITE_ONLY flag for invite-restricted signup Adds a tri-state env var to control who can register: keep the current open behavior (false), route uninvited users to the waitlist (waitlist), or hard-block them in the signIn callback before any user record is created (true). Existing users and ADMIN_EMAIL can always sign in. Co-Authored-By: Claude Opus 4.7 (1M context) --- .env.example | 7 +++++++ apps/web/src/env.js | 4 ++++ apps/web/src/server/auth.ts | 24 ++++++++++++++++++++++++ 3 files changed, 35 insertions(+) diff --git a/.env.example b/.env.example index 79539fe0..71a5bdb3 100644 --- a/.env.example +++ b/.env.example @@ -26,3 +26,10 @@ AUTH_EMAIL_RATE_LIMIT=5 # REDIS_KEY_PREFIX="" NEXT_PUBLIC_IS_CLOUD=true + +# Restrict new signups. +# false (default) — anyone can sign up (cloud variant still routes uninvited users to the waitlist) +# waitlist — uninvited users always land on the waitlist for admin approval +# true — hard block: uninvited users are rejected at sign-in (no DB entry, no waitlist). +# Existing users and ADMIN_EMAIL can still sign in. +# USESEND_INVITE_ONLY=false diff --git a/apps/web/src/env.js b/apps/web/src/env.js index 9c37cdde..d6cf3692 100644 --- a/apps/web/src/env.js +++ b/apps/web/src/env.js @@ -71,6 +71,9 @@ export const env = createEnv({ .string() .optional() .transform((str) => (str ? parseInt(str, 10) : undefined)), + USESEND_INVITE_ONLY: z + .enum(["false", "waitlist", "true"]) + .default("false"), }, /** @@ -133,6 +136,7 @@ export const env = createEnv({ SMTP_USER: process.env.SMTP_USER, CONTACT_BOOK_ID: process.env.CONTACT_BOOK_ID, EMAIL_CLEANUP_DAYS: process.env.EMAIL_CLEANUP_DAYS, + USESEND_INVITE_ONLY: process.env.USESEND_INVITE_ONLY, }, /** * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially diff --git a/apps/web/src/server/auth.ts b/apps/web/src/server/auth.ts index ccd9d388..6155eedc 100644 --- a/apps/web/src/server/auth.ts +++ b/apps/web/src/server/auth.ts @@ -116,6 +116,22 @@ export const authOptions: NextAuthOptions = { isWaitlisted: user.isWaitlisted, }, }), + signIn: async ({ user }) => { + if (env.USESEND_INVITE_ONLY !== "true") return true; + if (!user.email) return false; + + const existing = await db.user.findUnique({ + where: { email: user.email }, + }); + if (existing) return true; + + if (env.ADMIN_EMAIL && user.email === env.ADMIN_EMAIL) return true; + + const invites = await db.teamInvite.findMany({ + where: { email: user.email }, + }); + return invites.length > 0; + }, }, adapter: PrismaAdapter(db) as Adapter, pages: { @@ -133,6 +149,14 @@ export const authOptions: NextAuthOptions = { invitesAvailable = invites.length > 0; } + if (env.USESEND_INVITE_ONLY === "waitlist" && !invitesAvailable) { + await db.user.update({ + where: { id: user.id }, + data: { isBetaUser: true, isWaitlisted: true }, + }); + return; + } + if ( !env.NEXT_PUBLIC_IS_CLOUD || env.NODE_ENV === "development" ||