Skip to content

Operator dashboard and customer app share one better-auth identity (same user table, same secret) #120

Description

@AbirAbbas

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 designapps/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

  1. 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.
  2. Shared secret = shared blast radius. One leaked AF_STACK_AUTH_SECRET forges sessions for both surfaces at once.
  3. 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.
  4. 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.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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions