Files
touchbase/docs/progress/2026-05-02-auth-and-admin-shell.md

6.7 KiB

2026-05-02 — Auth.js + Admin Shell

Companion to Initial.md. Predecessor: 2026-05-01-end-to-end.md. Closes Step 5a of Initial.md §9.

Milestone

The practice has its first usable web surface: admin can sign in via magic-link email, see today's confirmed bookings, and sign out. Auth.js v5 is wired through to the same SMTP transport we built for booking confirmations — Mailpit catches sign-in emails in dev, Resend will catch them in prod with one config change.

What's verified end-to-end

In a real browser-equivalent flow (curl + Mailpit API):

  1. Anonymous GET /admin307 redirect to /login
  2. Submit email=admin@touchbase.local to /api/auth/signin/nodemailer → magic link delivered to Mailpit (subject "Sign in to localhost:3000")
  3. Click magic link from email → JWT session cookie set, redirect to /
  4. GET /admin/bookings with the cookie → renders the booking table (1 row from earlier book-on-behalf run: Tue May 5 10:00 AM, Alex Park, 60-minute Swedish, Mei Tanaka, Room 1 — Sunset, CONFIRMED)

Plus: pnpm test 64/64, pnpm lint clean, pnpm exec tsc --noEmit clean.

What landed

Path Role
src/auth.ts Auth.js v5 config: PrismaAdapter, Nodemailer provider via SMTP, JWT sessions, role-aware session/JWT callbacks
src/app/api/auth/[...nextauth]/route.ts Required Auth.js route handler
src/app/page.tsx Marketing-thin home with link to staff sign-in
src/app/login/page.tsx Magic-link request form (server action)
src/app/login/check-email/page.tsx Post-submission landing page
src/app/admin/layout.tsx Auth-required layout; redirects to /login if no session, shows 403 if not ADMIN; header with email + sign-out
src/app/admin/page.tsx Redirects to /admin/bookings
src/app/admin/bookings/page.tsx Server-rendered table of upcoming HOLD/CONFIRMED bookings, times in APP_TZ
prisma/schema.prisma User.image String? (Auth.js convention) + VerificationToken model
prisma/migrations/20260501231153_auth/ Migration
.env AUTH_SECRET set

Decisions ratified

Decision Resolution
Auth.js version v5 beta (5.0.0-beta.31). v4 stable doesn't support the App Router patterns we want
Session strategy JWT, not database. Reason: simpler — no Account/Session tables needed, no extra DB roundtrip per request
Magic-link expiry 24h (Auth.js default). Acceptable for staff
Email provider for auth Same Nodemailer SMTP transport as our booking confirmations. No separate config
Mailpit AUTH workaround Mailpit advertises AUTH PLAIN; nodemailer attempts it; without creds it errors. Fix: pass auth: { user: "dev", pass: "dev" } (Mailpit is configured with MP_SMTP_AUTH_ACCEPT_ANY=1)
Role refresh in JWT callback On subsequent calls (no user), re-fetch role from DB if missing. Tradeoff: tiny extra query per request, but role changes apply on next request without re-login
/admin/bookings is server-rendered with dynamic = "force-dynamic" Reason: bookings change continuously; static caching would lie. Cheap enough at our scale
Time formatting in admin table Intl.DateTimeFormat with APP_TZ (America/Detroit). No client JS needed
Sign-out via server action with signOut({ redirectTo: "/" }) Reason: stays consistent with sign-in pattern; no client component needed
session.user.id and session.user.role typing Module augmentation in src/auth.ts for next-auth and next-auth/jwt. Required import type {} from "next-auth/jwt" to force module loading before augmentation

Gotchas hit

1. Mailpit SMTP AUTH advertisement

nodemailer attempted AUTH PLAIN because Mailpit advertises it; threw "Missing credentials for PLAIN" when given auth: undefined. Resolution above. Same code path in src/lib/email.ts (booking confirmations) doesn't hit this because those tests run after Mailpit-bound env is set, but the prod path with real SMTP credentials wouldn't have the same issue regardless. Worth a note: setting auth: { user: "dev", pass: "dev" } works for Mailpit and harmless if real creds are set later via env.

2. Module augmentation for next-auth/jwt

TypeScript couldn't find next-auth/jwt for the declare module block until we added import type {} from "next-auth/jwt" to force the module to be loaded. Subtle but well-known TS quirk with submodule augmentation.

3. PrismaAdapter typing vs Prisma 7's generated client

PrismaAdapter(db) complained because Prisma 7's client generic doesn't match Auth.js's adapter expectation. Cast as db as any with an inline ESLint disable. Auth.js adapter doesn't actually use the unmatched types at runtime; this is purely a typing gap that may close as either side updates.

4. Workspace-root warning from Next.js

A stray /Users/noise/package-lock.json made Next.js ambiguous about the project root. Visible warning at startup; not blocking. Worth setting turbopack.root in next.config.ts later, or just removing the offending lockfile.

Open questions still unresolved

  1. Customer-visible brand name (TouchBase or other) — affects email sender/subject lines
  2. Currency — USD assumed
  3. Stripe account ownership — needed when 5c lands

Roadmap status (Initial.md §9)

  1. Spike — done 2026-04-30
  2. Schema + seed — done 2026-05-01
  3. Availability algorithm — done 2026-05-01
  4. First end-to-end story (admin booking + email) — done 2026-05-01
  5. Public self-booking → Stripe → reminders
    • 5a Auth.js + admin shell — done 2026-05-02 (this session)
    • 5b Admin "create booking" page (slot picker + form using existing pipeline) — recommended next
    • 5c Public booking page (CUSTOMER role, magic-link signup, slot search + checkout)
    • 5d Stripe deposit flow + webhook
    • 5e Email reminders (pg-boss scheduled jobs)

5b — admin "create booking" page. Wraps the book-on-behalf CLI in a UI: pick service, pick customer (search), pick a slot from findSlots, submit. Reuses the loader + createHold + email pipeline directly. ~1 day. After this lands, the practice has everything needed to take phone bookings without leaving the browser.

Then 5c (public self-booking page) becomes mostly a "remove the customer-search step + add public-facing styling" exercise on top of the admin form — most of the slot-picker UI carries over.

How to resume

cd /Users/noise/Documents/code/touchbase
docker-compose up -d postgres mailpit
pnpm db:seed                 # admin@touchbase.local exists in seed
pnpm dev                     # http://localhost:3000
# Open http://localhost:3000/login → submit admin@touchbase.local
# Open http://localhost:8025 → click magic link
# Lands on http://localhost:3000/admin/bookings