Kurier

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 a Caller, 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) via route(...).
  • 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-Key header → looked up in the api_keys table → { 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_DAYS via cron.
  • Webhooks are queued in the webhook_events outbox 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.