# 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 /admin` → `307` 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) ## 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 ```bash 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 ```