Skip to main content

Backend Runtime

The backend runtime lives in apps/api and uses Hono on Node.js 20.

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.

Middleware Chain​

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

Order is important and currently:

  1. secureHeaders()
  2. cors({...})
  3. correlationMiddleware
  4. dbMiddleware
  5. request logger
  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 / returns { service, links }
  • GET /health returns { status: "ok", version, env }

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/client also mounts campaignsRouter
  • /api/v1/marketing -> marketingRouter
  • /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
GET/offersWALLET_VIEW_OWNOffers for the consumer's merchants
GET/offers/:campaignIdWALLET_VIEW_OWNOne offer the consumer can open
GET/profileWALLET_VIEW_OWNConsumer date of birth
PATCH/profileWALLET_VIEW_OWNSet consumer date of birth

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/metrics/dailyTENANT_ANALYTICS_VIEWDaily series for transactions and points
GET/tenants/:tenantId/analyticsTENANT_ANALYTICS_VIEWMerchant analytics for the path tenant. Query days is an integer from 7 to 90 and defaults to 30. Returns earn vs redeem, top consumers, redemption conversion, new vs returning, and revenue impact from stored ledger-linked purchase amounts minus used redemption discounts
GET/tenants/:tenantId/consumersTENANT_CONSUMER_VIEWWallet holders list
POST/tenants/:tenantId/branding/logoTENANT_PROFILE_UPDATEUpload tenant logo (base64, max ~10 MB)
DELETE/tenants/:tenantId/branding/logoTENANT_PROFILE_UPDATERemove tenant logo
PATCH/tenants/:tenantId/brandingTENANT_PROFILE_UPDATESet white-label programName, primaryColor, and accentColor. Colors are hex. null clears a field
GET/tenants/:tenantId/promotionsTENANT_REWARD_CONFIGList earn multipliers and bonus configs
POST/tenants/:tenantId/promotions/multipliersTENANT_REWARD_CONFIGCreate an earn multiplier window
DELETE/tenants/:tenantId/promotions/multipliers/:multiplierIdTENANT_REWARD_CONFIGRemove an earn multiplier window
PUT/tenants/:tenantId/promotions/bonusesTENANT_REWARD_CONFIGSave first-purchase, milestone, and birthday bonus configs
POST/tenants/:tenantId/promotions/flyerTENANT_REWARD_CONFIGDownload a PDF flyer. Body is { "offer": "..." }. The PDF heading is the tenant business name
GET/tenants/:tenantId/store-operatorsTENANT_TEAM_INVITEList operators + pending invites
POST/tenants/:tenantId/store-operator-invitesTENANT_TEAM_INVITEInvite operator by email
DELETE/tenants/:tenantId/store-operator-invites/:inviteIdTENANT_TEAM_INVITERevoke pending invite
DELETE/tenants/:tenantId/store-operators/:userIdTENANT_TEAM_INVITERemove active operator
GET/debug/squareTENANT_VIEWSquare config diagnostics (no secrets)
GET/tenants/:tenantId/campaignsTENANT_CAMPAIGN_VIEWList campaigns
POST/tenants/:tenantId/campaignsTENANT_CAMPAIGN_MANAGECreate campaign
GET/tenants/:tenantId/campaigns/:campaignIdTENANT_CAMPAIGN_VIEWLoad campaign + sanitized HTML
PATCH/tenants/:tenantId/campaigns/:campaignIdTENANT_CAMPAIGN_MANAGEEdit draft campaign
POST/tenants/:tenantId/campaigns/:campaignId/previewTENANT_CAMPAIGN_MANAGERender a campaign preview
POST/tenants/:tenantId/campaigns/:campaignId/scheduleTENANT_CAMPAIGN_MANAGEMaterialize recipients and schedule send
POST/tenants/:tenantId/campaigns/:campaignId/cancelTENANT_CAMPAIGN_MANAGECancel a draft or scheduled campaign
GET/tenants/:tenantId/campaigns/:campaignId/statsTENANT_CAMPAIGN_VIEWCampaign recipient stats
POST/tenants/:tenantId/campaigns/mediaTENANT_CAMPAIGN_MANAGEUpload campaign media asset

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_MANAGEApprove user + auto-provision workspace
PATCH/users/:userId/roleADMIN_USER_MANAGEChange user role + audit log

POS Operator (/api/v1/pos)​

MethodPathPermissionDescription
GET/contextREDEMPTION_VERIFYResolve single-tenant context for operator
POST/redemptions/lookupREDEMPTION_VERIFYLook up redemption code without burning
POST/redemptions/completeREDEMPTION_VERIFYBurn redemption code (SELECT FOR UPDATE)

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

MethodPathAuthDescription
POST/squareHMAC signatureSquare webhook receiver (fast-ack)
POST/cloverHMAC signatureClover webhook receiver (fast-ack, handles verification handshake)

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
POST/clover-poll-paymentsX-Cron-Secret headerPoll Clover payments for all merchants
POST/process-pending-webhooksX-Cron-Secret headerRetry sweeper for unprocessed raw webhook events
POST/process-campaignsX-Cron-Secret headerSend queued campaign recipients

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 and never block the main flow.
  • OAuth state is HMAC-signed and time-limited.

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