ByteBite Docs
Architecture

Staff authentication

Better Auth session required for every /dashboard API route and the dashboard SPA. Path context stays the tenant authority.

Staff authentication

Slice 9 wired Better Auth (ADR-019) for staff email + password. This slice (Auth → Dashboard-Gate) requires that session for every /dashboard/* API route and for the dashboard SPA.

Dashboard mutations stay on /dashboard/restaurants/:restaurantId/... (ADR-021). session.restaurantId is not this slice and does not replace the path. Guest checkout stays without a customer account. Storefront hosts have no staff login.

Staging Traefik HTTP BasicAuth is unchanged and is not a substitute for this session gate.

Session shape

Staff session identity is:

FieldSourceNotes
userIdBetter Auth session / user idOpaque string (Better Auth text id)
roleuser.role additional fieldRESTAURANT or ADMIN only (STAFF_ROLES)

There is no session.restaurantId and nothing reads one for authorization. A logged-in Luigi operator may still call Mario's dashboard path. Isolation Luigi ↔ Mario remains an API/DB property (WHERE restaurant_id = :pathId). Both RESTAURANT and ADMIN sessions may use the dashboard. /admin requires a session and has no extra role matrix.

Customer accounts are not this slice. Guest checkout is unchanged.

Tables

Better Auth + Drizzle core tables, plus a server-owned role on user. Order tables are untouched. No membership table.

user

ColumnNotes
idText (Better Auth default, not a product UUID)
name / emailEmail is unique
email_verifiedSeeds mark demo users verified
imageOptional
roleRESTAURANT | ADMIN. input: false — clients cannot set it
created_at / updated_atTimestamps

session

ColumnNotes
id / tokenSession row and unique token
user_idFK → user.id (ON DELETE cascade)
expires_atExpiry
ip_address / user_agentOptional
created_at / updated_atTimestamps

No restaurant_id column.

account

Credential accounts: provider_id = credential, account_id = the user's id, password = Better Auth hash.

verification

Required by Better Auth. Unused (no email verification flow).

API

Handler lives on Fastify at /auth/*. Browsers call same-origin /api/auth/*; Traefik already strips /api. There is no host-only Next /api rewrite.

Better Auth basePath is /auth because Fastify never sees the /api prefix. Cookie Path=/ so the browser sends the cookie on /api/auth/* and /api/dashboard/*. Cookies are host-only (no Domain) — storefront hosts do not receive a staff session.

MethodFastify pathBrowser (dashboard)
POST/auth/sign-in/email/api/auth/sign-in/email
POST/auth/sign-out/api/auth/sign-out
GET/auth/get-session/api/auth/get-session

Public sign-up is disabled. Operators come from seeds.

BETTER_AUTH_SECRET signs cookies. Production/staging without a secret: auth routes are not registered (no silent Better Auth default). Local and test may use the .env.example placeholder. When auth is unregistered, /dashboard/* still returns 401.

A single Fastify hook (dashboardAuthPlugin) rejects unauthenticated /dashboard/* with 401. Menu CRUD, media uploads, hours GET/PUT, restaurant list, order list/get, accept/reject, and kitchen writes are in that set. Body restaurantId still does not change the tenant.

Guest storefront (restaurant, menu, cart, checkout, order GET, hours GET) stays 200 without a staff session. /auth/*, /health, /media, and /webhooks stay public (existing tenant skip list).

Dashboard

Unauthenticated visits to /, /menu, /orders, and /admin redirect to /login. After login, the SPA returns to the intended page. Sign out goes to /login. The shell shows email + role after login.

/admin requires a session. There is no extra role matrix: RESTAURANT and ADMIN may both open it.

Seeds

Optional demo users (idempotent upserts). No orders.

EmailRolePassword (seed/test only)
restaurant@bytebite.testRESTAURANTrestaurant-seed
admin@bytebite.testADMINadmin-seed

These are not production secrets.

Isolation

  • Login with a seed user → session with userId + role. Dashboard menu/orders/hours/uploads return 200. Logout → no session.
  • Same dashboard calls without a cookie → 401, no writes (including logo upload and kitchen/accept).
  • Wrong password → Better Auth 401 or 400. No extra user-enumeration behavior.
  • Luigi guest checkout without a customer account stays 200.
  • Luigi orderId under Mario's dashboard path stays 404 (with a valid session).
  • Seeds create no orders.
  • Body restaurantId still does not change the tenant.

Not in this slice

Replacing the path with session.restaurantId, a user↔restaurant membership table, customer accounts, 2FA, OAuth, role-gating beyond “must be logged in”, BitePay, delivery, variants, e-mail sends, sound/PWA, production deploy. Closed checkout 409: Opening hours. SMS on accept/reject/timeout/ready: Transactional notifications.

On this page