Skip to content

feat: add USESEND_INVITE_ONLY flag for invite-restricted signup#398

Open
schemann wants to merge 1 commit into
usesend:mainfrom
schemann:feature/invite-only-flag
Open

feat: add USESEND_INVITE_ONLY flag for invite-restricted signup#398
schemann wants to merge 1 commit into
usesend:mainfrom
schemann:feature/invite-only-flag

Conversation

@schemann
Copy link
Copy Markdown

@schemann schemann commented May 12, 2026

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.


Summary by cubic

Adds a tri-state USESEND_INVITE_ONLY env flag to control new signups: open, waitlist, or invite-only hard block. Existing users and ADMIN_EMAIL can always sign in.

  • New Features
    • USESEND_INVITE_ONLY supports false (default), waitlist, and true.
    • Hard block in NextAuth signIn when set to true: no user record is created; only invited emails, existing users, and ADMIN_EMAIL are allowed.
    • Waitlist mode flags uninvited signups as isWaitlisted (and isBetaUser) for review.
    • Env validation updated in apps/web/src/env.js and documented in .env.example.

Written for commit e73c9e9. Summary will update on new commits.

Summary by CodeRabbit

  • New Features
    • Added configurable invite-only signup mode with three options: allow open signups (default), place uninvited users on a waitlist, or block uninvited sign-ups entirely.
    • Sign-in now validates user eligibility based on invite status when invite-only mode is enabled.
    • Admin users retain access regardless of invite configuration.

Review Change Stack

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) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 12, 2026

@schemann is attempting to deploy a commit to the kmkoushik's projects Team on Vercel.

A member of the Team first needs to authorize it.

Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 12, 2026

Walkthrough

This PR introduces invite-only and waitlist signup modes via a new USESEND_INVITE_ONLY environment variable. The variable accepts three string values: "false" (default, open signup), "waitlist" (allow signup but queue unregistered users), and "true" (only allow invited users). The configuration is validated in the environment schema, documented in .env.example, and enforced through a new NextAuth signIn callback that checks database invite records before permitting authentication. When in waitlist mode, the user creation event marks newly registered users without invites as beta users and waitlisted.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and concisely summarizes the main change: adding a USESEND_INVITE_ONLY flag to control invite-restricted signup behavior.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/src/server/auth.ts`:
- Around line 119-133: The signIn handler uses raw user.email for comparisons
and DB lookups, which can fail for mixed-case or spaced emails; normalize the
email once (e.g., const normalizedEmail = user.email.trim().toLowerCase()) at
the start of signIn and then use normalizedEmail for the env.ADMIN_EMAIL
comparison, for db.user.findUnique({ where: { email: ... } }) and
db.teamInvite.findMany lookups so checks succeed regardless of
casing/whitespace.
- Around line 152-157: The waitlist branch currently applies to every new user
when env.USESEND_INVITE_ONLY === "waitlist", which also catches the
bootstrap/admin account; update the condition to exempt the admin by checking
env.ADMIN_EMAIL and comparing it to user.email and only mark isWaitlisted when
the user is not the admin. Concretely, in the block that calls
db.user.update(...) (and uses invitesAvailable and user), if user.email ===
env.ADMIN_EMAIL then do not set isWaitlisted (you can still set isBetaUser as
needed), otherwise set isWaitlisted: true as before.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 217c1e17-df13-45cd-afc0-9e3fa5c877aa

📥 Commits

Reviewing files that changed from the base of the PR and between 964bbf9 and e73c9e9.

📒 Files selected for processing (3)
  • .env.example
  • apps/web/src/env.js
  • apps/web/src/server/auth.ts

Comment on lines +119 to +133
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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Normalize email before admin/invite gating checks.

Line 121 onward uses raw user.email for equality and DB lookups. Mixed-case emails can fail matches and incorrectly block allowed sign-ins.

Suggested fix
     signIn: async ({ user }) => {
       if (env.USESEND_INVITE_ONLY !== "true") return true;
-      if (!user.email) return false;
+      const normalizedEmail = user.email?.trim().toLowerCase();
+      if (!normalizedEmail) return false;

       const existing = await db.user.findUnique({
-        where: { email: user.email },
+        where: { email: normalizedEmail },
       });
       if (existing) return true;

-      if (env.ADMIN_EMAIL && user.email === env.ADMIN_EMAIL) return true;
+      if (
+        env.ADMIN_EMAIL?.trim().toLowerCase() === normalizedEmail
+      ) return true;

       const invites = await db.teamInvite.findMany({
-        where: { email: user.email },
+        where: { email: normalizedEmail },
       });
       return invites.length > 0;
     },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/server/auth.ts` around lines 119 - 133, The signIn handler uses
raw user.email for comparisons and DB lookups, which can fail for mixed-case or
spaced emails; normalize the email once (e.g., const normalizedEmail =
user.email.trim().toLowerCase()) at the start of signIn and then use
normalizedEmail for the env.ADMIN_EMAIL comparison, for db.user.findUnique({
where: { email: ... } }) and db.teamInvite.findMany lookups so checks succeed
regardless of casing/whitespace.

Comment on lines +152 to +157
if (env.USESEND_INVITE_ONLY === "waitlist" && !invitesAvailable) {
await db.user.update({
where: { id: user.id },
data: { isBetaUser: true, isWaitlisted: true },
});
return;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve ADMIN_EMAIL exemption in waitlist mode.

Line 152 waitlists every uninvited new user, including ADMIN_EMAIL. That conflicts with the stated exemption for admin sign-in/bootstrap.

Suggested fix
-      if (env.USESEND_INVITE_ONLY === "waitlist" && !invitesAvailable) {
+      if (
+        env.USESEND_INVITE_ONLY === "waitlist" &&
+        !invitesAvailable &&
+        user.email?.trim().toLowerCase() !== env.ADMIN_EMAIL?.trim().toLowerCase()
+      ) {
         await db.user.update({
           where: { id: user.id },
           data: { isBetaUser: true, isWaitlisted: true },
         });
         return;
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (env.USESEND_INVITE_ONLY === "waitlist" && !invitesAvailable) {
await db.user.update({
where: { id: user.id },
data: { isBetaUser: true, isWaitlisted: true },
});
return;
if (
env.USESEND_INVITE_ONLY === "waitlist" &&
!invitesAvailable &&
user.email?.trim().toLowerCase() !== env.ADMIN_EMAIL?.trim().toLowerCase()
) {
await db.user.update({
where: { id: user.id },
data: { isBetaUser: true, isWaitlisted: true },
});
return;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/server/auth.ts` around lines 152 - 157, The waitlist branch
currently applies to every new user when env.USESEND_INVITE_ONLY === "waitlist",
which also catches the bootstrap/admin account; update the condition to exempt
the admin by checking env.ADMIN_EMAIL and comparing it to user.email and only
mark isWaitlisted when the user is not the admin. Concretely, in the block that
calls db.user.update(...) (and uses invitesAvailable and user), if user.email
=== env.ADMIN_EMAIL then do not set isWaitlisted (you can still set isBetaUser
as needed), otherwise set isWaitlisted: true as before.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant