WordloopWordloop
Guides

Code Generation

AsyncAPI, Orval, and oapi-codegen client generation workflows.

Code Generation

The platform uses code generation pipelines to keep API contracts in sync across all services.

Event types (AsyncAPI)

There is no single asyncapi.yaml. Core owns two AsyncAPI specifications, and they are not interchangeable:

  • services/wordloop-core/asyncapi-ws.yaml — WebSocket events between the browser (App) and Core: recording commands/events, live insight events, and so on. Consumed by the App.
  • services/wordloop-core/asyncapi-pubsub.yaml — Pub/Sub messages between Core and ML (transcription jobs, meeting-terminated). Internal service-to-service contracts, never consumed by the App.
# Compile AsyncAPI specs to typed internal Events for all services
./dev gen events

This produces:

TargetToolSource specOutput
Goasyncapi-codegenbothservices/wordloop-core/internal/provider/generated/{ws,pubsub}/asyncapi-*.gen.go
TypeScript@asyncapi/cli (Modelina)asyncapi-ws.yaml onlyservices/wordloop-app/lib/generated/ (Modelina's per-model .ts files)
Python@asyncapi/cli (Modelina)bothservices/wordloop-ml/src/wordloop/providers/generated/asyncapi_models.py

The App's scripts/generate-events.sh deliberately processes only asyncapi-ws.yaml — Pub/Sub events are Core↔ML only and are never consumed by the App.

:::info Core owns both specs and generates its own types locally. App and ML are consumers that pull the relevant spec(s) from the monorepo path — following the same pattern as OpenAPI client generation. :::

lib/generated/asyncapi.ts is a hand-curated facade, not generated output

services/wordloop-app/lib/generated/asyncapi.ts is not produced by generate-events.sh and must not be assumed to regenerate automatically when events are added to asyncapi-ws.yaml. Modelina's raw output cannot express a discriminated union over CloudEvents' type field: it emits reservedType for the type discriminator, Map<string, any> for additionalProperties, and an AnonymousSchema_NN name for every inline data object — none of which TypeScript can narrow on. asyncapi.ts is a hand-written facade that re-expresses Modelina's per-model files as one discriminated ServerCloudEvent / ClientCloudEvent union.

Whenever a WebSocket event is added, renamed, or its data shape changes in asyncapi-ws.yaml, lib/generated/asyncapi.ts must be updated by hand to match — running generate-events.sh alone does not close this loop. Two things Modelina genuinely cannot express, and which live only in the facade (and in lib/recording/events.ts, which re-exports it): the generic outbound RecordingCloudEvent envelope, and the typed command builders (startRecordingCommand/stopRecordingCommand/resumeRecordingCommand) — AsyncAPI describes each message as one whole schema, so generated models cannot be parameterised over type/data the way a builder function needs.

Core → ML client (oapi-codegen)

wordloop-core generates a Go HTTP client for calling wordloop-ml's API.

# Core must be running at localhost:4002 and ML at localhost:4003
./dev gen clients

Under the hood:

cd services/wordloop-core
WORDLOOP_ML_BASE_URL=http://127.0.0.1:4003 ./scripts/generate-clients.sh

Adding a new external API client in Core:

  1. Create internal/provider/<name>/
  2. Add an oapi-codegen.yaml config in that directory
  3. Set <NAME>_BASE_URL when running the script

ML → Core client (openapi-python-client)

wordloop-ml generates a Python client for calling wordloop-core's API.

# Generated simultaneously alongside Core's
./dev gen clients

Under the hood:

cd services/wordloop-ml
./scripts/generate_wordloop_core_client.sh

The generated client is written to src/wordloop/providers/wordloop_core/client/ and must not be edited manually.

App TypeScript client (Orval)

wordloop-app generates TypeScript types, SWR hooks, and API functions from Core's OpenAPI spec.

# Generated simultaneously via Orval
./dev gen clients

Under the hood:

curl http://localhost:4002/openapi.json -o services/wordloop-app/openapi.json
cd services/wordloop-app && pnpm orval

The generated file is lib/api/generated.ts — never edit it manually. Use the wrapper hooks in hooks/use-data.ts.

App worker bundle (check:workers)

The App ships a hand-authored Web Worker (public/opfs-buffer.worker.ts, the OPFS shadow-buffer worker for live recording) that is bundled separately from the Next.js app via vite.worker.config.mts — Next's own bundler does not produce a worker script the browser can load directly. This is not spec-driven codegen, but it is generated output that must stay in sync with its source the same way:

  • npm run prebuild and pretest run workers:build, which rebuilds public/opfs-buffer.worker.js from source before the app builds or the test suite runs.
  • node scripts/check-workers.mjs rebuilds the bundle into a temp directory with the repo's pinned Vite and diffs it against the committed public/opfs-buffer.worker.js, exiting 1 on any difference. Run it after touching worker source to confirm the committed bundle still matches; CI treats a mismatch as a build failure, not a warning.

Regenerate everything

# All services must be running for clients to pull live specs
./dev gen all

This runs: events → clients → docs.

On this page