You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 in suite_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:3000 and localhost: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_SECRET forges 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.
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.md so 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.
Summary
The operator dashboard (
apps/dashboard) and the customer app (apps/customer-app) run separate better-auth instances against the same Postgres database, the sameuser/session/accounttables, and the sameAF_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.tsstates it explicitly: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 byrequireOperator(); they cannot reach admin routes.Why it's still worth hardening
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.AF_STACK_AUTH_SECRETforges sessions for both surfaces at once.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.Options to consider
AF_STACK_AUTH_SECRET) and/or distinct cookie names/prefixes so sessions don't cross-validate.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.mdeither way.