Base app architecture:
Route groups for customer, staff and owner.
* Reusable design system with around 20 UI components.
* Complete Prisma schema.
* Authentication with email/password and Google.
* Role and permission model.
* Protected routes.
* Clean separation between features, services, components and shared utilities.
19 KiB
NextChair — Phase 2 Summary
Project scaffold, design system, database schema, authentication foundation
Status: Complete and verified. Typecheck, lint, and next build are all clean. The full schema was migrated against a real Postgres instance and the entire auth flow (register → sign-in → sign-out → permission-gated routing) was exercised end-to-end in a browser, not just eyeballed. See §7 for exactly what was verified and how.
Per your instructions, this phase stops here: no booking, appointments, loyalty, or staff/owner functionality has been implemented. Where those areas needed a route to exist for the layout/nav/auth architecture to be provable, the route renders a labeled "Coming in Phase N" placeholder — see §3.
1. What was built
- Project scaffold — Next.js 16 (App Router, Turbopack), React 19, TypeScript 6 (strict), Tailwind CSS v4, ESLint 9 (flat config), Prettier.
- Architecture —
src/layout, five route groups ((public)reserved,(auth),(customer),(staff),(owner)), aproxy.ts(Next 16's renamedmiddleware.ts) gating protected paths, and afeatures//lib//services//components/separation matching the structure proposed indocs/implementation-plan.md§11. - Design system — 20 reusable components under
components/ui,components/navigation, andcomponents/feedback, all built from the visual language in/templates, plus a/style-guidereference page showing every one with placeholder content. - Database schema — a complete
prisma/schema.prismacovering every entity your brief listed (and a few more identified in the Phase 1 domain model), migrated and seeded successfully against a real Postgres database. - Authentication foundation — email/password (Credentials) and Google OAuth via Auth.js (NextAuth v5), a working register → auto-sign-in flow, sign-out, and real role/permission-based route gating (not just "is logged in").
2. Folder structure
NextChair/
docs/
implementation-plan.md # Phase 1 output
phase-2-summary.md # this file
prisma/
schema.prisma
seed.ts
migrations/20260802161213_init/migration.sql
templates/ # untouched — 35 UX Pilot exports + prototypes
src/
app/
(auth)/ # sign-in, register — shared centered-card layout
(customer)/ # home, appointments, settings — light bottom nav, session-gated
(staff)/staff/ # calendar, current, clients, settings — dark bottom nav, StaffProfile-gated
(owner)/owner/ # dashboard, team — same shell as (staff), MANAGE_STAFF-gated
api/auth/[...nextauth]/route.ts
style-guide/ # design-system showcase (reference only)
page.tsx # Welcome/splash
layout.tsx, globals.css
auth.ts # full NextAuth config (adapter + real providers)
auth.config.ts # DB-free subset, used by proxy.ts
proxy.ts # Next 16's middleware convention — see §6
components/
ui/ # Button, Card, Input, Textarea, Select, Checkbox, Switch,
# Modal, BottomSheet, Tabs, MonthCalendar, AppointmentCard,
# ServiceCard, BarberCard, ProfileCard, LoyaltyCard, StatusPill
navigation/ # BottomNav, TopNav
feedback/ # EmptyState, ErrorState, LoadingState, Skeleton, ComingSoon
features/auth/ # schemas, register server action, sign-in/up/out components
lib/
auth/permissions.ts # StaffProfile → Role → RolePermission lookup
db/prisma.ts # the one file that imports the Postgres driver adapter
navigation/staff-nav-items.ts
utils/cn.ts
types/
domain.ts # UI-facing types, deliberately independent of the ORM
next-auth.d.ts # session.user.id module augmentation
3. What each route actually does right now
| Route | Renders | Gate |
|---|---|---|
/ |
Welcome/splash (matches template 20, minus "Continue as Guest" — see §8) | redirects to /home if already signed in |
/sign-in, /register |
Real, working forms | none |
/home, /appointments, /settings |
/home and /settings show real session data + a working sign-out; /appointments is a labeled placeholder |
any authenticated session |
/staff, /staff/current, /staff/clients |
Labeled placeholders | authenticated and has a StaffProfile |
/staff/settings |
Real profile card + conditional "Switch to Owner Admin" link (only if the signed-in staff member's Role grants MANAGE_STAFF) + sign-out |
same as above |
/owner, /owner/team |
Labeled placeholders | authenticated, has a StaffProfile, and MANAGE_STAFF permission |
Every placeholder exists only to prove the layout/nav/auth architecture around it works — none contain business logic. This was a deliberate choice to satisfy "Create the architecture: Route groups, Shared layouts" (Phase 2 scope §3) without drifting into "Staff functionality, Owner functionality" (explicitly excluded).
4. Design system
All 20 components are in components/ui, components/navigation, components/feedback. Highlights and the judgment calls behind them:
- Tokens (
globals.css) — Tailwind v4's CSS-based@theme, carrying forward theink/stone/surface/muted/line/accentpalette that dominated the templates (Phase 1 plan §8), not the salon-red or blue variants found on two outlier templates. Added a--color-accent-loyaltytoken for the gold loyalty-card accent instead of letting it leak into the base palette. - Button —
cva-based variants (primary/accent/secondary/ghost/destructive),asChildvia RadixSlotso it can wrap a NextLinkand still look like a button. - Switch, Tabs — Radix primitives. These are the two places a hand-rolled implementation is genuinely easy to get subtly wrong (roving tabindex,
aria-checked); the templates' ownswitchTab()DOM-manipulation script is exactly the kind of thing this replaces. - Modal, BottomSheet — built on the native
<dialog>element rather than Radix Dialog or a hand-rolled focus trap.showModal()gives focus containment, Escape-to-close, and top-layer stacking from the browser for free. No template had a modal/sheet pattern at all (Phase 1 §8 flagged this as a gap); these are new. - Select, Checkbox — deliberately native
<select>/<input type="checkbox">, not Radix — native select gets the OS picker on mobile for free, which no custom listbox matches. - MonthCalendar — real date math (no external date library), matches the templates' booking/reschedule calendar. The staff day/agenda calendar is not built — Phase 1 §14 found the templates disagree on its layout (grid-columns vs. vertical agenda) and it depends on the booking engine's availability model, which doesn't exist yet.
- LoyaltyCard — ports the 3D flip card from template 11 faithfully, but fixes a real gap: the original had no actual flip trigger wired up (presentational only). Here it's a real
<button aria-pressed>, and the flip transition respectsprefers-reduced-motion. - BottomNav — one component serves both the customer (light) and staff/owner (dark) variants, replacing the four inconsistent nav markups found across the templates. It resolves its own "active" tab from
usePathname()rather than requiring every layout to compute and pass it — see the RSC lesson in §6 for why itsiconprop is a string key, not a component reference. - EmptyState, ErrorState, LoadingState, Skeleton — none of these existed in any template; every list screen needs one for the zero-items/failure/loading case.
5. Database schema
Full schema in prisma/schema.prisma, migrated cleanly (prisma/migrations/20260802161213_init/migration.sql) and seeded (prisma/seed.ts) against a real Postgres instance. Covers every entity from your brief:
Business, BusinessLocation, BusinessHours, BusinessSettings, BookingRule · User, Account, Session, VerificationToken (Auth.js standard shape) · CustomerProfile, StaffProfile, Role, RolePermission · ServiceCategory, Service, StaffService · WorkingHours, Break, BreakDay, TimeOff · Appointment, AppointmentService · LoyaltyProgramme, LoyaltyEligibleService, LoyaltyEligibleStaff, LoyaltyAccount, LoyaltyTransaction, LoyaltyReward · Notification, NotificationPreference · CustomerNote, AuditLog.
Key decisions (each is a comment in the schema itself, not just here):
- Multi-service appointments.
AppointmentServiceis a proper join table withpriceAtBooking/durationAtBookingMinutessnapshots — a later price change to aServicenever rewrites historical bookings. Directly required by your "support multiple services within a single appointment" instruction, and matches the "add another service" step visible in the booking templates. - Loyalty is a ledger, not a counter.
LoyaltyAccount.currentStampsis a cached total;LoyaltyTransactionis the append-only source of truth (STAMP_ADDED/STAMP_REMOVED/STAMP_EXPIRED/REWARD_REDEEMED), andLoyaltyRewardrecords a completed redemption before the account's count resets — per your explicit instruction. - Roles/Permissions.
Roleis a real per-business table (seeded with "Owner" and "Barber");Permissionis a code-defined enum joined viaRolePermission. A barber who is also an owner is oneUserwith oneStaffProfile, whoseRolehappens to grant more permissions — never two accounts. This directly resolves the role-conflict the Phase 1 review found in the templates (a "Switch to Admin View" button with no real permission model behind it). - Guest-friendly by design.
CustomerProfile.userIdis nullable andemail/phoneare not unique-constrained — a hard constraint would force guests to either collide with each other or be blocked from repeat bookings under a shared household email/phone. Guest deduplication stays an explicit open question (Phase 1 plan §15, Question 4), not something silently decided by a DB constraint. - Portability (Postgres → MySQL). No scalar-list columns anywhere —
Break's applicable days are aBreakDayjoin table, not aWeekday[]array, because MySQL has no native array type. Money fields useDecimal, long text uses@db.Text, dates use@db.Date, andAuditLog.metadatausesJson— all four map to native equivalent types on both providers. Recurring wall-clock times (working hours, breaks, business hours) are stored asIntminutes-from-midnight rather than a DBTIMEtype, so scheduling math lives in application code, not provider-specific SQL. AuditLogexists in the schema but nothing writes to it yet — added now so Phase 4/6 features that need it don't require a migration to start.
A real, current-generation Prisma detail worth flagging
Prisma 7 (what npm install prisma currently resolves to) changed two things that aren't yet reflected in most tutorials/training data, and I hit both empirically rather than assuming:
- The client generates to a custom folder (
generated/prisma, via the newprisma-clientgenerator), notnode_modules/@prisma/client. Connection config moved out ofschema.prismaintoprisma.config.ts. PrismaClientnow requires an explicit driver adapter (@prisma/adapter-pgfor Postgres,@prisma/adapter-mariadbfor MySQL) instead of a bare connection string.src/lib/db/prisma.tsis the only file in the app that imports the adapter — switching providers later means changing that one file,datasource.providerin the schema, and re-runningprisma migrate dev, not touching application code.
6. Architecture decisions worth knowing about (found by actually building and running this, not by inspection)
- Next.js 16 renamed
middleware.tstoproxy.ts, and — significantly — proxy files now run on the Node.js runtime by default, not Edge. That alone would have resolved the "Prisma isn't Edge-compatible" warning I initially hit, but I still split a DB-freeauth.config.tsfrom the fullauth.ts(§ below) because spinning up a Postgres connection pool just to check "is there a valid session JWT" on every request is wasteful regardless of which runtime it's allowed to run on. - RSC serialization boundary bit me twice, in the same way, and I want to flag the pattern rather than bury it in a diff. A Server Component cannot pass a live function (an inline handler, or a React component reference like a Lucide icon) as a prop to a Client Component — both fail with slightly different error messages ("Event handlers cannot be passed to Client Component props" / "Only plain objects can be passed to Client Components from Server Components"). This hit
ErrorState/ProfileCarddemo handlers in the style guide (fixed by moving the interactive-with-real-handlers instances into an already-"use client"wrapper) andBottomNav'siconprop (fixed by passing a string key, resolved to a component inside the Client Component itself, instead of the component reference). Both fixes are the general-purpose pattern for this class of bug, not one-off patches — worth knowing before Phase 3 adds more Server-Component pages that render interactive children. - Auth split:
auth.config.ts(pages, providers: [], session strategy — no DB) vs.auth.ts(the full thing: Prisma adapter, Credentialsauthorize(), Google).proxy.tsbuilds its own lightweightNextAuth(authConfig)instance from the former; everything else (route handlers, Server Components, Server Actions) uses the latter. - Permission enforcement happens in two places on purpose:
proxy.tsdoes a cheap "is there a session at all" check (Edge/Node-safe, no DB), and each protected layout does the real DB-backed check (StaffProfileexists? does theRolegrantMANAGE_STAFF?). This matches the Phase 1 plan's explicit instruction not to trust route-group gating alone. - Icons: swapped the templates' Font Awesome CDN script for
lucide-reacttree-shaken imports, as recommended in the Phase 1 plan.
7. What was actually verified (not just built)
I don't consider "it compiles" sufficient proof for an auth system, so:
- Ran
prisma migrate devagainst a real, throwaway local Postgres (Docker, torn down afterward — did not touch the two unrelated Postgres containers already running on this machine) — the full schema applies as real SQL, not just Prisma-schema-valid. - Ran the seed script against that database — business, roles + permissions, services, two staff users (an Owner and a plain Barber), and a customer all created successfully.
- In an actual browser: registered a brand-new customer account end-to-end (form → server action → DB write → auto sign-in → redirect to
/home). - Signed in as the seeded Owner (
marcus@blakeheritage.co.uk/password123) and confirmed/ownerrenders for them. - Signed in as the seeded plain Barber (
leo@blakeheritage.co.uk/password123) and confirmed/ownerredirects him to/staff— the negative case, which is the one that actually matters for a permission system. - Confirmed a customer with no
StaffProfileis bounced from/staffto/home. - Confirmed an unauthenticated request to any protected path (
/settingswhile logged out) is redirected to/sign-inbyproxy.ts. - Confirmed sign-out actually clears the session (not just a UI state change).
npm run typecheck, npm run lint, and npm run build are all clean with zero warnings.
8. Assumptions made this phase
- Password reset ("Forgot Password", template 8) was not wired up, even as a dead link — it needs the Notifications system (real email delivery) to do anything honest, and that's out of scope. I'd rather have no link than a link that silently does nothing, which is exactly the kind of broken-link issue the Phase 1 review flagged in the original templates.
- The Welcome/splash page's "Continue as Guest" button (template 20) isn't shown, for the same reason — it leads into the booking flow, which doesn't exist yet.
- Registration auto-signs the user in immediately, with no email-verification gate (
emailVerifiedis modeled onUserbut not enforced) — consistent with the "auto-confirm" default recommended in the Phase 1 plan (§15, Question 5) and with every template's own instant-success flow. - Seeded demo accounts (
marcus@blakeheritage.co.uk,leo@blakeheritage.co.uk,alex.chen@example.com, all passwordpassword123) exist only for this verification — delete or change them before any real deployment. - Google OAuth is wired in code but untested end-to-end (needs real
AUTH_GOOGLE_ID/AUTH_GOOGLE_SECRETfrom a Google Cloud project, which I don't have). The Credentials path is fully verified; the Google path follows the same, standard Auth.js provider pattern and should work once real credentials are supplied — flagging this as the one piece of Phase 2 I couldn't verify firsthand.
9. What should be reviewed before moving on
- The Role/Permission seed set (
prisma/seed.ts) — I picked a reasonable permission split between "Owner" and "Barber" based on the Phase 1 domain model, but you may want different defaults before this becomes load-bearing. - Whether email verification should gate anything before Phase 2's account-creation flow is considered "final" — right now it's purely decorative on the
Usermodel. - The route/URL naming (
/staff,/owner,/home— no/customerprefix) — chosen to match the Phase 1 route map, worth confirming before more pages accumulate under it.
10. Getting a real database running
docker run -d --name nextchair-pg -e POSTGRES_USER=nextchair \
-e POSTGRES_PASSWORD=nextchair -e POSTGRES_DB=nextchair \
-p 5432:5432 postgres:16-alpine
npm run prisma:migrate # applies prisma/migrations/
npm run prisma:seed # creates the demo business/staff/customer
npm run dev
.env already has a matching DATABASE_URL and a real AUTH_SECRET; only AUTH_GOOGLE_ID/AUTH_GOOGLE_SECRET need filling in to exercise Google sign-in.
Ready for direction on the next slice of work — per the phasing in docs/implementation-plan.md §13, that would be Phase 2: Customer Accounts (the real, data-backed home/appointments/loyalty pages, replacing the placeholders in §3 above). Note this is the implementation plan's own Phase 2, distinct from "Phase 2" as you've been using it in this conversation to mean the whole scaffold-and-foundation stage just completed. I won't start building it until you say so.