{"record":{"id":"adeda1a3bc996811","repo":"Significant-Gravitas/AutoGPT","slug":"forbidden","errorCode":"FORBIDDEN","errorMessage":"Signups are not allowed.","messagePattern":"Signups are not allowed\\.","errorType":"error_code","errorClass":"APIError","httpStatus":403,"severity":"error","filePath":"autogpt_platform/frontend/src/lib/auth/auth.ts","lineNumber":81,"sourceCode":"}\n\nexport const auth = betterAuth({\n  baseURL,\n  secret: process.env.BETTER_AUTH_SECRET,\n  database: authDbPool,\n  telemetry: { enabled: false },\n  databaseHooks: {\n    user: {\n      create: {\n        // Env-driven signup gate (see signup-gate.ts). Fires for both\n        // email/password signup AND a first OAuth sign-in, since both create\n        // a user row. Existing users and the SQL data-migration bypass it.\n        // The thrown message is phrased so the frontend `isWaitlistError()`\n        // maps it to the existing \"not allowed\" modal.\n        before: async (user: { email: string }) => {\n          const decision = isSignupAllowed(user.email, readSignupGateConfig());\n          if (!decision.allowed) {\n            throw new APIError(\"FORBIDDEN\", {\n              message: decision.reason ?? \"Signups are not allowed.\",\n            });\n          }\n        },\n      },\n      update: {\n        // updateUserByEmail (fired when a change-email link is confirmed)\n        // runs this hook post-commit; mirror the now-verified email onto the\n        // platform User row so notifications/Stripe track the confirmed\n        // identity. See email-mirror.ts for the why.\n        after: async (user: { id: string; email: string }) => {\n          await mirrorVerifiedEmailToPlatformUser(authDbPool, user);\n        },\n      },\n    },\n  },\n  advanced: {\n    database: {","sourceCodeStart":63,"sourceCodeEnd":99,"githubUrl":"https://github.com/Significant-Gravitas/AutoGPT/blob/9c8bb5550f446ba5d3046b78896578742495b3cf/autogpt_platform/frontend/src/lib/auth/auth.ts#L63-L99","documentation":"An APIError with code FORBIDDEN thrown from a Supabase databaseHooks.user.create.before hook in the frontend auth config (src/lib/auth/auth.ts). The signup gate (signup-gate.ts) checks isSignupAllowed(email, config) for EVERY new user row — email/password signup and first OAuth sign-in alike — and throws when disallowed, using decision.reason or this default. The phrasing is intentionally matched so the frontend isWaitlistError() maps it to the 'not allowed' modal.","triggerScenarios":"A new user completing email/password signup or first OAuth sign-in while the signup gate config disallows it: waitlist mode enabled without the email on the allowlist, allowlist-only mode with an unknown email, or domain-restriction mode with a disallowed domain. Existing users never hit this (their rows already exist); SQL data-migration inserts bypass the hook.","commonSituations":"Self-hosters leaving waitlist/allowlist enabled after meaning to open signups; SignupRestrictedUntil / allowlist env vars set in .env (see signup-gate.ts config source) that the operator forgot; invited users signing in with a different email than the one allowlisted.","solutions":["Check the signup-gate config (env vars read by readSignupGateConfig in signup-gate.ts) — disable the gate or add the email/domain to the allowlist.","Restart the frontend after changing the env vars; the config is read server-side.","If the user SHOULD be allowed, verify the exact email (case, aliases like +tags, OAuth provider email vs allowlisted one).","For data migrations, insert users via SQL (documented bypass) instead of the auth API."],"exampleFix":"# before (frontend/.env)\nSIGNUP_ALLOWLIST_ONLY=true\n\n# after — open signups\nSIGNUP_ALLOWLIST_ONLY=false","handlingStrategy":"validation","validationCode":"import { isSignupAllowed, readSignupGateConfig } from \"@/lib/auth/signup-gate\";\n\nfunction checkSignupAllowed(email: string): { allowed: boolean; reason?: string } {\n  return isSignupAllowed(email, readSignupGateConfig());\n}","typeGuard":"function isWaitlistError(err: unknown): boolean {\n  return (\n    err instanceof Error && /signups are not allowed/i.test(err.message)\n  );\n}","tryCatchPattern":"// server action / route handler wrapping supabase.auth.signUp\ntry {\n  await supabase.auth.signUp({ email, password });\n} catch (error) {\n  if (isWaitlistError(error)) {\n    return { error: \"Signups are currently restricted. Join the waitlist instead.\" };\n  }\n  throw error;\n}","preventionTips":["Run isSignupAllowed(email, config) BEFORE calling signUp to give users a precise message instead of a thrown hook error.","Keep the thrown message phrasing in lockstep with isWaitlistError() — the frontend maps on the exact wording.","Document the gate env vars in deployment docs so operators know signups are closed by default in this mode."],"tags":["signup","supabase","forbidden","config","waitlist"],"backgroundTag":null,"analyzedSha":"9c8bb5550f446ba5d3046b78896578742495b3cf","analyzedAt":"2026-08-14T17:17:21.957Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}