Vencura
Architecture

API Architecture

Single-source, type-safe REST APIs: Fastify routes (TypeBox) generate OpenAPI, which generates typed clients and React Query hooks.

We treat Fastify routes + TypeBox schemas as the single source of truth for both:

  • Runtime behavior: request/response validation in Fastify
  • Tooling: OpenAPI spec generation, then typed clients/hooks generation

Data flow: TypeBox (backend) → OpenAPI spec → generated types & Zod schemas → generated clients & React Query hooks.

Principles

  • Single source of truth: schemas live with the route (no parallel hand-written spec)
  • Runtime validation by default: every route defines request/response schemas
  • Generated consumers: clients/hooks are generated from the route-derived OpenAPI spec

Stack

  • Runtime: Node.js LTS — stability, ecosystem, tooling
  • Framework: Fastify — performance, TypeBox, plugin-based, ESM-native
  • Database: PostgreSQL (any host via DATABASE_URL; PGLite for tests with PGLITE=true)
  • ORM: Drizzle — type-safe queries, SQL-like syntax, no code generation
  • Spec: OpenAPI 3 — standard interoperability for docs/tooling/SDKs
  • Client generation: @hey-api/openapi-ts — typed TS client + Zod schemas
  • React integration: @tanstack/react-query hooks generated in @repo/react

How it works

  1. Author routes with TypeBox schemas (in apps/api/src/routes/; Fastify uses them for validation)
  2. Generate OpenAPI from route definitions
  3. Generate SDKs from OpenAPI (TypeScript client + Zod schemas + React Query hooks)

At runtime, the request path is: request → Fastify → schema validation → handler → Drizzle queries → typed JSON response.

Example

Route schema (Fastify)

import { Type } from '@sinclair/typebox'

const HealthResponseSchema = Type.Object({
  ok: Type.Literal(true),
})

fastify.get('/health', {
  schema: {
    response: {
      200: HealthResponseSchema,
    },
  },
}, async (request, reply) => {
  return reply.send({
    ok: true,
  })
})

Generate OpenAPI

The OpenAPI spec is generated from route definitions and written to apps/api/openapi/openapi.json.

pnpm --filter @repo/api generate:openapi

Generate TypeScript client

Hey API generates type-safe clients from the OpenAPI spec (generated output lives in packages/core; do not hand-edit):

// Generated by @hey-api/openapi-ts
import { createClient } from './gen/client'

const client = createClient({
  baseUrl: 'https://api.example.com',
})

Usage (Next.js)

createClient supports three auth modes. See Authentication for full details.

import { createClient } from '@repo/core'
import { useHealthCheck } from '@repo/react'

// No-auth: health check without Bearer
const client = createClient({ baseUrl: process.env.NEXT_PUBLIC_API_URL! })

// JWT mode: token from cookie or session
const client = createClient({
  baseUrl: process.env.NEXT_PUBLIC_API_URL!,
  getAuthToken: async () => session?.token,
  getRefreshToken: async () => session?.refreshToken,
  onTokensRefreshed: async ({ token, refreshToken }) => { /* persist */ },
})

// API key mode: static Bearer for servers/CLIs
const client = createClient({
  baseUrl: process.env.NEXT_PUBLIC_API_URL!,
  apiKey: 'venc_xxx_secret',
})

const health = await client.healthCheck()
const { data } = useHealthCheck()

OpenAPI Integration

  • Generation: apps/api/openapi/openapi.json (generated from routes via generate-openapi.ts)
  • Serving: /reference/openapi.json (programmatic access)
  • Docs UI: /reference (Scalar)
  • AI tooling: MCP integration can consume the spec for agent workflows

API Reference auth

The Scalar UI at /reference supports two sign-in flows. Dynamic Labs (primary): embedded iframe to the web app login; on success the Dynamic JWT is passed via postMessage and applied as Bearer. API key fallback: collapsible "Use API key instead" section for pasting venc_* or Bearer tokens. Requires WEB_APP_URL and DYNAMIC_ENVIRONMENT_ID for the Dynamic flow; when unset, only the paste flow is shown.

SDK Generation

TypeScript is generated by @hey-api/openapi-ts. Other languages can use standard OpenAPI generators (for example openapi-generator, oapi-codegen, progenitor). All SDKs are generated from the same OpenAPI spec produced from routes.

Database

The API layer uses Drizzle for queries and drizzle-kit for migrations. Migration strategy depends on the database runtime; see ADR 008: Database Platform & Strategy.

For local Supabase: pnpm reset from the monorepo root runs Supabase DB reset, Drizzle migrations, and scripts/seed.ts (details in ADR 008).

Custodial Wallets

The /wallets routes implement the Vencura Wallet custodial wallet API: create, list, balance, sign, and send. All operations require Bearer auth (Dynamic JWT or API key). See Custodial Wallet API for endpoint details and Security Model for encrypted keys and rate limiting.

Security

  • Headers: X-Content-Type-Options, X-Frame-Options, CSP, HSTS
  • CORS: configurable origin restrictions
  • Rate limiting: per-IP
  • Validation & observability: schema validation on requests/responses, security events logged, trust proxy for correct client IP behind a reverse proxy

On this page