Skip to content

feat(auth): add invite-only signup mode for production deployments#28

Open
Matt Van Horn (mvanhorn) wants to merge 1 commit into
ComposioHQ:mainfrom
mvanhorn:feat/trustclaw-invite-only-signup
Open

feat(auth): add invite-only signup mode for production deployments#28
Matt Van Horn (mvanhorn) wants to merge 1 commit into
ComposioHQ:mainfrom
mvanhorn:feat/trustclaw-invite-only-signup

Conversation

@mvanhorn

Copy link
Copy Markdown
Contributor

Summary

Closes the smallest of the three production gaps the README's "Before deploying to production" block calls out:

"If you put a TrustClaw instance on the public internet for strangers to sign up to, any user can drain your Composio + AI Gateway credits indefinitely. Before opening signups to anyone but yourself / a trusted handful of people, add at least: ... Billing or invite-only signup if you want to recoup costs"

Adds opt-in invite-only signup: a new ALLOW_OPEN_SIGNUP env flag (default true, so existing deploys are unaffected), an InviteCode Prisma model, a Better Auth databaseHooks.user.create.before hook that claims a code atomically at signup, and a minimal /dashboard/admin/invites page guarded by ADMIN_USER_EMAIL for minting and revoking codes.

Behavior change

  • ALLOW_OPEN_SIGNUP=true (default): identical to today's behavior. No migration-required redeploy.
  • ALLOW_OPEN_SIGNUP=false + ADMIN_USER_EMAIL set: signup rejected without a valid invite code. Login form surfaces an "Invite code" field with a clear error message on rejection.
  • Race-safe: invite code is claimed via prisma.inviteCode.updateMany with WHERE usedAt IS NULL AND (expiresAt IS NULL OR expiresAt > now). Concurrent signups can't reuse the same code.

What changed

  • prisma/schema.prisma + new migration: InviteCode table (code, createdAt, createdBy, expiresAt, usedAt, usedByUser, note).
  • src/env.ts: ALLOW_OPEN_SIGNUP (boolean, default true) and ADMIN_USER_EMAIL (optional email).
  • src/server/auth.ts: databaseHooks.user.create.before hook; throws APIError on missing/invalid/expired code.
  • src/server/api/trpc.ts: new adminProcedure (session.user.email must match ADMIN_USER_EMAIL).
  • src/server/api/routers/trustclaw/admin/: createInviteCode, listInviteCodes, revokeInviteCode procedures + schemas.
  • src/app/(authenticated)/dashboard/admin/invites/: page + invite-list + create-invite-form shadcn components.
  • src/app/login/_components/login-page.tsx: invite-code field rendered when NEXT_PUBLIC_ALLOW_OPEN_SIGNUP=false.
  • .env.example + README updates documenting the new flag.

Validation

  • pnpm typecheck passes.
  • pnpm lint shows the same Prisma "type could not be resolved" warnings that exist on main (affects src/server/clients/db.ts, src/server/api/routers/trustclaw/updateSettings.ts, etc.) - pre-existing ESLint module-resolution issue, not introduced by this diff.
  • Atomic invite-claim verified via updateMany + WHERE clause that includes usedAt: null.
  • Migration is additive (new table), zero impact on existing deployments.

AI was used for assistance.

README's "Before deploying to production" block calls out three production gaps;
this PR addresses the smallest of them (invite-only signup) without touching
the broader rate-limiting / billing surface.

Behavior change is opt-in: ALLOW_OPEN_SIGNUP defaults to true so existing
deployments and the Vercel template keep working unchanged. Operators flip the
flag and set ADMIN_USER_EMAIL to gate signups behind invite codes minted from a
new /dashboard/admin/invites page.

The Better Auth databaseHooks.user.create.before hook claims an invite code
atomically (prisma updateMany with WHERE usedAt IS NULL ... + SET usedAt=now)
so concurrent signups can't reuse the same code. APIError surfaces a clear
'Invite code required' / 'Invalid or expired invite code' message back to the
signup form.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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