Skip to content
PharmaFlow Knowledgebase
Esc
↑↓navigate↵open⌘Jpreview
On this page

Request Lifecycle

Follow one HTTP request through middleware, auth, handlers, and errors so you can trace any endpoint.

How a request enters the app

Open apps/server/src/app.ts first. Middleware runs in this order on every request: requestId() from hono/request-id assigns a correlation id, structuredLogger from @hono/structured-logger logs start and completion, cors() enforces the origin list, then a small redirect strips trailing slashes with a 307. Routes mount after that, so /api/v1/sales/ becomes /api/v1/sales before your handler ever sees it.

The Bun entry apps/server/src/index.ts does not run middleware. It calls ensureLoginAttemptsTable() from apps/server/src/core/auth/login-attempts.ts, reads PORT through apps/server/src/core/config/env.ts (default 8000), and exports the app from apps/server/src/app.ts as a default export with port and fetch.

What the middleware logs

The structuredLogger setup in apps/server/src/app.ts writes a debug line on request start and an info line on response with method, path, status, and elapsed milliseconds. Its skip check drops /health entirely so liveness probes do not flood your logs. It builds the per request logger with createRequestLogger(...) from apps/server/src/core/logging/logger.ts, passing the request id from requestId(), so you can grep one id and see the full story.

How auth attaches your user

Protected routes call authMiddleware from apps/server/src/core/auth/middleware.ts. It reads the Authorization: Bearer <token> header, verifies the HS256 JWT with verifyAccessToken in apps/server/src/core/auth/token.ts, and checks claims sub, role, branch_id, and email (with org_id defaulting to "org"). It then re reads the user row so a role change or deactivation takes effect immediately, and throws 401 when the account is inactive. Handlers read the result with requireUser(context) from apps/server/src/core/common/http.ts, which throws a 401 when nobody is attached.

Auth also pins the tenant. authMiddleware calls applyTenantSearchPath(...) from apps/server/src/core/database/tenancy.ts with the token org_id: on Postgres the request runs inside one transaction so the tenant search_path dies with it, on SQLite it is a no op.

How roles gate handlers

After auth, routes add requireRoles(...) from apps/server/src/core/auth/middleware.ts. An admin always passes, everyone else must hold one of the listed roles. The requireAdmin helper is just requireRoles() with no listed roles, so only admins pass. Expect a 403 with the role name in the message when access is denied.

How errors become responses

Throw HttpError from apps/server/src/core/common/http.ts whenever the client did something wrong. The onError handler in apps/server/src/app.ts turns it into { detail } with your status, maps a Zod failure to a 400 with [AUTH] Invalid request body, and turns anything else into a 500. Never leak stack traces; the detail string is the whole contract.

The full path as a chart

Try it with curl

Log in and reuse the token. Feature routes mount under /api/v1, so the login URL includes it.

curl -s -X POST http://localhost:8000/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"pharm1@demo.pharmaflow","password":"Demo@1234"}'
TOKEN="<paste access_token here>"
curl -s http://localhost:8000/api/v1/medicines \
  -H "Authorization: Bearer $TOKEN"

GET /health stays at the root and needs no token:

curl -s http://localhost:8000/health

Next steps

  • Read /architecture/branches-and-scoping to see how your branch id scopes every query
  • Read /architecture/offline-sync to see how devices replay the same handlers
  • Browse /api for the endpoint reference behind each route

Last updated on October 20, 2026