Files
NextChair/docs/phase-2-summary.md
Stefan Lazic 090646fdfd Phase 2
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.
2026-08-02 17:47:23 +01:00

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

  1. Project scaffold — Next.js 16 (App Router, Turbopack), React 19, TypeScript 6 (strict), Tailwind CSS v4, ESLint 9 (flat config), Prettier.
  2. Architecture — src/ layout, five route groups ((public) reserved, (auth), (customer), (staff), (owner)), a proxy.ts (Next 16's renamed middleware.ts) gating protected paths, and a features//lib//services//components/ separation matching the structure proposed in docs/implementation-plan.md §11.
  3. Design system — 20 reusable components under components/ui, components/navigation, and components/feedback, all built from the visual language in /templates, plus a /style-guide reference page showing every one with placeholder content.
  4. Database schema — a complete prisma/schema.prisma covering 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.
  5. 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 the ink/stone/surface/muted/line/accent palette that dominated the templates (Phase 1 plan §8), not the salon-red or blue variants found on two outlier templates. Added a --color-accent-loyalty token for the gold loyalty-card accent instead of letting it leak into the base palette.
  • Button — cva-based variants (primary/accent/secondary/ghost/destructive), asChild via Radix Slot so it can wrap a Next Link and 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' own switchTab() 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 respects prefers-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 its icon prop 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. AppointmentService is a proper join table with priceAtBooking/durationAtBookingMinutes snapshots — a later price change to a Service never 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.currentStamps is a cached total; LoyaltyTransaction is the append-only source of truth (STAMP_ADDED/STAMP_REMOVED/STAMP_EXPIRED/REWARD_REDEEMED), and LoyaltyReward records a completed redemption before the account's count resets — per your explicit instruction.
  • Roles/Permissions. Role is a real per-business table (seeded with "Owner" and "Barber"); Permission is a code-defined enum joined via RolePermission. A barber who is also an owner is one User with one StaffProfile, whose Role happens 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.userId is nullable and email/phone are 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 a BreakDay join table, not a Weekday[] array, because MySQL has no native array type. Money fields use Decimal, long text uses @db.Text, dates use @db.Date, and AuditLog.metadata uses Json — all four map to native equivalent types on both providers. Recurring wall-clock times (working hours, breaks, business hours) are stored as Int minutes-from-midnight rather than a DB TIME type, so scheduling math lives in application code, not provider-specific SQL.
  • AuditLog exists 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:

  1. The client generates to a custom folder (generated/prisma, via the new prisma-client generator), not node_modules/@prisma/client. Connection config moved out of schema.prisma into prisma.config.ts.
  2. PrismaClient now requires an explicit driver adapter (@prisma/adapter-pg for Postgres, @prisma/adapter-mariadb for MySQL) instead of a bare connection string. src/lib/db/prisma.ts is the only file in the app that imports the adapter — switching providers later means changing that one file, datasource.provider in the schema, and re-running prisma 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.ts to proxy.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-free auth.config.ts from the full auth.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/ProfileCard demo handlers in the style guide (fixed by moving the interactive-with-real-handlers instances into an already-"use client" wrapper) and BottomNav's icon prop (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, Credentials authorize(), Google). proxy.ts builds its own lightweight NextAuth(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.ts does a cheap "is there a session at all" check (Edge/Node-safe, no DB), and each protected layout does the real DB-backed check (StaffProfile exists? does the Role grant MANAGE_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-react tree-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:

  1. Ran prisma migrate dev against 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.
  2. 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.
  3. In an actual browser: registered a brand-new customer account end-to-end (form → server action → DB write → auto sign-in → redirect to /home).
  4. Signed in as the seeded Owner (marcus@blakeheritage.co.uk / password123) and confirmed /owner renders for them.
  5. Signed in as the seeded plain Barber (leo@blakeheritage.co.uk / password123) and confirmed /owner redirects him to /staff — the negative case, which is the one that actually matters for a permission system.
  6. Confirmed a customer with no StaffProfile is bounced from /staff to /home.
  7. Confirmed an unauthenticated request to any protected path (/settings while logged out) is redirected to /sign-in by proxy.ts.
  8. 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 (emailVerified is modeled on User but 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 password password123) 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_SECRET from 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 User model.
  • The route/URL naming (/staff, /owner, /home — no /customer prefix) — 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.