Nextbase Docs
Nextbase Docs
HomeBlogWelcome to Nextbase v5!Choose your starterGetting startedLocal Supabase per agent
Next.js + NeonInstallationEnvironment variablesAuthenticationAgent connectionsProject structureDeployment
AuthenticationWorkspaces and the admin area
Building with agentsEmails with Resend
TroubleshootingNextbase v5
The 8 starter kitsNext.js + Neon

Authentication

How Better Auth is configured in the Next.js + Neon starter, how sessions, the separate admin app and impersonation work, and what protects your data.

This kit uses Better Auth. Users, sessions and accounts live in your own Postgres database, and the same login is used by the website, the admin app and agent connections. For the concepts shared by all kits, read the v5 authentication guide.

Where the configuration lives

WhatPath
Server configuration (plugins, email hooks, session settings)apps/web/src/lib/auth/server.ts
Browser clientapps/web/src/lib/auth/client.ts
Better Auth API route, web appapps/web/src/app/api/auth/[...all]/route.ts
Better Auth API route, admin appapps/admin/src/app/api/auth/[...all]/route.ts
Session helpers (getCurrentSession, requireSession, requireAdminSession)apps/web/src/lib/auth/session.ts
Actor contracts (requireActor, requireWorkspaceActor)apps/web/src/lib/auth/actors.ts
Workspace roles and permissionsapps/web/src/lib/auth/organization-access.ts
Secondary storage, rate limits and locksapps/web/src/lib/auth/secondary-storage.ts
Impersonation handoff bookkeepingapps/web/src/lib/auth/support-handoff.ts and apps/web/src/lib/cross-app-url.ts
Auth tablesapps/database/src/schema.ts

