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

Authentication

How Better Auth is configured in the Next.js + Cloudflare D1 starter, how admins are assigned, where the admin app lives, and what protects application data.

This kit uses Better Auth with the Drizzle adapter on top of the same D1 database that holds your workspaces and projects. There is no separate auth service to run. The shared authentication page explains the concepts that apply to all kits; this page is about where things live in this repository.

Where the configuration lives

WhatPath
Better Auth server instanceapps/web/src/auth/server.ts (createAuth(env)), reused by the admin app through its @/* path alias
Browser clientapps/web/src/auth/client.ts
Social provider wiringapps/web/src/auth/social-providers.ts
Auth tables (generated Drizzle schema)apps/database/src/schema/auth.generated.ts, regenerate with pnpm auth:generate
Route handlersapps/web/src/app/api/auth/[...all]/route.ts and apps/admin/src/app/api/auth/[...all]/route.ts
Web login, sign-up and recovery pagesapps/web/src/app/(dynamic-pages)/(login-pages)/ and (misc-login-pages)/
Admin login and recovery pagesapps/admin/src/app/login/, forgot-password/, update-password/, two-factor/
Web middleware chainapps/web/src/middleware.ts and apps/web/src/middlewares/
Admin middlewareapps/admin/src/middleware.ts

createAuth is built per request from the Worker env, so it reads BETTER_AUTH_SECRET, BETTER_AUTH_URL, NEXT_PUBLIC_SITE_URL, ADMIN_EMAIL and the provider credentials from the bindings described in environment variables. Each Worker's own NEXT_PUBLIC_SITE_URL is its only trusted origin, and cookies are marked secure when that URL starts with https://. When ADMIN_APP is true, the cookie prefix changes from better-auth to nextbase-admin, so the web and admin sessions never share a cookie even on the same parent domain.

Email, password and verification

Email and password sign-up is enabled and requireEmailVerification is on. A verification email is sent on sign-up and on sign-in attempts by unverified users, and the user is signed in automatically after clicking the link. Password resets revoke all sessions and clear trusted devices.

Auth emails are written to the D1 transactional outbox first. When USE_LOCAL_EMAIL is true and MAILPIT_API_URL is set, the Worker also posts the message to Mailpit and marks the outbox row complete. Otherwise the platform Worker's scheduled dispatcher delivers it through Resend. See installation for starting Mailpit, and emails for production delivery.

The local seed creates already-verified accounts so you can skip the inbox entirely during development.

Magic links, two-factor and API keys

  • Magic links are enabled through the magicLink plugin with a 15-minute expiry. Sign-up through a magic link is allowed on the web app; the admin login page also offers a magic-link form.
  • Two-factor uses the twoFactor plugin with TOTP and email OTP. Users manage it under /user/settings/security; the challenge page is /two-factor on both apps.
  • API keys use @better-auth/api-key with the st_ prefix, a required name, metadata, and a rate limit of 120 requests per minute. Keys are accepted as Bearer tokens or in the x-api-key header and are hashed in the database. Users create them under /user/settings/developer on the web app.

Social providers

Google, GitHub and X are supported. A provider appears on a login page only when both of its credentials are present in that Worker:

ProviderVariables
GoogleGOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET
GitHubGITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET
XTWITTER_API_KEY, TWITTER_API_SECRET

Register the callback URL as <origin>/api/auth/callback/<provider> in each provider's console, where <provider> is google, github or twitter. Register the web origin, and the admin origin too if administrators should sign in with a provider there. Put the credentials in apps/web/.dev.vars and apps/admin/.dev.vars locally and in each Worker's secrets in production.

Sessions

Sessions live in the D1 session table and expire after 30 days, refreshed once a day. A short cookie cache (60 seconds, JWE) keeps page loads fast without a database read on every request. Users can revoke sessions from their security settings.

Better Auth's built-in rate limiting is on (100 requests per 60 seconds), and the kit adds its own AUTH_RATE_LIMITER Workers rate-limit binding (10 requests per 60 seconds per key) in a before hook. If the limiter binding fails, auth returns 503 rather than running unprotected.

Becoming an application admin

Admin status is the role column on the Better Auth user table, managed by the admin plugin with adminRoles: ["admin"]. Two trusted paths assign it:

  1. ADMIN_EMAIL: a databaseHooks.user.create.before hook sets role to admin when the new account's email matches ADMIN_EMAIL (case-insensitive). Set the same address in the vars block of apps/web/wrangler.jsonc and apps/admin/wrangler.jsonc before the first sign-up with that address.
  2. Local seed: pnpm db:seed inserts admin@example.com with the admin role.

The repository doesn't ship a UI or script to promote an existing account afterwards. Change the role column through a trusted server-side path, for example wrangler d1 execute, rather than from the app.

The admin app

Platform administration lives in apps/admin, a separate Next.js Worker on its own origin: http://localhost:3001 locally and https://nextjs-d1-admin.usenextbase.com for the demo. The customer web app no longer has /app-admin routes or admin navigation. On the admin origin:

  • /login signs the administrator in with the shared Better Auth tables but an independent BETTER_AUTH_SECRET and the nextbase-admin cookie prefix.
  • apps/admin/src/middleware.ts redirects anonymous visitors to /login?next=/app-admin, returns 403 for accounts without the admin role and for impersonation sessions, and marks every page no-store and noindex.
  • The /app-admin/(admin-pages) layout re-checks the role with serverGetUserType() and renders unauthorized() otherwise.
  • Users are at /app-admin/users, workspaces at /app-admin/workspaces, billing at /app-admin/payment-gateway, marketing content under /app-admin/marketing/..., settings at /app-admin/settings, and feedback and roadmap moderation at /feedback and /roadmap.

Impersonation crosses the two origins. From /app-admin/users the admin app writes an audit row, asks Better Auth for a one-minute single-use token and redirects to <NEXT_PUBLIC_WEB_APP_URL>/auth/impersonate?token=.... The web route verifies the token, re-checks that the impersonating account is still a verified, unbanned admin and that the target is not an admin, and only then sets the impersonation session (up to one hour). The web impersonation banner links back to <NEXT_PUBLIC_ADMIN_APP_URL>/app-admin/users. Impersonation sessions are rejected by the MCP endpoints and by the admin middleware. The /admin/remove-user Better Auth path is disabled so account deletion goes through the application's token flow, which also cleans up R2 media.

What is protected and how

  • Middleware is optimistic. apps/web/src/middleware.ts runs guest, auth and onboarding checks for the matched route; the admin middleware gates the admin origin. In both apps, layouts, server actions and repositories perform the authoritative checks again.
  • Every tenant operation takes an Actor. The Actor type in apps/database/src/authorization.ts is anonymous, user (with optional isAppAdmin) or service (platform-worker or migration). Repositories check a permission such as projects.read or members.manage against the member's workspace role (owner, admin, member, readonly) plus any per-member overrides stored in workspace_members.permissions.
  • No raw bindings in UI code. Request code obtains APP_DB through getCloudflareContext; browser clients never receive a database binding. The apps/web/src/d1-clients/ and apps/web/src/data/ folders are the server-only data layer; the admin app adds apps/admin/src/data/admin/.
  • No interactive transactions. D1 doesn't support them, so multi-statement workflows use D1Database.batch() or conditional compare-and-set updates. Side effects go through the transactional outbox and are delivered idempotently by the platform Worker.
  • Companion rows are reconciled. Better Auth's user insert and the application's profile rows can't share one transaction on D1, so an idempotent reconciliation runs after user creation, user updates and session reads until it succeeds.

For the agent side of authentication (consent, token lifetimes, what ends access), see agent connections.

Environment variables

Every variable in the Next.js + Cloudflare D1 starter's example files, which Worker reads it, and where the value comes from.

Agent connections

The User and Admin MCP servers in the Next.js + Cloudflare D1 starter: endpoints, tools, OAuth behaviour and where the code lives.

On this page

Where the configuration livesEmail, password and verificationMagic links, two-factor and API keysSocial providersSessionsBecoming an application adminThe admin appWhat is protected and how