Architecture
This page explains the internal mechanisms of UseBetter Console: the request flow through the middleware and core, how routing and authentication work, and the security model.
Request flow
Section titled “Request flow”When a request arrives at your Hono app, it passes through the Console middleware:
- Middleware intercept —
createConsoleMiddleware()checks if the URL starts with/.well-known/better/. If not, the request passes through to your normal routes. - Request conversion — the middleware converts the Hono
Requestinto aConsoleRequest(method, path, headers, query, body) and strips the/.well-known/betterprefix from the path. - Route matching —
ConsoleRouter.match()finds a registered route matching the method and path. If no route matches, a 404 is returned. - CORS — for
OPTIONSrequests, preflight CORS headers are returned immediately (204). For all other requests, CORS headers are added to the response based onallowedOrigins. - Authentication — if the route requires auth (
requiresAuth: true), the router extracts theAuthorization: Bearer <token>header, verifies the JWT, checks permissions, and attaches thesessionto the request. - Handler execution — the matched handler receives the enriched request and returns a
ConsoleResponse. The middleware converts it back to a standardResponse.
Middleware as thin adapter
Section titled “Middleware as thin adapter”The Hono middleware is intentionally minimal. It performs only two tasks:
- Convert between Hono’s
Request/Responseand Console’sConsoleRequest/ConsoleResponse - Route requests that start with the base path (
/.well-known/better/) tohandleConsoleRequest()
All routing, authentication, CORS handling, and error recovery live inside the core handleConsoleRequest() function. This design means:
- The middleware has no knowledge of routes, auth, or session logic
- Adding support for other frameworks (Express, Fastify) requires only a thin adapter
- Testing the core does not require an HTTP server
Routing
Section titled “Routing”ConsoleRouter is a simple pattern-matching router that registers two kinds of routes:
Console routes
Section titled “Console routes”Built-in routes registered at startup, prefixed with /console/:
GET /console/healthGET /console/capabilitiesPOST /console/session/initPOST /console/session/verify (only with adapter)GET /console/session/poll (only with adapter)POST /console/session/claim (only with adapter)Console routes are unauthenticated — they handle the session handshake and health checks.
Product routes
Section titled “Product routes”Registered via registerProduct(), prefixed with /<productId>/:
GET /tenant/tenantsGET /tenant/tenants/:idPOST /tenant/tenantsDELETE /tenant/tenants/:idProduct routes are always authenticated and require a valid session token with the specified permission level.
Path matching
Section titled “Path matching”Routes support :param segments for dynamic path parameters:
Pattern: /tenant/tenants/:idActual: /tenant/tenants/550e8400-e29b-41d4-a716-446655440000Params: { id: "550e8400-e29b-41d4-a716-446655440000" }The router performs exact segment matching — the pattern and actual path must have the same number of segments.
Authentication internals
Section titled “Authentication internals”Auto-approve (stateless JWT)
Section titled “Auto-approve (stateless JWT)”In auto-approve mode, initSession() signs a JWT immediately using the connection token secret. The JWT payload contains:
sessionId— random UUIDemail— from the request body (defaults to"dev@localhost")permissions— fromallowedActionsconfigexpiresAt— current time +tokenLifetime
No database interaction occurs. The JWT is verified on each subsequent request by decoding and checking the signature and expiry.
Magic link (database-backed)
Section titled “Magic link (database-backed)”Magic link sessions use the database adapter for persistent storage:
- Init — generates a random 6-character code, SHA-256 hashes it, stores a
ConsoleMagicLinkrecord with a uniquesessionId, and sends the raw code to the user’s email. - Verify — the user submits the code. The server hashes it and compares against the stored hash. Failed attempts are tracked; after
maxAttemptsfailures, the magic link is locked. - Claim — creates a
ConsoleSessionrecord in the database with a new session token hash. Returns a signed JWT to the client. The claim is idempotent — it usesWHERE token_hash IS NULLto prevent double-claiming.
Session verification on subsequent requests: the JWT is decoded, the token hash is computed, and the session is looked up in the database by token hash. If the session exists and hasn’t expired, the request proceeds.
CORS is handled at the core level, not in the middleware. Every response from handleConsoleRequest() includes CORS headers when the request’s Origin header matches an entry in allowedOrigins.
- Preflight (OPTIONS) — returns 204 with
Access-Control-Allow-Origin,Access-Control-Allow-Methods(GET, POST, PATCH, DELETE, PUT, OPTIONS),Access-Control-Allow-Headers(Content-Type, Authorization), andAccess-Control-Max-Age. - Normal requests —
Access-Control-Allow-Originis set to the matched origin. If no origin matches, no CORS headers are added (the browser blocks the request).
The default allowed origin is https://console.usebetter.dev.
Security model
Section titled “Security model”Token hashing
Section titled “Token hashing”The connection token is never stored in plaintext. During init, the CLI generates a random token and its SHA-256 hash. Only the hash (sha256:<hex>) is stored in the environment. The raw token is shown once and then discarded.
At runtime, the hash is used as the JWT signing secret. This means:
- The JWT cannot be forged without knowing the hash
- Rotating the hash invalidates all existing JWTs
- The original raw token is not needed after setup
Strength enforcement
Section titled “Strength enforcement”In production (NODE_ENV is not "development" or "test"), the connection token secret must be at least 32 characters. Shorter secrets throw ConsoleWeakSecretError at startup.
Brute-force protection
Section titled “Brute-force protection”Magic link code verification tracks failed attempts per magic link. After maxAttempts (default 5), the magic link is locked — no further verification attempts are accepted. The user must initiate a new session.
Magic link codes expire after 10 minutes regardless of attempts.
CORS restriction
Section titled “CORS restriction”By default, only https://console.usebetter.dev can make cross-origin requests to your console endpoints. This prevents unauthorized frontends from accessing your data.
Auto-approve restriction
Section titled “Auto-approve restriction”Auto-approve is blocked in production via ConsoleAutoApproveInProductionError. This prevents accidental deployment of a configuration that grants instant admin access to anyone.
Next steps
Section titled “Next steps”- Configuration — full config reference, permissions, CORS
- Authentication — auth methods and session management
- Troubleshooting — common issues and fixes