Skip to main content

Backend Runtime

The backend runtime is implemented in apps/api using Hono and Node.js.

Startup Sequence

apps/api/src/index.ts performs startup in this order:

  1. Import ./lib/env.js to validate environment variables at process start.
  2. Initialize Sentry via initSentry({ dsn, environment }).
  3. Build app via createApp().
  4. Start HTTP server with @hono/node-server on env.PORT.

If env validation fails, startup exits immediately (process.exit(1)).

Middleware Chain

Defined in apps/api/src/app.ts.

Order is important and currently:

  1. secureHeaders()
  2. cors({...})
  3. correlationMiddleware
  4. dbMiddleware
  5. honoLogger()
  6. Route-level middleware (auth, rbac, tenant, profile-specific)
  7. Global error handlers (onError, onNotFound)

CORS and Allowed Headers

CORS is configured from CORS_ALLOWED_ORIGINS and explicitly allows:

  • Authorization
  • Content-Type
  • Idempotency-Key
  • X-Correlation-Id

Credentials are allowed (credentials: true).

Route Topology

All routes are mounted under createApp().

Service and Health

  • GET / — root check
  • GET /health — health check

Versioned API groups

  • /api/v1/auth -> authRouter
  • /api/v1/consumer -> consumerRouter
  • /api/v1/client -> clientRouter
  • /api/v1/admin -> adminRouter
  • /api/v1/webhooks -> webhooksRouter
  • /api/v1/oauth -> posOAuthRouter
  • /api/v1/internal/cron -> cronRouter

Complete Endpoint Reference

Auth (/api/v1/auth)

MethodPathAuthDescription
GET/meauthProfileMiddlewareProfile state for authenticated user

Consumer (/api/v1/consumer)

MethodPathPermissionDescription
GET/walletsWALLET_VIEW_OWNList all wallets for user
GET/wallets/:walletIdWALLET_VIEW_OWNSingle wallet detail
GET/wallets/:walletId/historyWALLET_HISTORY_OWNLedger entries (limit capped at 100)
POST/wallets/:walletId/redeem/previewWALLET_REDEEMRedemption preview calculation
POST/wallets/:walletId/redeem/confirmWALLET_REDEEMExecute redemption with row lock

Client (/api/v1/client)

MethodPathPermissionDescription
POST/bootstrap-merchant-workspaceTENANT_PROFILE_UPDATEIdempotent workspace creation
GET/tenants/:tenantIdTENANT_VIEWDashboard overview with stats
PATCH/tenants/:tenantId/onboardingTENANT_PROFILE_UPDATEUpdate onboarding profile
GET/tenants/:tenantId/pos-connectionsTENANT_VIEWList POS connections
POST/tenants/:tenantId/pos-connections/square/connectTENANT_POS_CONNECTInitiate Square OAuth
POST/tenants/:tenantId/pos-connections/clover/connectTENANT_POS_CONNECTInitiate Clover OAuth
DELETE/tenants/:tenantId/pos-connections/:connectionIdTENANT_POS_CONNECTDisconnect POS (soft delete)
GET/tenants/:tenantId/reward-rulesTENANT_VIEWList all reward rule versions
POST/tenants/:tenantId/reward-rulesTENANT_REWARD_CONFIGCreate new reward rule version
GET/tenants/:tenantId/transactionsTENANT_ANALYTICS_VIEWTransaction feed (limit capped at 100)
GET/tenants/:tenantId/consumersTENANT_CONSUMER_VIEWWallet holders list
GET/debug/squareTENANT_VIEWSquare config diagnostics (no secrets)

Admin (/api/v1/admin)

MethodPathPermissionDescription
GET/tenantsADMIN_TENANTS_VIEW_ALLList all tenants (limit 200)
GET/tenants/:tenantIdADMIN_TENANTS_VIEW_ALLTenant detail with members and POS
POST/wallets/:walletId/freezeADMIN_WALLET_FREEZEFreeze wallet + audit log
POST/wallets/:walletId/unfreezeADMIN_WALLET_FREEZEUnfreeze wallet + audit log
POST/wallets/:walletId/adjustADMIN_WALLET_ADJUSTManual points adjustment + audit log
GET/audit-logsADMIN_AUDIT_LOGS_VIEWImmutable audit trail (limit 200)
GET/users/searchADMIN_USER_SEARCHSearch users or list pending approvals
PATCH/users/:userId/approveADMIN_USER_SEARCHApprove user + auto-provision workspace
PATCH/users/:userId/roleADMIN_USER_SEARCHChange user role + audit log

Webhooks (/api/v1/webhooks) — No JWT auth

MethodPathAuthDescription
POST/squareHMAC signatureSquare webhook receiver (fast-ack)
POST/cloverHMAC signatureClover webhook receiver (fast-ack)

OAuth Callbacks (/api/v1/oauth) — No JWT auth

MethodPathDescription
GET/square/callbackSquare OAuth code exchange + redirect
GET/clover/callbackClover OAuth code exchange + redirect

Internal (/api/v1/internal/cron)

MethodPathAuthDescription
POST/square-poll-paymentsX-Cron-Secret headerPoll Square ListPayments for all merchants

Request Context Contract

Context variables are attached by middleware and consumed by handlers:

  • db (Drizzle client) from dbMiddleware
  • correlationId from correlationMiddleware
  • auth from strict authMiddleware
  • authProfile from authProfileMiddleware
  • tenantContext from tenantMiddleware

Error Handling Model

apps/api/src/middleware/error-handler.ts centralizes response shaping.

Known error classes handled explicitly

  • AppError
  • AuthError
  • RbacError
  • TenantError
  • HTTPException
  • ZodError

Standard response shape

{
"code": "...",
"message": "...",
"requestId": "req_..."
}

For validation failures, issues is also returned.

Unhandled exceptions are logged and returned as INTERNAL_ERROR (500) without stack leakage.

Execution Characteristics

  • DB client is singleton-per-process (dbMiddleware references a shared instance).
  • Webhooks use fast-ack behavior with asynchronous post-response processing (setImmediate).
  • Consumer redemption confirmation uses SQL row locking (FOR UPDATE) to avoid concurrent over-redemption.
  • Ingestion dedup uses DB constraints and idempotency_keys entries.
  • Email notifications are fire-and-forget (never block main flow).
  • OAuth state is HMAC-signed with 10-minute TTL, not encrypted blobs.

Design Intent

The backend favors explicit boundaries:

  • transport concerns in route handlers,
  • domain decisions in services/packages,
  • cross-cutting concerns in middleware,
  • invariants in schema constraints and DB transactions.
Written byDhruv Doshi