Skip to main content

Module Boundaries

Each module in the monorepo has strict ownership rules. No module writes another module's database tables directly, and cross-module communication happens through well-defined interfaces.

Apps

apps/web (Next.js)

AspectDetails
OwnsBetter Auth config and routes (/api/auth/*), user sessions (HttpOnly cookie), passkey enrollment/login, social sign-in UX, token exchange endpoint
ExposesGET /.well-known/jwks.json (public key for JWT verification), POST /api/internal/token/exchange (session to JWT exchange)
Must NOTStore provider refresh tokens, poll device flow, call provider token endpoints directly

apps/api (Fastify)

AspectDetails
OwnsBusiness APIs, JWT validation plugin (via JWKS), OAuth integration service (auth code flow), encrypted token vault, RBAC/scope authorization, policy engine
Exposes/v1/* business APIs, /v1/oauth/{provider}/start (web auth-code connect), /v1/oauth/{provider}/callback (OAuth callback)
Must NOTHandle passkey ceremonies for app login, manage browser user sessions

Packages

PackageOwnsExportsMust NOT
packages/contractsZod schemas and TS types for all cross-service requests/responses, JWT claim interfaces (ApiAccessClaims, WorkspaceRole, Scope)Types, schemas, branded ID typesInclude runtime HTTP logic
packages/securityJWT sign/verify utilities, AES-256-GCM envelope encryption, key rotation helpers (kid, key versioning)JwtIssuer, JwtVerifier, EnvelopeEncryptor, EncryptedEnvelopeV1Know database schema details
packages/oauth-connectorsProvider metadata discovery, oauth4webapi wrappers for auth-code exchange, refresh, revokeOAuthConnector interface, ProviderCapabilities, per-provider implementationsAccess web session state

Data Ownership

DataOwnerNotes
users, sessions, accountsapps/webBetter Auth tables
oauth_connections, oauth_token_setsapps/apiProvider integrations
oauth_device_requestsapps/apiDevice flow state
audit_eventsapps/apiAudit trail
policies, policy_rulesapps/apiPolicy engine configuration

Token Model

The system uses three distinct token types, each scoped to its layer:

  • Scope: Browser only
  • Security: HttpOnly, SameSite=Lax/Strict
  • Lifetime: 30 days (configurable)
  • Contains: Session ID referencing the database

2. Internal API JWT (issued by apps/web)

  • Scope: Web UI to API calls
  • TTL: 5 minutes
  • Audience: api
  • Issuer: web
  • Claims: { sub, wid, roles, scp, sid }
  • Verification: JWKS from /.well-known/jwks.json

3. Provider Token Set (stored by apps/api)

  • Contents: access_token, refresh_token, expiry, provider_account_id
  • Storage: Encrypted at rest (AES-256-GCM) with key ID for rotation
  • Rotation: Key versioning supported via kid

Dependency Flow

apps/web ──────────> packages/contracts
| ^
| |
+──────────> packages/security
^
|
apps/api ──────────> packages/contracts
| ^
| |
+──────────> packages/security
|
+──────────> packages/oauth-connectors ──> packages/contracts

Packages depend only on other packages, never on apps. Apps depend on packages and never on each other -- they communicate over HTTP.