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:
- Import
./lib/env.jsto validate environment variables at process start. - Initialize Sentry via
initSentry({ dsn, environment }). - Build app via
createApp(). - Start HTTP server with
@hono/node-serveronenv.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:
secureHeaders()cors({...})correlationMiddlewaredbMiddlewarehonoLogger()- Route-level middleware (
auth,rbac,tenant, profile-specific) - Global error handlers (
onError,onNotFound)
CORS and Allowed Headers
CORS is configured from CORS_ALLOWED_ORIGINS and explicitly allows:
AuthorizationContent-TypeIdempotency-KeyX-Correlation-Id
Credentials are allowed (credentials: true).
Route Topology
All routes are mounted under createApp().
Service and Health
GET /— root checkGET /health— health check
Versioned API groups
/api/v1/auth->authRouter/api/v1/consumer->consumerRouter/api/v1/pos->posRouter(POS operator code verification)/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)
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /me | authProfileMiddleware | Profile state for authenticated user |
Consumer (/api/v1/consumer)
| Method | Path | Permission | Description |
|---|---|---|---|
| GET | /wallets | WALLET_VIEW_OWN | List all wallets for user |
| GET | /wallets/:walletId | WALLET_VIEW_OWN | Single wallet detail |
| GET | /wallets/:walletId/history | WALLET_HISTORY_OWN | Ledger entries (limit capped at 100) |
| POST | /wallets/:walletId/redeem/preview | WALLET_REDEEM | Redemption preview calculation |
| POST | /wallets/:walletId/redeem/confirm | WALLET_REDEEM | Execute redemption with row lock |
Client (/api/v1/client)
| Method | Path | Permission | Description |
|---|---|---|---|
| POST | /bootstrap-merchant-workspace | TENANT_PROFILE_UPDATE | Idempotent workspace creation |
| GET | /tenants/:tenantId | TENANT_VIEW | Dashboard overview with stats |
| PATCH | /tenants/:tenantId/onboarding | TENANT_PROFILE_UPDATE | Update onboarding profile |
| GET | /tenants/:tenantId/pos-connections | TENANT_VIEW | List POS connections |
| POST | /tenants/:tenantId/pos-connections/square/connect | TENANT_POS_CONNECT | Initiate Square OAuth |
| POST | /tenants/:tenantId/pos-connections/clover/connect | TENANT_POS_CONNECT | Initiate Clover OAuth |
| DELETE | /tenants/:tenantId/pos-connections/:connectionId | TENANT_POS_CONNECT | Disconnect POS (soft delete) |
| GET | /tenants/:tenantId/reward-rules | TENANT_VIEW | List all reward rule versions |
| POST | /tenants/:tenantId/reward-rules | TENANT_REWARD_CONFIG | Create new reward rule version |
| GET | /tenants/:tenantId/transactions | TENANT_ANALYTICS_VIEW | Transaction feed (limit capped at 100) |
| GET | /tenants/:tenantId/consumers | TENANT_CONSUMER_VIEW | Wallet holders list |
| POST | /tenants/:tenantId/branding/logo | TENANT_PROFILE_UPDATE | Upload tenant logo (base64, max ~10 MB) |
| DELETE | /tenants/:tenantId/branding/logo | TENANT_PROFILE_UPDATE | Remove tenant logo |
| GET | /tenants/:tenantId/store-operators | TENANT_TEAM_INVITE | List operators + pending invites |
| POST | /tenants/:tenantId/store-operator-invites | TENANT_TEAM_INVITE | Invite operator by email |
| DELETE | /tenants/:tenantId/store-operator-invites/:inviteId | TENANT_TEAM_INVITE | Revoke pending invite |
| DELETE | /tenants/:tenantId/store-operators/:userId | TENANT_TEAM_INVITE | Remove active operator |
| GET | /debug/square | TENANT_VIEW | Square config diagnostics (no secrets) |
Admin (/api/v1/admin)
| Method | Path | Permission | Description |
|---|---|---|---|
| GET | /tenants | ADMIN_TENANTS_VIEW_ALL | List all tenants (limit 200) |
| GET | /tenants/:tenantId | ADMIN_TENANTS_VIEW_ALL | Tenant detail with members and POS |
| POST | /wallets/:walletId/freeze | ADMIN_WALLET_FREEZE | Freeze wallet + audit log |
| POST | /wallets/:walletId/unfreeze | ADMIN_WALLET_FREEZE | Unfreeze wallet + audit log |
| POST | /wallets/:walletId/adjust | ADMIN_WALLET_ADJUST | Manual points adjustment + audit log |
| GET | /audit-logs | ADMIN_AUDIT_LOGS_VIEW | Immutable audit trail (limit 200) |
| GET | /users/search | ADMIN_USER_SEARCH | Search users or list pending approvals |
| PATCH | /users/:userId/approve | ADMIN_USER_MANAGE | Approve user + auto-provision workspace |
| PATCH | /users/:userId/role | ADMIN_USER_MANAGE | Change user role + audit log |
POS Operator (/api/v1/pos)
| Method | Path | Permission | Description |
|---|---|---|---|
| GET | /context | REDEMPTION_VERIFY | Resolve single-tenant context for operator |
| POST | /redemptions/lookup | REDEMPTION_VERIFY | Look up redemption code without burning |
| POST | /redemptions/complete | REDEMPTION_VERIFY | Burn redemption code (SELECT FOR UPDATE) |
Webhooks (/api/v1/webhooks) — No JWT auth
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /square | HMAC signature | Square webhook receiver (fast-ack) |
| POST | /clover | HMAC signature | Clover webhook receiver (fast-ack, handles verification handshake) |
OAuth Callbacks (/api/v1/oauth) — No JWT auth
| Method | Path | Description |
|---|---|---|
| GET | /square/callback | Square OAuth code exchange + redirect |
| GET | /clover/callback | Clover OAuth code exchange + redirect |
Internal (/api/v1/internal/cron)
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /square-poll-payments | X-Cron-Secret header | Poll Square ListPayments for all merchants |
| POST | /process-pending-webhooks | X-Cron-Secret header | Retry sweeper for unprocessed raw webhook events |
Request Context Contract
Context variables are attached by middleware and consumed by handlers:
db(Drizzle client) fromdbMiddlewarecorrelationIdfromcorrelationMiddlewareauthfrom strictauthMiddlewareauthProfilefromauthProfileMiddlewaretenantContextfromtenantMiddleware
Error Handling Model
apps/api/src/middleware/error-handler.ts centralizes response shaping.
Known error classes handled explicitly
AppErrorAuthErrorRbacErrorTenantErrorHTTPExceptionZodError
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 (
dbMiddlewarereferences 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_keysentries. - 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