Orthogonal to credentials: useAuth's session strategy (how the identity is persisted/validated/issued) is independent of the credential providers (login/signup/social/verification/mfa/magicLink). Any provider composes with any strategy — e.g. a Passport social provider issuing JWTs.
Single issuance path: every successful login (password, social, magic link, signup) goes through strategy.issue(c, userId); the auth middleware authenticates every request through strategy.authenticate(c). cookieSession mints a server-side session + cookie; jwtSession mints a bearer access token + refresh cookie and exposes /refresh.
Back-compat: a legacy adapter passed to useAuth is mapped to a session strategy internally, preserving its prior behavior.
Redirect URI component validation: redirect_uri is matched component-by-component (protocol, host, port, normalized path, and query/fragment when registered), never by prefix; an unregistered URI is rejected with 400 before any redirect is issued
Federated id_token verification: external id_tokens are signature-verified against the provider's JWKS (issuer + audience checked), the nonce is compared to the stored interaction nonce, and the id_tokensub is cross-checked against the userinfosub
External login resumes the interaction: a successful external callback (federated OIDC or backends.passport) establishes the provider session and continues the pending /authorize interaction to consent or the authorization-code redirect — the same completion path as email/password login; it never returns the user as a bare JSON body
Passport backend state binding: backends.passport carries the OAuth state/PKCE handle across the redirect→callback in a signed, httpOnly, short-lived cookie + state store; a callback with missing/expired/mismatched state is rejected before any token exchange
Correct at_hash: the id_tokenat_hash claim is the left-half of the hash matching the signing algorithm, computed whenever an access token is issued
Endpoint rate limiting: /token, /jwks, /introspect rate-limited per client/IP when security.rateLimiting is configured (KV-backed when a global KV exists)
Persistent stores by default: clients, codes, refresh tokens, consents, interactions, and state are KV-backed (with expiry-derived TTLs) whenever a global KV is registered; stores.type: "memory" forces in-memory
Dynamic registration is opt-in: POST /register returns 404 unless registration.enabled is true; an initialAccessToken, when set, is required as a Bearer token
Consent revocation + TTL: POST /consent/revoke clears a user's consent (one client or all) for an authenticated session; stored consents expire after security.consent.ttlSeconds (default 1 year)
login_hint escaping: dynamic values in the default login template are HTML-escaped
Password policy: when passwordPolicy is set, weak passwords are rejected with 422 on POST /signup and POST /password/reset; a built-in denylist of common passwords is enabled by default
MFA/TOTP: mfa adds /mfa/enroll, /mfa/enroll/confirm, and /mfa/verify; enrollment is two-step (created disabled, then confirmed). With requireOnLogin, /login returns { mfaRequired: true } (401) for enrolled users until a valid TOTP or single-use backup code is supplied; a used backup code triggers consumeBackupCode
Magic links: magicLink adds /magic-link/request (always returns { success: true }, anti-enumeration) and /magic-link/verify (single-use token, creates the session)
API keys: createApiKey/verifyApiKey/rotateApiKey/revokeApiKey store only a hash of the key; the raw key ([prefix_]<id>.<secret>) is returned once; verification returns a typed reason (not_found/expired/mismatch) on failure
Enforced allowlist: when fields.writable is configured, any table column not in the list is stripped from create/update bodies (single, batch, and POST /batch/upsert) before hooks and the database see it
Exemptions: the primary key and generatedFields are never stripped; non-column keys (relation payloads) pass through
Hook precedence: stripping happens before lifecycle hooks, so a server-side hook can still set protected fields
strictInput: with strictInput: true, unknown fields are rejected with 422 instead of being silently ignored
Session rotation: Session ID rotated on login (prior session invalidated, prevents fixation)
Logout invalidation: All session tokens invalidated on logout
Bulk invalidation: Adapters may implement invalidateUserSessions(userId, exceptSessionId?); password reset uses it when logoutEverywhere is set
Indexed bulk invalidation: When the session store implements the per-user index (getByUser/deleteByUser — the Redis, Drizzle, and in-memory stores all do), bulk invalidation touches only that user's sessions; the scan-all getAll() path is a fallback for stores without the index
Session activity metadata: Login/signup store the client IP and user agent in session.data (ipAddress, userAgent); session use stamps session.data.lastActiveAt, throttled to at most one persisted write per minute per session and fire-and-forget (activity tracking never blocks or fails auth)
Cookie security: HttpOnly, Secure (in production), SameSite attributes set
Allowlist semantics: When fields.readable is configured on a resource, only the listed table columns may appear in any response
Universal application: Masking applies to list, get, create, update, batch, and search responses, plus every subscription event (existing, added, changed) and the initial subscription snapshot
No client bypass: A hidden column cannot be recovered via ?select= or by subscribing — masking is enforced server-side after projection
Column-only: Only table columns are stripped; relation keys, computed values, and internal markers (_etag, _optimisticId) pass through
Included relations respect target scope: On the read path (GET / and GET /:id with ?include=), each included relation AND-s the target resource's read scope (resolved for the effective/impersonated user) into its query — a relation cannot return rows the user could not read directly
Deny semantics: A user denied read on the target resource yields null (belongsTo/hasOne) or an empty array (hasMany/manyToMany) for that relation; out-of-scope rows are filtered, not just hidden
Applies to discovered relations: Auto-discovered (autoRelations) relations use the same enforced loader as explicit ones
Unregistered targets: Relations to tables not registered as resources have no scope to enforce (no resolver) — these are an explicit author choice
Applies to subscriptions: Relations embedded in subscription events (existing, added, changed) are scope-filtered per subscriber — the subscriber's user is captured at subscribe time, and on every push the target resource's read scope is resolved for that user and AND-ed into the included relation, with the same deny semantics as the read path. A subscriber can never receive related rows it could not read directly. Relations are loaded per subscriber rather than shared across them (cost scales with subscriber count; loads are deduplicated per subscriber within a single push)
Logical contract is stable: SessionData (id/userId/createdAt/expiresAt/data) is always exposed in logical keys regardless of the underlying column names; remapping (defineInternalSchemafieldMap) applies only at the SQL persistence boundary — see Internal tables
Fail-fast validation: defineInternalSchema validates that every required logical key resolves to a column on the supplied table at construction time, never at query time
Defaults are unchanged: with no internalSchema, the built-in tables and the byte-for-byte original migration DDL are used; createCovara({ internalSchema }) is for migration/introspection and never silently rewires a pre-built store
Migration safety: generated DDL (migrateInternal mode c) covers single-primary-key tables only and throws for compound-PK tables (auth_accounts, auth_verification_tokens) rather than emitting an incorrect schema; managedExternally: true makes migration a no-op
KV session store is backend-agnostic: createKVSessionStore works over any KV adapter; its hash field names are a private serialization, never user-facing, and are not remappable
Users/files are app-owned: the framework never owns a users table (reached via getUserById) or a files table (supplied to fileResource); the changelog and rate limits are KV/memory-backed, not database tables
Defaults preserve behavior: the audit log, request/error logs, and metrics are append-only logs backed by an in-memory ring buffer when no KV is configured — identical to before; with a global KV they persist automatically
append never throws: a failing storage backend can never break the audited/served action; the in-memory mirror always records the entry even if a KV write fails
setAdminAuditSink is write-only: it runs alongside the adapter and is for forwarding only; durable read/query/export requires an ObservabilityLogAdapter
Cross-process reads: in KV mode synchronous reads reflect only the local process mirror; authoritative cross-instance reads use the async query()/export() path
Identity re-verification, no secret: The resource layer skips per-resource auth scopes only when the request carries the x-covara-admin-bypass marker and its authenticated user (or forwarded admin apiKey) passes the registered admin predicate. The marker is not a secret and grants nothing on its own
Leaked marker is inert: A non-admin presenting the marker is served under normal scope enforcement (fail-closed); the marker value is constant and confers no authority
Disabled by default: Bypass is inert unless an admin predicate is registered, which happens only when the admin UI is mounted via createCovara; standalone useResource never bypasses
Gated on admin auth: The admin predicate is derived from the UI's security config — locking down the UI's auth locks down bypass
Audited: Every bypassed API-explorer request is recorded in the admin audit log (api_explorer_execute)
Runs as the target, under their scope: A request carrying the x-covara-impersonate: <userId> marker resolves the impersonated user's per-operation scope and attributes writes to them — it does NOT grant allScope()
Identity re-verification, no secret: The marker is honored only when the forwarded request's real authenticated user passes the admin predicate (same test as bypass) and a userManager-backed resolver yields the target user; a forged/leaked marker from a non-admin is inert
Replaces bypass, never stacks: When impersonation is active the scope layer uses the impersonated user's scope and skips the bypass branch entirely — impersonating an admin yields only that admin's scope, never full bypass (no escalation)
Single effective-user swap: A middleware swaps c.get("user") to the impersonated user once, so scope, write attribution, and hooks all observe the same identity; admin-UI routes (/__covara/*) are exempt so admin authorization is unaffected
Audited with both ids: Every impersonated action records the real admin id and the impersonated user id (impersonate_execute, data_explorer_list)