Auth (magic link)
NextAuth v5 with Resend magic link plus an optional password-credentials provider.
Cartwright uses NextAuth v5 (Auth.js) configured in lib/auth.ts + lib/auth.config.ts. The default sign-in path is a magic link emailed via Resend. A credentials provider is also wired, used by accounts that opted to set a password.
On a brand-new shop there's no RESEND_API_KEY yet, so the login page hides magic-link and shows only the password tab. For your very first admin login, use the seeded password — see Sign in for the first time.
Magic link flow
- Customer enters email on
/[locale]/account/login(for example/en/account/login). - Server generates a single-use token, persists it (typically in a
VerificationTokentable), and sends a link via Resend. - Customer clicks the link → token is consumed → session is established (JWT strategy).
- Anonymous cart is merged into the customer record on first authenticated request.
Auth.js lowercases the address a customer types. On v0.64.0 a link therefore signs in to the account spelled exactly that way, so for an account the shop holds as Ada@Example.com the first link makes a second, password-less account ada@example.com, and every later link signs in there, away from the account's orders and password. From v0.65.0 a link picks the account the way a password reset does: the one spelled exactly as given (lowercased), else the one a password login with that address signs in to, else the only one there is. The link names that account's stored spelling and is mailed to the address the account holds, so it signs in to that account and creates none (lib/auth/magic-link-account.ts). Where several accounts differ only in case and the rule cannot pick one, no link is sent, and the page still says to check the inbox. An address with no account gets its link as before. A customer whose earlier link already made the lowercased account keeps signing in there: nothing joins the two.
AUTH_SECRET must be set (generate with npx auth secret or openssl rand -hex 32). It is used to sign the JWT session token. Rotating AUTH_SECRET invalidates every active session — customers and admins will need to sign in again.
Resend is optional
If RESEND_API_KEY is not configured, the magic-link mailer falls back to writing each outbound email to .mail-previews/ as a local HTML file. Open it in a browser, copy the link, paste in the URL bar — same effect, no network. This is the recommended local-dev path.
The dev fallback is .mail-previews/ HTML files — not a console.log of the raw link. Watch that folder on first sign-in attempt in local dev.
Admin vs customer
Admin status is a boolean column on the User model. There is no separate admin auth flow — admins sign in the same way, and lib/admin.ts:requireAdmin() checks the session for the admin claim before letting any /admin/* route render.
This is intentional: you cannot accidentally create an "admin via OAuth" bypass. The only path to admin is a manual flip of the column, normally done by the first admin during setup wizard, or via Prisma Studio in pre-launch.
Credentials provider
A credentials provider is also configured for users who set a password during onboarding (User.passwordHash). It is rarely the default path for customers but provides a fallback when email is unreliable (corporate spam filters, offline demos, etc.).
The provider hashes with a standard bcrypt/argon2-equivalent setup in lib/auth/ — see source for the exact algorithm currently in use.
Session shape
The JWT carries userId, email, isAdmin, and a short-lived expiration. Refresh happens via NextAuth's normal middleware path. No refresh-token rotation is implemented because magic links re-issue cheaply.
Production checklist
AUTH_SECRETset to a strong random value.RESEND_API_KEYconfigured and the Resend sending domain verified (SPF + DKIM).brand.config.ts:emails.frommatches a verified Resend sender.NEXT_PUBLIC_APP_URLmatches your canonical host — magic links use it for the callback URL.
Authentication & accounts
Every sign-in method (magic link, password, GitHub, Google), password rules, the forgot/reset flow, and the iron rule that admin is never granted via OAuth.
Google Sign-In
A "Continue with Google" button for customer login via NextAuth — flag-gated, env-gated, and never a path to admin.