ADR-0015: V5 auth and account model¶
- Status: Accepted
- Date: 2026-09-06
- Deciders: Jian (product owner)
Context¶
V5 (KAN-1118) needs real user accounts and workspace membership — today there is exactly zero
concept of a user, session, or workspace anywhere in the codebase (confirmed by grep across
packages/cli/packages/engine: the only "session"/"workspace" hits are unrelated — a kampong
dev process-lifetime comment (run-manager.ts:18) and npm-workspace package resolution
(cli.ts:255-273), not multi-tenancy). This decision must not need to be thrown away when V6
(KAN-1122) adds SSO/SCIM — building an account system now that has to be replaced, not extended,
when enterprise SSO lands would be exactly the kind of rework this project's staged-scoping
approach exists to avoid.
Decision¶
Better Auth, a TypeScript-native, framework-agnostic, self-hostable auth library, for V5's account system — not a hand-rolled session/password/OAuth implementation, and not a Next.js-coupled library (NextAuth/Auth.js) that doesn't fit a Fastify-served SPA as naturally.
- Sign-in methods for V5: email+password, plus GitHub OAuth (a natural fit for a developer-tool audience already comfortable authorizing a GitHub app). Both are supported directly by Better Auth's core and OAuth plugins with no custom cryptography or token-handling code written in this project.
- Session strategy: server-side session cookies, not JWTs. A single self-hosted backend (ADR-0013) has no need for stateless multi-region JWT validation; cookie sessions give a simpler, more auditable revocation story ("delete the session row") which matters more here than the marginal scalability JWTs would buy.
- Workspace modeling: Better Auth's
organizationplugin, which models organizations/members/roles directly — mapped onto this project's existing "workspace" vocabulary (ADR-0014'sworkspace_memberstable). This gives V6's RBAC (KAN-1123, ADR-0017) a role/permission primitive to build on directly, rather than V5 inventing its own membership model that V6 would need to reconcile with Better Auth's later. - SSO plugin, adopted now but unused until V6. Better Auth ships an
ssoplugin implementing OIDC and SAML as a relying party (i.e., accepting a customer's own identity provider as the authentication source for their workspace) — exactly what V6's SSO/SCIM requirement (KAN-1122) needs. Choosing Better Auth now, with this plugin available but dormant, means V5 and V6 share one auth system end to end rather than V6 requiring a migration off whatever V5 shipped. - Database adapter: Better Auth's Drizzle adapter, matching ADR-0014's ORM choice — one ORM, one migration pipeline, shared between the auth system's own tables and the rest of the application schema, rather than two separate data-layer stacks.
Why a library, not hand-rolled, when this project otherwise avoids adding dependencies casually
(ADR-0004 rejected a gateway, ADR-0013 rejected a custom LiteLLM-equivalent): authentication is
different in kind, not degree. A missing LLM failover is a reliability gap with a visible, bounded
blast radius (one run fails, visibly, per ADR-0004's own philosophy); a bug in hand-rolled password
hashing, session-fixation handling, or OAuth state validation is a security breach with an
unbounded blast radius and no visible failure mode until it's exploited. This is squarely the kind
of problem where a maintained, widely-used library is the responsible default, the same way this
project already defers to Mastra for LLM provider plumbing rather than writing its own (ADR-0003)
and to yaml for comment-preserving parsing rather than a bespoke parser (ADR-0007).
Alternatives considered¶
| Option | Why not |
|---|---|
| Hand-rolled session/password/OAuth implementation | Security-critical code with an unbounded failure blast radius; no reason to take on that risk when a maintained library exists and fits the stack. |
| NextAuth/Auth.js | Historically coupled to Next.js conventions; a worse fit for a Fastify-served React SPA than a genuinely framework-agnostic library. |
| Self-hosting Keycloak or Authentik as this app's own primary identity provider | Conflates two different roles: being an IdP (what Keycloak/Authentik are) versus accepting SSO from a customer's IdP (what V6 actually needs, as a relying party). This app is the relying party, not the identity provider, for its own V5 signups. |
| JWT-based stateless sessions | No multi-region/stateless-validation requirement exists for a single self-hosted backend; cookie sessions give a simpler revocation story with no offsetting benefit foregone. |
Consequences¶
packages/serverdepends onbetter-authand its Drizzle adapter; every hosted API route gains an auth/session middleware resolving the current user and activeworkspace_id— the same value ADR-0014's RLS policies key their tenant-scoping session variable on, meaning auth and multi-tenancy enforcement share one seam rather than being independently-implemented concerns that happen to agree by convention.- V6's RBAC (ADR-0017) extends Better Auth's
organization/role primitives rather than replacing them; V6's SSO (KAN-1122) activates the already-adoptedssoplugin per-workspace rather than swapping auth systems. - Better Auth becomes a real external dependency this project must track and upgrade — an accepted, bounded cost matching how ADR-0003 already accepted the same trade-off for Mastra.
Open questions for V5 implementation to revisit¶
- Whether email+password requires standing up transactional email (an SMTP relay, for verification and password-reset emails) as new homelab infrastructure, or whether a first cut is OAuth-only (GitHub) to avoid that dependency entirely.
- Exact session cookie lifetime and refresh/rotation policy.
- Whether additional OAuth providers (Google, GitLab) are worth adding at V5 launch or are a demand-driven follow-on.