Skip to content

Auth model

Auth is provided by better-auth with the organization, apiKey, and bearer plugins. Three modes resolve to the same (user, organizationId) on the request context.

  1. Cookie session. Set by POST /api/auth/sign-in/email. Browser UIs.
  2. Bearer session token. Authorization: Bearer <session_token>. Same identity as cookie path; lets non-browser clients (mobile, scripts) carry a session without a cookie jar.
  3. API key. Authorization: Bearer thd_… OR x-api-key: thd_…. Identity is the key, not a person. Configured with references: "organization" so a verified key resolves to its org in one call (no metadata join).

After the auth guard passes, every route handler reads:

c.get("user"); // { id, email }
c.get("organizationId"); // string
c.get("authMode"); // "session" | "api-key"
c.get("apiKeyId"); // string | undefined (only when api-key)
ResourceScopeCross-org access
Workfloworganization_idReturns 404, not 403 — we don’t reveal existence.
Scheduleorganization_idSame.
RunInherits from workflowSame.
API keyreferenceId = organizationIdA key works only for its org.
Webhook routeProgrammatic at bootNo HTTP surface to register.
SessionAPI key
Tied toA userThe org
LifetimeSession expiry (sliding)Until revoked
Used byBrowser UI, mobile appsLLM orchestrators, CI, server-to-server
Active orgsession.activeOrganizationIdKey’s referenceId
RevocationSign-out (this device) / password reset (all)DELETE /api/auth/api-key/:id

When an employee leaves, you don’t scramble through their personal keys — service tokens stay because they belong to the organization. That separation is deliberate.

databaseHooks.user.create.after inserts a personal organization + membership for every new user. The org plugin’s setActiveOrganizationOnSessionCreate (default true) auto-selects the only membership when the session has no active org. Result: a freshly-signed-up user can hit any protected route immediately — no explicit organization/create + set-active orchestration needed.

  • An empty apikey table = no programmatic request authorizes.
  • A user with no organizations = 401 no_active_organization.
  • Any path NOT under /api/auth/* and NOT in openPaths (just /health and /api/bootstrap) requires resolved identity.
  • THODARE_BOOTSTRAP=1 only opens /api/bootstrap if the user table is empty AND the env flag is set. Misconfig → 404.

Per-(organizationId, principal) token bucket. principal is the API key id when authenticating via key, or the user id when via session. Default 60 req/min. Bucket per-pair means one tenant cannot starve another, and one user’s session cannot starve another’s keys.

packages/api/src/auth.ts + middleware/session.ts.