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 ofInitial.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):
- Anonymous
GET /admin→307redirect to/login - Submit
email=admin@touchbase.localto/api/auth/signin/nodemailer→ magic link delivered to Mailpit (subject "Sign in to localhost:3000") - Click magic link from email → JWT session cookie set, redirect to
/ GET /admin/bookingswith the cookie → renders the booking table (1 row from earlierbook-on-behalfrun: 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
- Customer-visible brand name (TouchBase or other) — affects email sender/subject lines
- Currency — USD assumed
- Stripe account ownership — needed when 5c lands
Roadmap status (Initial.md §9)
- Spike — done 2026-04-30
- Schema + seed — done 2026-05-01
- Availability algorithm — done 2026-05-01
- First end-to-end story (admin booking + email) — done 2026-05-01
- 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)
Recommended next step
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