The admin app has no auth code of its own. Its @/* alias falls back to apps/web/src, so both apps run the same createAuth() from server.ts against the same database, each with its own origin.

Better Auth's models keep their names in code but map to the app tables: user is users, session is sessions, organization is workspaces, member is workspace_members and subscription is subscriptions.

The environment variables that matter are BETTER_AUTH_URL, BETTER_AUTH_SECRET, NEXT_PUBLIC_APP_URL and, optionally, BETTER_AUTH_TRUSTED_ORIGINS. Trusted origins are BETTER_AUTH_URL, NEXT_PUBLIC_APP_URL and anything in the comma-separated extra list. Each app sets BETTER_AUTH_URL and NEXT_PUBLIC_APP_URL to its own origin, and both must share BETTER_AUTH_SECRET. Cookies are host-only, so a session on the web host is not a session on the admin host.

Sign-up, verification and the local inbox

Email and password is enabled with requireEmailVerification: true. A new account can't sign in until the verification link is opened. The pages on the web app are /sign-up (also /signup), /login, /forgot-password and /update-password.

Verification, password reset, email change and account deletion emails are rendered from apps/email and sent through apps/web/src/lib/email/send-email.ts. Locally, EMAIL_PROVIDER=smtp points at Mailpit, so open http://127.0.0.1:8025 to find the message. In production, set EMAIL_PROVIDER=resend and RESEND_API_KEY. See emails with Resend.

Account deletion sends a confirmation link that expires after one hour. Email change sends a confirmation to the current address.

Magic links and social providers

The magicLink plugin is enabled. The login and sign-up pages include magic link forms, and the email is sent through the same email setup. Administrators can also send a customer a login link from the admin app; that action posts to the web app's /api/auth/sign-in/magic-link so the link lands on the web origin.

Google, GitHub and X (Twitter) are configured in server.ts, but only when both the client ID and client secret for a provider are set. Out of the box, the login form in apps/web/src/components/auth-form.tsx offers email and password plus the magic link form; it renders no social buttons, and the README says the same. If you want social login, set the provider variables and add the buttons that call the Better Auth client's social sign-in. The provider icons already exist in apps/web/src/components/authentication/icons-list.tsx. Register the callback URL with your provider for the origin in BETTER_AUTH_URL.

Sessions

Sessions are stored in the database (storeSessionInDatabase: true) and expire after 7 days, extended once a day while in use. The cookie cache is disabled, so every request validates against the sessions table.

Rate limiting is on (100 requests per 60-second window) and uses Better Auth's secondary storage. With VALKEY_URL set that is Valkey; without it the kit uses the Postgres tables auth_secondary_storage and auth_rate_windows and transaction-scoped advisory locks, which work through Neon's connection pooling.

Users can also create API keys under /user/settings/developer. Keys use the nb_ prefix, require a name, and have their own rate limit.

Workspaces and roles

Workspaces are Better Auth organizations. Any user can create one, up to BETTER_AUTH_ORGANIZATION_LIMIT (default 10). Roles are owner, admin, member and readonly, and permissionsForRole in actors.ts maps them to permissions such as workspace:write, members:write, billing:write and projects:write.

Invitations expire after 7 days. The email links to /api/invitations/view/<id>, and the user accepts under /user/invitations or /accept-invitation. Creating a workspace also creates its workspace_settings and workspace_application_settings rows and writes an audit and outbox event.

The admin app and application administrators

Application admin is separate from owning a workspace. It is the Better Auth admin plugin's role column on the users table.

The admin area lives in apps/admin, served on its own origin (port 3001 locally, NEXT_PUBLIC_ADMIN_APP_URL in production). Administrators sign in at /login on the admin host with email and password; that page has no magic link, social or sign-up option, and it redirects straight to /app-admin when an admin session already exists. Every route under apps/admin/src/app/(dynamic-pages)/(authenticated-pages)/ runs requireAdminSession, which sends anyone without role = 'admin', or anyone inside an impersonation session, to /login?error=forbidden. The web app's sidebar shows an Open admin panel link to <NEXT_PUBLIC_ADMIN_APP_URL>/app-admin for administrators; the web app itself no longer serves /app-admin.

How an account gets the role:

  • Locally, the seed creates admin@example.com with the admin role (password password123).
  • An existing administrator can change another user's role from the users page in the admin app. The action in apps/admin/src/data/admin/user.tsx calls Better Auth's setRole and writes an admin.user_role_updated audit entry.
  • For a fresh production database, the repository has no script for the first administrator. The code reads users.role, so set that column to admin for your own account using trusted database access (for example the Neon SQL editor), then use the admin app for everyone else. Don't expose this through the app.

Admins can also ban and unban users, create users and send login links.

Impersonation across the two apps

Impersonation starts in the admin app and lands in the web app:

  1. The admin action calls Better Auth's impersonateUser, which creates a support session carrying impersonatedBy, and the oneTimeToken plugin issues a one-use token that lives for one minute. The action records an admin.user_impersonated audit entry and registers the handoff with the issuing admin session; if either write fails, the new support session is revoked.
  2. The browser is sent to <NEXT_PUBLIC_WEB_APP_URL>/auth/impersonation with the token in the URL fragment, so it never reaches a server log.
  3. That page posts the token to /api/impersonation on the web app, which only accepts same-origin requests, verifies the token, checks that the session is an impersonation session for a user who isn't banned, and consumes the handoff. Consuming re-checks the issuing administrator's role, ban state and exact session in the database; if any check fails, the support session is deleted and no cookie is set.
  4. On success the web app sets its own host-only session cookie and opens /dashboard. An impersonation banner offers to stop; stopping signs the support session out and returns to <NEXT_PUBLIC_ADMIN_APP_URL>/app-admin/users, where the admin's own session is still valid.

The MCP servers refuse impersonation sessions, and requireAdminSession refuses them too.

What is protected and how

There is no row-level security for application data in this kit (the only exception is the mcp_records table, which is restricted to the server's database role). Protection is explicit in server code:

  • apps/web/src/proxy.ts redirects requests to /dashboard, /onboarding, /project, /user and /workspace to /login?next=... when there is no session cookie. This is only a convenience; it doesn't authorize anything.
  • Layouts under app/(dynamic-pages)/(authenticated-pages) in the web app call unauthorized() when there are no claims. In the admin app, the equivalent layout calls requireAdminSession.
  • Server actions use authActionClient or adminActionClient from apps/web/src/lib/safe-action.ts, validate input with zod, and then check workspace membership with requireWorkspaceActor or getWorkspaceMembership. Administrative queries and actions live only in apps/admin/src/data/admin/.
  • Policies in apps/web/src/lib/domain/*-policy.ts (assertProjectWrite, requireWorkspaceManager and friends) hold the rules, with unit tests next to them.
  • Mutations insert audit log and outbox rows in the same db.transaction through domainEventValues, so every change has a trail and a realtime event. Admin mutations also call refreshWebAfterAdminMutation, which posts to the web app's /api/internal/admin-revalidate with REVALIDATE_SECRET so public pages pick up CMS changes.
  • Banned users are redirected to /login?error=banned, and the MCP servers re-check verification, ban state and role on every call.

When you add a feature, keep the pattern: parse input, resolve the actor, check the workspace role, then query or write with the workspace ID from the server, never from the client. The AGENTS.md files in the repository spell this out for coding agents too.

Environment variables

Every variable in the Next.js + Neon starter's .env.example and apps/admin/.env.local.example, what it does and where the value comes from.

Agent connections

The User MCP and Admin MCP servers in the Next.js + Neon starter: endpoints on the web and admin origins, tools, OAuth behaviour and how to add a tool.

On this page

Where the configuration livesSign-up, verification and the local inboxMagic links and social providersSessionsWorkspaces and rolesThe admin app and application administratorsImpersonation across the two appsWhat is protected and how