Skip to content

real-auth: real Supabase Auth — sessions, identity bridge, role gating, staff PIN, customer magic-link - #11

Merged
snackman merged 1 commit into
mainfrom
real-auth
Jun 6, 2026
Merged

real-auth: real Supabase Auth — sessions, identity bridge, role gating, staff PIN, customer magic-link#11
snackman merged 1 commit into
mainfrom
real-auth

Conversation

@snackman

@snackman snackman commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

What this does

Makes authentication real in the multi-tenant POS, env-guarded exactly like the existing data-driver + payment-rail pattern:

  • No Supabase env (zero-env / CI / local): SIMULATED auth — no real login; every surface resolves a seeded demo session derived from memberships (not a hardcoded constant). Build + the full Vitest suite + the Vercel preview stay green with no env.
  • Supabase env present (production): REAL Supabase Auth — sessions, login, magic-link, role gating, the identity bridge.

Design

Session plumbing (@supabase/ssr)

  • src/lib/auth/supabase-browser.ts (browser client), supabase-server.ts (cookie-backed server client, RLS-as-user), src/middleware.ts (refreshes the session on every protected request + coarse-gates signed-out visitors to the right login).
  • getServerSession() / getCurrentUser() (src/lib/auth/session.ts) → authed user + memberships (tenant_ids + roles) + platform-admin flag, all derived from the session.

Identity-bridge migration (supabase/migrations/20260606000000_auth_user_bridge.sql)

  • Trigger on auth.users (insert) upserts public.users with id = auth.uid() + email. Existing seed users are linked by email by re-pointing the seeded public.users.id to the auth id (FKs made ON UPDATE CASCADE). Idempotent; the trigger is skipped on vanilla Postgres (no auth schema), so the RLS-isolation CI job still applies it cleanly.
  • Also adds staff.pin_hash + orders.staff_id.

Role-gating matrix (middleware + authoritative server checks)

Surface Allowed roles
/admin owner | manager (of active tenant)
/terminal, /kitchen owner | manager | cashier | kitchen (of active location's tenant)
/platform platform_admin only
/shop PUBLIC (guest checkout + optional customer account)

Unauthenticated → /login (tenant) or /platform/login. Multi-tenant user → /login/choose. Gated pages are now server components that resolve the session-derived tenant/location (replacing the hardcoded demo context) and pass it to the existing client shells. Tenant-scoped API routes re-check the session/membership against the request's tenant (src/lib/auth/api.ts).

Data-access model (user-RLS vs service-role)

  • All authorization comes from the real session/memberships; every tenant-scoped query is scoped to the session's tenant, and the sensitive admin/order API routes re-verify membership server-side.
  • The Supabase driver still uses the service-role key server-side (implements the full PosDriver, filters every query by tenant_id/location_id) — reserved for trusted server ops (webhooks, platform-admin actions w/ explicit checks, the bootstrap script, system jobs). A user-scoped RLS client (getServerSupabase()) is wired and used by the auth flows; a full per-query RLS switch across the entire feature-complete app was intentionally not done in this pass (documented trade-off — RLS is the backstop, the session is the gate).

Staff PIN quick-switch

  • The device logs into a location (real session); cashiers switch the active staff by a 4–8 digit PIN, verified server-side at /api/terminal/pin against staff.pin_hash (scrypt, node:crypto, no new dep). The hash never leaves the server (listStaff strips it; only getStaffById carries it). Orders attribute to the active staff via orders.staff_id.

Customer magic-link

  • /shop stays PUBLIC. The optional customer account sign-in now sends a real Supabase Auth magic link in real mode (returns to /shop/<slug>?signedin=1); falls back to the legacy returns-the-link stub with zero env.

Verification

  • Local (zero env): typecheck, lint, build, test:run all green (122 tests, +16 new for PIN, roles matrix, identity resolution).
  • Build registers the middleware; gated routes are dynamic.

Post-merge steps the orchestrator must run on the LIVE project

  1. npm run db:apply (applies migrations incl. the identity bridge + seed).
  2. Supabase dashboard → Authentication: enable Email provider (magic link), Site URL = production domain, Redirect URLs = https://<domain>/auth/callback + https://<domain>/shop/*, configure SMTP.
  3. npm run auth:bootstrap (with NEXT_PUBLIC_SUPABASE_URL + SUPABASE_SERVICE_ROLE_KEY; optional BOOTSTRAP_*_PASSWORD) → creates the demo owner (tony@tonys-pizza.example) + platform admin (ops@pizzapos.example) Auth users, linked to the seed by the bridge.
  4. Sign in at /login (owner) and /platform/login (admin). Demo staff PINs: Tony 1111 · Carmela 2222 · Christopher 3333 · Furio 4444.

Full walkthrough in docs/PRODUCTION_READINESS.md §1a.

🤖 Generated with Claude Code

…g, staff PIN, customer magic-link

Make authentication real, env-guarded like the data driver + payment rails:
with no Supabase env the app runs SIMULATED auth (seeded demo session derived
from memberships) so build + Vitest + preview stay green with zero env; with
Supabase env present it uses REAL Supabase Auth.

- Session plumbing via @supabase/ssr: browser + cookie-backed server clients,
  middleware that refreshes the session + coarse-gates protected routes, and
  getServerSession()/getCurrentUser() returning user + memberships + platform
  flag from the session (src/lib/auth/*).
- Identity-bridge migration: trigger linking auth.users -> public.users
  (auth.uid() == users.id, links seed users by email via ON UPDATE CASCADE);
  adds staff.pin_hash + orders.staff_id. Idempotent; skipped on vanilla PG.
- Auth flows/UI: /login (magic-link + optional password), /login/choose tenant
  chooser, /platform/login (admin-only), /forbidden, /auth/callback + /auth/
  signout, sign-out controls. /shop customer magic-link now uses real Supabase
  OTP in real mode (stub fallback otherwise).
- Role gating matrix (middleware + server guards): /admin owner|manager;
  /terminal+/kitchen operational roles; /platform platform_admin; /shop public.
  Gated pages are server components resolving session-derived tenant/location;
  tenant-scoped API routes re-check session/membership (src/lib/auth/api.ts).
- Staff PIN quick-switch: device logs in (session), cashiers switch active
  staff by PIN verified server-side against staff.pin_hash (scrypt; hash never
  leaves server); orders attributed via staff_id.
- Bootstrap script (npm run auth:bootstrap) creates owner + platform-admin
  Supabase Auth users linked to the seed. Docs + .env.example + seed updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
pos Ready Ready Preview, Comment Jun 6, 2026 4:51am

Request Review

@snackman
snackman marked this pull request as ready for review June 6, 2026 04:54
@snackman
snackman merged commit 63275c7 into main Jun 6, 2026
5 checks passed
@snackman
snackman deleted the real-auth branch June 6, 2026 04:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant