Today Platform Web — Dev Docs
Architecture

API codegen

TypeScript clients are generated from the backend's OpenAPI spec via `@hey-api/openapi-ts`. `pnpm api:generate` writes to `apps/web/src/lib/api/generated/` — never edit those files by hand.

The cloud backend exposes an OpenAPI spec at https://api.todayai.dev/doc. The web client side consumes it through a code generator: TypeScript types, request/response Zod schemas, and a fetch-based SDK, all generated and committed.

The script

pnpm api:generate         # writes apps/web/src/lib/api/generated/

Behind the scenes (in apps/web/openapi-ts.config.ts):

import { defineConfig } from '@hey-api/openapi-ts'

export default defineConfig({
  input: 'https://api.todayai.dev/doc',
  output: {
    path: 'src/lib/api/generated',
  },
  plugins: [
    '@hey-api/typescript',
    {
      name: '@hey-api/client-fetch',
      runtimeConfigPath: '../client-config.ts',
    },
    {
      name: 'zod',
      definitions: true,
      requests: true,
      responses: true,
    },
    '@hey-api/sdk',
  ],
})

What gets generated

PluginOutput
@hey-api/typescripttypes.gen.ts — TS types for every schema in the OpenAPI spec
@hey-api/client-fetchclient.gen.ts — fetch-based client wired to client-config.ts
zodzod.gen.ts — Zod schemas matching requests + responses + definitions
@hey-api/sdksdk.gen.ts — typed SDK methods, one per OpenAPI operation

The output lives under apps/web/src/lib/api/generated/ and is committed. Never edit these files by hand — they're regenerated on every pnpm api:generate run, and your changes will be silently overwritten on the next regeneration. oxlint.config.ts includes **/lib/api/generated/ in its ignore patterns so we don't fight the generator over style.

The runtime config bridge

The client-fetch plugin's runtimeConfigPath: '../client-config.ts' points at a hand-written file at apps/web/src/lib/api/client-config.ts. That file is not generated — it's the seam where the generated client gets wired into the app's auth + headers + base URL:

// apps/web/src/lib/api/client-config.ts (sketch)
import { client } from './generated/client.gen'
import { CLIENT_PLATFORM_HEADER_VALUE, APP_VERSION_HEADER_VALUE } from './platform-constants'

client.setConfig({
  baseURL: resolveApiBaseUrl(),
  headers: {
    'X-Client-Platform': CLIENT_PLATFORM_HEADER_VALUE,
    'X-App-Version': APP_VERSION_HEADER_VALUE,
  },
})

Hand-written + regeneration-safe = the split that makes this codegen flow viable. The generated code never embeds the base URL or auth-relevant headers; those are wired through the runtime config.

When to regenerate

TriggerAction
Backend ships a new OpenAPI operationpnpm api:generate + commit
Backend tightens an existing schemapnpm api:generate + fix any consumers that broke
Local dev wants to point at a different spec sourceEdit input: in openapi-ts.config.ts (don't commit), regenerate, then revert

What about apps/admin?

apps/admin does not consume the same generated client. It has its own admin-specific API endpoints and codegen flow (under apps/admin/src/lib/api/). The principle is identical; the spec source and output path differ.

MSW + generated types

The MSW handlers and fixtures under apps/web/src/lib/msw/ import the generated types so mock fixtures stay shaped like real API responses. The fixtures-contract.test.ts test runs the generated Zod schemas against every fixture — when the schema changes after a regeneration, the test points at the field that diverges. This is the safety net that keeps mock-mode honest.

See MSW mocks for the mock layer's role in the architecture and Toolchain → Testing → MSW for the implementation layout.

Common issues

  • pnpm api:generate fetches nothing. The OpenAPI URL is the preview-environment spec (api.todayai.dev/doc). If that environment is down or behind protection, regeneration fails. Use a local mirror or point input: at a saved spec file temporarily.
  • Generated types disagree with what the backend actually sends. This happens when the spec is out of sync with the deployed backend — the deployed code is ahead of (or behind) the spec generator. Reach out to the cloud team to confirm what's authoritative.
  • fixtures-contract.test.ts fails after regeneration. A fixture shape drifted from the new schema. Update the fixture to match — that's the contract test working as intended.

On this page