tx-agent-kit
Apps

API Server

Effect HttpApi server powering auth, organizations, teams, billing/subscriptions, and permissions with OpenAPI spec generation.

API Server (apps/api)

The API server is the backbone of tx-agent-kit. It is built on Effect HttpApi and exposes a fully typed, schema-validated REST API. The server runs on port 4000 by default.

Architecture

The API server follows a layered architecture where routes delegate to domain services, which in turn interact with repositories through Effect's dependency injection system.

Request -> Route Handler -> Domain Service -> Repository Port -> DB Repository

Each route handler extracts the authenticated principal from the Authorization header, invokes the appropriate domain service, and maps the result to an API response shape.

Route Groups

OpenAPI tags map directly to generated API reference pages. The full route surface lives in apps/api/src/routes/:

Route modulePath prefixPurpose
auth.ts/v1/auth/*Password auth, refresh rotation, Google OIDC, password reset
organizations.ts/v1/organizations/*, /v1/invitations/*Org CRUD, members, invitations, onboarding state
teams.ts/v1/teams/*Team (workspace) CRUD and team-member management
teams-brand.ts/v1/teams/{teamId}/brand-settingsTeam brand settings (logo, colors)
roles.ts/v1/roles/*Role + permission management (org-scoped)
permissions.ts/v1/permissions, /v1/permissions/meRole-permission map + current-user permission resolution
billing.ts/v1/organizations/{id}/billing, /v1/billing/*, /v1/webhooks/stripeSubscription state, top-up, credits, Stripe webhooks, auto-recharge
assets.ts/v1/teams/{id}/assets, /v1/teams/{id}/collections, presign/confirm/thumbnail endpointsMedia uploads, deduplication, thumbnails, collections
storage.ts/v1/storage/*Presigned upload/download URLs, object metadata, list/delete
storage-metering.ts/v1/organizations/{id}/storage-metering/*Storage usage reporting to Stripe metered billing
notifications.ts/v1/notifications/*In-app notification listing and mark-as-read
email-campaigns.ts/v1/teams/{id}/email-campaigns/*Reference implementation: CRUD, enrollments, analytics
email-webhooks.ts/v1/webhooks/email/resendResend delivery-event webhook handler
email-unsubscribe.ts/v1/email/unsubscribe/*Token-verified unsubscribe landing + POST
list-query.ts(internal helper)Shared list query builder used by other route files
health.ts/healthLiveness probe

For full request/response shapes, use the generated pages in docs/api-reference/* or runtime /openapi.json.

Billing and Roles

Billing authorization is role-gated:

  • owner and admin can manage billing (PATCH /v1/organizations/{organizationId}/billing, POST /v1/billing/checkout, POST /v1/billing/portal).
  • member cannot manage billing and receives 401 on management endpoints.
  • Subscription state is synced from Stripe webhooks (/v1/webhooks/stripe) into organization billing fields (isSubscribed, subscriptionStatus, subscriptionPlan).

Rate Limiting

Sensitive auth paths are protected by an in-memory rate limiter that enforces two independent limits per sliding window: one keyed by client IP address and one keyed by the authentication identifier (email address). Both must pass for a request to proceed.

VariableDefaultDescription
AUTH_RATE_LIMIT_WINDOW_MS60000Sliding window duration in milliseconds.
AUTH_RATE_LIMIT_MAX_REQUESTS15Maximum requests per IP per window.
AUTH_RATE_LIMIT_IDENTIFIER_MAX_REQUESTSSame as IP limitMaximum requests per identifier (email) per window.

The set of rate-limited paths is defined centrally in @tx-agent-kit/contracts (authRateLimitedPaths).

Proxy trust

When the API runs behind a load balancer or reverse proxy, client IPs arrive in the X-Forwarded-For header rather than the socket address. Set TRUST_PROXY=true (or 1) to extract the leftmost IP from that header. The default is false, which uses the direct socket address.

429 response shape

When a client exceeds either limit the API returns HTTP 429 with a Retry-After header (seconds) and the following body:

{
  "error": {
    "code": "TOO_MANY_REQUESTS",
    "message": "Too many authentication attempts. Please try again later."
  }
}

Domain Events

The API writes domain events transactionally using *WithEvent port methods. Each mutation that produces a domain event inserts the event row inside the same database transaction as the business write, guaranteeing at-least-once delivery semantics via the outbox pattern.

The API never imports @temporalio/* directly. Events reach the worker exclusively through the outbox table, which the worker polls and processes. This keeps the API deployment free of Temporal SDK dependencies and ensures that event publication cannot fail independently of the business write.

Kind Markers

Every route file declares an explicit kind marker that must match the corresponding repository port kind:

// apps/api/src/routes/organizations.ts
export const OrganizationsRouteKind = 'crud' as const

// apps/api/src/routes/auth.ts
export const AuthRouteKind = 'custom' as const

The enforcement layer validates that crud routes expose the full list/get/create/update/remove surface, and that custom routes do not accidentally implement the full CRUD pattern.

OpenAPI Generation

The API spec is generated from the Effect HttpApi definitions:

pnpm openapi:generate

This produces apps/api/openapi.json, which is then consumed by Orval to generate typed client hooks for the web and mobile apps.

When the API server is running (default http://localhost:4000), runtime docs are available at:

  • /docs for Swagger UI
  • /openapi.json for the raw OpenAPI document

Both runtime endpoints come from the same Effect HttpApi definitions. The generated apps/api/openapi.json remains the repo artifact used by downstream generation workflows (including pnpm docs:api:generate).

Security Headers

Every API response carries a fixed set of security headers applied by securityHeadersMiddleware in apps/api/src/middleware/security-headers.ts:

HeaderValue
strict-transport-securitymax-age=63072000; includeSubDomains
x-content-type-optionsnosniff
x-frame-optionsDENY
referrer-policystrict-origin-when-cross-origin
permissions-policycamera=(), microphone=(), geolocation=()
x-download-optionsnoopen
x-permitted-cross-domain-policiesnone
cache-controlno-store

No configuration is required; headers are always applied on all responses including error paths.

Config and Environment

Runtime environment variables are read exclusively through apps/api/src/config/env.ts. Direct process.env access is forbidden in route and service modules. The module caches parsed config on first call; resetApiEnvCache() is exported for use in tests.

Required variables

VariableDescription
NODE_ENVRuntime environment (development, staging, production)
API_PORTPort the server listens on (default: 4000)
API_HOSTHost the server binds to
DATABASE_URLPostgreSQL connection string
AUTH_SECRETSecret key for JWT signing (min 32 chars; cannot be the default placeholder in production)
API_CORS_ORIGINAllowed CORS origin (must not be * in production or staging)

Optional variables and group invariants

VariableDescription
TRUST_PROXYSet to true or 1 to extract client IP from X-Forwarded-For (default: false)
AUTH_RATE_LIMIT_WINDOW_MSSliding window in ms (default: 60000)
AUTH_RATE_LIMIT_MAX_REQUESTSMax requests per IP per window (default: 15)
AUTH_RATE_LIMIT_IDENTIFIER_MAX_REQUESTSMax requests per identifier per window (default: same as IP limit)
SUBSCRIPTION_GUARD_ENABLEDSet to false or 0 to disable subscription enforcement (default: true)
RESEND_API_KEYResend API key for transactional email
RESEND_FROM_EMAILSender email address
WEB_BASE_URLDashboard base URL included in outgoing emails
GOOGLE_OIDC_ISSUER_URLGoogle OIDC issuer URL
GOOGLE_OIDC_CLIENT_IDGoogle OIDC client ID
GOOGLE_OIDC_CLIENT_SECRETGoogle OIDC client secret
GOOGLE_OIDC_CALLBACK_URLGoogle OIDC redirect callback URL
STRIPE_SECRET_KEYStripe secret key
STRIPE_WEBHOOK_SECRETStripe webhook signing secret
STRIPE_TRY_ME_PRICE_IDStripe Try Me recurring plan price ID
STRIPE_PRO_PRICE_IDStripe Pro recurring plan price ID
STRIPE_AGENCY_PRICE_IDStripe Agency recurring plan price ID

Group invariants enforced at startup:

  • RESEND_API_KEY, RESEND_FROM_EMAIL, and WEB_BASE_URL must all be set together. In production, all three are required.
  • All four GOOGLE_OIDC_* variables must be configured together. In production they are required.
  • Stripe plan checkout requires the selected plan's recurring Stripe Price ID.
  • STRIPE_WEBHOOK_SECRET is required in production/staging when Stripe is configured.

Integration Testing

The API integration suite lives at apps/api/src/api.integration.test.ts and uses the standardized harness:

import { createDbAuthContext } from '@tx-agent-kit/testkit'

const ctx = createDbAuthContext({
  apiCwd: resolve(dirname(fileURLToPath(import.meta.url)), '..')
})

The harness provides authenticated HTTP helpers and handles DB reset between test cases. Manual process spawning and direct createSqlTestContext wiring are forbidden in the API integration suite.

On this page