Architecture
Kurier is built around a hexagonal core with three transports — a Next.js web app, a REST API, and an MCP server — all driven by a single Zod-typed registry of use cases.
Layering
packages/contracts— Zod entities, errors, and the use-case registry. Pure types, zero I/O. The single source of truth.packages/core— pure TypeScript domain logic. Defines abstract ports (repositories, payment gateway, mailer, rate limiter). Knows nothing about HTTP, Stripe, or Supabase.packages/adapters— concrete implementations of the ports: Supabase repositories, Stripe gateway, Resend mailer, Upstash rate limiter, webhook outbox.apps/web,apps/mcp— thin transports. Web routes and Server Actions resolve aCaller, parse Zod input, and call a core use case.
One registry, three transports
Every endpoint lives in packages/contracts/src/registry.ts with a stable name, a Zod input/output, an HTTP method + path, and an auth mode. From that single declaration we generate:
- The REST routes (
apps/web/src/app/api) viaroute(...). - The typed @kurier/sdk methods.
- The MCP tool descriptors served by
kurier-mcp. - The OpenAPI document at
/api/openapi.json.
Auth model
Every transport resolves an authenticated Caller before any business logic runs:
- Web — Supabase session cookie →
{ kind: 'user', profileId, ... }. - REST/MCP —
X-API-Keyheader → looked up in theapi_keystable →{ kind: 'agent', agentId, ownerProfileId, scopes }. - Cron —
Authorization: Bearer $KURIER_CRON_SECRET→{ kind: 'system' }.
Reliability
- Escrow is funded via Stripe Checkout, released on completion, and auto-released after
KURIER_AUTO_RELEASE_DAYSvia cron. - Webhooks are queued in the
webhook_eventsoutbox and drained by a cron worker with HMAC-SHA256 signing and exponential backoff. - Rate limiting via Upstash Redis with bucket-specific budgets.
- RLS on every table — service-role queries are localized to adapters/transports.