Repository navigation
Description
Summary
The operator dashboard (apps/dashboard) and the customer app (apps/customer-app) run separate better-auth instances against the same Postgres database, the same user/session/account tables, and the same AF_STACK_AUTH_SECRET. As a result, a single account/credential authenticates against both apps. Raised while fixing the operator login (#119).
This is partly by design — apps/customer-app/src/lib/auth.ts states it explicitly:
the SAME better-auth tables (
user,session,account,verification) in the SAME Postgres database. [...] We separate them by tenant membership: customers [are tenants], operators are insuite_operators.
So the intended model is: shared authentication, separated at authorization — operator access is gated by suite_operators (requireOperator()), customer access by tenant membership. A customer who signs in at the dashboard gets a valid session but is bounced by requireOperator(); they cannot reach admin routes.
Why it's still worth hardening
- Cookie scoping in local dev. The two apps run on
localhost:3000andlocalhost:34000. Cookies are not isolated by port, and both use the same cookie name +AF_STACK_AUTH_SECRET, so a session minted by the customer app is also presented to — and validated by — the dashboard (same DB + secret). Authorization still blocks admin access, but the session is genuinely cross-valid, which is surprising and easy to get subtly wrong. - Shared secret = shared blast radius. One leaked
AF_STACK_AUTH_SECRETforges sessions for both surfaces at once. - One compromised customer credential is one
INSERT INTO suite_operators(or one buggy auto-grant) away from operator access, because the identity already exists in the operator app's user table. - The default operator account seeded in fix(dashboard): seed default operator, fix infinite /setup redirect #119 is, by the same mechanism, also a valid customer-app login.
Options to consider
- Separate the secret per app (distinct
AF_STACK_AUTH_SECRET) and/or distinct cookie names/prefixes so sessions don't cross-validate. - Separate auth schemas/databases for operator vs customer identity (strongest isolation; biggest change).
- Keep shared identity but make the authz boundary explicit and tested — e.g. an e2e test asserting a customer session is rejected from every dashboard route, and that the dashboard never auto-grants operator to a customer-origin user.
- At minimum, document the shared-identity model and its blast radius in
ARCHITECTURE.md/SECURITY.mdso operators set distinct secrets per surface in production.
Acceptance
Decide whether shared identity stays intended; if so, isolate cookies/secrets per surface and add a regression test proving cross-app authorization separation; otherwise split the identity stores. Update SECURITY.md either way.
Activity