This repository is an Express + TypeScript authentication service for passwordless auth flows. It supports WebAuthn/passkeys, OTP, magic links, JWKS publication, session management, admin endpoints, and a small system configuration layer persisted in Postgres.
Use this file as the fast path. For the deeper architecture walkthrough, see docs/architecture.md.
These rules apply to every repository in the fells-code org. Repo-specific guidance may extend them but must not contradict them.
- Commit and open PRs solely under the repository owner's identity. Never commit under an agent or assistant identity.
- Never attribute work to an AI assistant: no
Co-Authored-By: Claude(or any assistant) trailers, no "Generated with" / "Created with Claude" notes, and no assistant branding or emoji anywhere in commit messages, PR or issue titles and descriptions, changesets, code comments, or docs.
- Comment only when the code genuinely needs explaining: a non-obvious reason, a gotcha, or an invariant. Never narrate what the code plainly does.
- Every
TODO/FIXMEmust reference a ticket, e.g.// TODO(#123): .... Do not leave a bare TODO. If no ticket exists, create one first.
- Conventional Commits (
feat:,fix:,chore:,docs:,ci:,test:). - Descriptive branch names (
feat/...,fix/...); never aclaude/or other tool-generated prefix.
- No em dashes in commit messages, code comments, PR or issue text, changesets, or docs. Use a comma, parentheses, or a separate sentence.
- All code quality checks must pass before you open a PR or call the work done: tests, linting, type checks, and formatting. Run them and report the real output; do not open a PR while any check is failing.
- Typical commands:
npm run typecheck,npm run lint,npm run format:check(ornpm run format), andnpm test. Never claim a change works without running them. - Match the surrounding code's style, naming, and comment density.
- Install dependencies:
npm install - Run in development:
npm run dev - Type-check/build:
npm run build - Lint:
npm run lint - Run tests once:
npm run test:run - Coverage:
npm run coverage
The service starts from src/server.ts, which initializes models, connects to the database, bootstraps system_config, builds the Express app, and begins listening.
- src/app.ts: global middleware, CORS, OpenAPI docs in dev, rate limiting, and top-level error handlers.
- src/lib/loadRoutes.ts: dynamically imports every
*.routes.tsfile undersrc/routes. - src/lib/createRouter.ts: thin wrapper used by route modules.
- src/lib/defineRoute.ts: registers Express handlers and OpenAPI metadata, validates request payloads, and optionally validates JSON responses.
When tracing behavior, start at the route file, then the controller, then the service/model/helper layers.
- src/routes: endpoint registration
- src/controllers: request handling
- src/services: session issuance, messaging, bootstrap promotion, auth event logging
- src/models: Sequelize models
- src/config: bootstrapped system config, required config metadata
- src/lib: routing, cookies, tokens, OpenAPI helpers
- src/middleware: auth, rate limits, logging
- tests: unit, integration, e2e, and shared factories/setup
There are three token states worth keeping straight:
- Ephemeral token: short-lived pre-auth token used to continue a registration or login flow up to the point a factor is proven. It is issued from an identifier alone, so it proves possession of an address, not of the account.
- Access token: signed JWT used for authenticated application access.
- Refresh token: opaque random token stored hashed in the
sessionstable.
That first distinction is load bearing. /login and /registration/register both mint an
ephemeral token for an account that already exists, from an email address alone, so an
ephemeral token authorises continuing a flow and nothing else. Anything that changes what
an account can sign in with takes an access session instead. Passkey enrollment is on
auth: 'access' for that reason, alongside TOTP enrollment and credential deletion.
The API exposes a single bearer/JSON auth contract:
- Ephemeral, access, and refresh tokens are returned in JSON response payloads.
- Protected routes expect bearer credentials from a trusted server adapter or backend.
- The API does not set or read browser auth cookies.
Auth middleware is chosen centrally by src/middleware/attachAuthMiddleware.ts.
There are two config layers:
- Environment variables: needed to boot the process and connect external systems.
system_configtable: runtime configuration such as token TTLs, origins, roles, and WebAuthn RP settings.
src/config/bootstrapSystemConfig.ts seeds missing system_config rows from env vars at startup. src/config/getSystemConfig.ts caches reads in-process, so remember to invalidate the cache after writes.
OTP and magic link flows can operate in two modes:
- Direct delivery: the API sends email/SMS itself via src/services/messagingService.ts.
- External delivery: callers send header
x-seamless-auth-delivery-mode: externaland the API returns delivery payloads instead of sending them directly.
Direct provider wiring currently lives in src/config/directMessaging.ts.
- Prefer changing controllers/services over adding logic directly in routes.
- When adding or updating routes, use
schemas, notschema, so request parsing and OpenAPI generation both work. - Public, ephemeral, and token-issuing routes must declare
adapterindefineRoute(see src/lib/adapterManifest.ts). It feeds/.well-known/seamless-adapter.json, which server adapters follow instead of a hard-coded route list. Runnpm run generate:apiafter route changes soopenapi.jsonandadapter-manifest.jsonstay committed in step. - If a route requires auth, prefer the
authoption increateRouterdefinitions. That keeps middleware wiring and OpenAPI security metadata aligned. - If a route also needs admin checks or rate limiting, combine
authwith extramiddleware. - Be careful around token response shapes and bearer auth. Browser-cookie auth mode has been removed.
- Preserve existing local worktree changes unless the user explicitly asks you to clean them up.
- Comment, attribution, TODO, and public-facing-text rules live in Working Standards above.
- Run
npm run build - Run
npm run lint - Run targeted tests for the touched area when practical
- If you change request/response schemas or route auth, inspect the generated
/openapi.jsonbehavior in dev
- OpenAPI security metadata is only added when routes use the
authfield in route definitions. Routes that manually callattachAuthMiddleware(...)can drift from the documented security model. - Cookie names in runtime code should be treated as the source of truth when checking auth behavior.
system_configvalues may come from the database even when.envchanges, so config bugs are not always fixed by editing env vars alone.