Skip to content

Vox–React backend interop audit (2026)

Self-critique note (v2). This is the second pass. The v1 of this doc undersold three problems and missed three artifacts entirely; corrections are listed in §9 Audit self-critique at the end. Use the v2 findings, not the older summary.

The audit is structured to be executable by a less-capable LLM follower:

  • Every claim has at least one file path with line range as evidence.
  • Every milestone has a discrete task list ordered by dependency and gives acceptance commands (mostly cargo test / rg) that confirm completion without ambiguity.
  • Vox-specific policies (no new shell scripts; .vox automation only; secrets via vox-secrets) are honored throughout — see AGENTS.md.

If you are an automation agent: do not start coding from §6 alone. Read §1–§5 first so you understand which doc and which crate own each surface.


Original user intent (2026-05-11):

Audit how Vox as a language interoperates with the React environment, enhancing its capabilities as a backend-only service and API provider for React frontends that exist.

The audit covers three workstreams:

  1. API contract quality — wire format, OpenAPI parity, versioning, error envelope.
  2. Build-target ergonomicsserver and client outputs, plus the supporting vox emit / vox dev surfaces.
  3. Runtime ops hardening — auth, CORS, rate limits, request IDs, observability.

Scope was expanded during v2 to additionally cover:

  • Streaming (SSE / chunked) and file uploads / multipart.
  • Duplicate TS client emission (api.ts vs vox-client.ts) — resolved: only vox-client.ts ships (see §3.2 C4).
  • TanStack Query helper emit alignment.
  • npm-publishable client packaging (per Phase 1 spec).
  • Vox.toml [build] and VOX_BUILD_TARGET env-var wiring.
  • Container output for backend-only mode.
  • Documentation drift between phase-numbering-index.md (“Phases 1–4 complete”) and code reality.

flowchart LR
voxSource[VoxSource] --> hir[HIR]
hir --> contractIr[ContractIR]
hir --> appContract[AppContract]
contractIr --> openapiEmit[OpenAPI emit]
contractIr --> voxClient[vox-client.ts emit]
contractIr --> zodEmit[schemas.ts emit]
hir --> rustHttp[Rust Axum emit]
hir --> tanstackQuery[vox-tanstack-query.tsx emit]
rustHttp --> axumServer[Generated Axum binary]
voxClient --> reactApp[External React app]
openapiEmit --> reactApp
tanstackQuery --> reactApp

Files behind each node:

NodeFileRole
HIRcrates/vox-compiler/src/hir/lower/mod.rsLowers AST and stamps route_path per web_prefixes.rs.
ContractIRcrates/vox-compiler/src/contract_ir/project.rsWire-shape projection (Decimal/BigInt → string, Option → optional).
AppContractcrates/vox-compiler/src/app_contract.rsStable JSON contract; VOX_PORT, VOX_SSR_DEV_URL baked in.
OpenAPI emitcrates/vox-codegen/src/codegen_ts/openapi_emit.rsOpenAPI 3.1; servers[0].url empty string so composed URLs match absolute /api/... paths; components.schemas.ErrorEnvelope + error responses on operations.
vox-client.tscrates/vox-codegen/src/codegen_ts/vox_client.rsTyped client (Zod, VITE_API_URL, optional configureVoxApiBase, VoxApiError with optional wireError / VoxWireError for SSOT §6 bodies).
api.ts (retired)crates/vox-codegen/src/codegen_rust/emit/client.rsLegacy Rust-path emitter; api_client_ts is forced empty in emit/mod.rsno api.ts is written.
Zod / schemas.tscrates/vox-codegen/src/codegen_ts/zod_emit.rsRuntime validators consumed by vox-client.ts.
TanStack Querycrates/vox-codegen/src/codegen_ts/tanstack_query_emit.rsGeneric useVoxServerQuery provider; not per-endpoint.
Rust Axum emitcrates/vox-codegen/src/codegen_rust/emit/http.rsmain.rs + per-endpoint Axum handlers.
HIR HTTP ergonomicscrates/vox-compiler/src/hir/nodes/http_ergonomics.rsHirCorsPolicy, HirRateLimitPolicy defined and tested at language layer.

  • GET vs POST split matches SSOT §2 across emitters and the Axum router (vox_client.rs, emit_query_fn_handler/emit_server_fn_handler in http.rs).
  • Sorted lexicographic query keys + JSON.stringify + encodeURIComponent align between client (voxQueryString) and server decode (Query<BTreeMap<String, String>>).
  • Type projection is single-source. Decimal / BigInt → string, Option → absent key, sum types → _tag are enforced once in contract_ir::project and re-used by Zod, OpenAPI, and the typed client.

3.2 Critical contract bugs (frozen findings — 2026-05-11 audit pass)

Section titled “3.2 Critical contract bugs (frozen findings — 2026-05-11 audit pass)”

The table below records original gaps. Current remediation status is authoritative in §3.2.1 and in code (do not treat line-number citations here as still accurate).

IDIssueOriginal severityResolution / supersession
C1OpenAPI servers.url composed incorrectly with absolute /api/... paths.P0Fixed: servers[0].url = ""; tests in openapi_emit.rs.
C2SSOT examples vs real /api/query/… routes.P0Doc fixed: wire-format-v1-ssot.md §2 / §2.1 aligned with web_prefixes.rs.
C3Error envelope not used consistently in handlers.P0Partial: vox-http-envelope + Axum paths updated; not every branch guaranteed (see §3.2.1).
C4Duplicate api.ts vs vox-client.ts.P1Resolved: Rust codegen sets api_client_ts to empty; no api.ts from generate() (emit/mod.rs). Consumers use vox-client.ts.
C5OpenAPI query param schema vs JSON-in-query encoding.P1Mitigated: per-parameter description + SSOT §2.1 OpenAPI note; composite types still “logical schema after parse”.
C6No wire-format goldens.P2Started: crates/vox-codegen/tests/golden/wire-format/ + wire_format_golden.rs.
C7OpenAPI missing error responses.P1Mitigated: ErrorEnvelope + 400/429/500/default; domain Result errors still not first-class in HIR.

Follow-through landed in-tree for several rows above (re-run cargo test -p vox-codegen openapi_emit and cargo test -p vox-codegen wire_format_golden):

  • C1: OpenAPI servers[0].url = "" — see openapi_emit.rs and openapi_paths_do_not_double_api_segment_when_server_url_empty.
  • C3 / C7: Axum codegen uses vox_http_envelope on multiple handler failures; OpenAPI documents ErrorEnvelope and 400 / 429 / 500 / default responses. Domain Result errors on the wire remain an application/HIR modeling gap.
  • C5: Query parameters include an explicit JSON-after-decode description; SSOT §2.1 references OpenAPI.
  • C6: Fixture crates/vox-codegen/tests/golden/wire-format/error-envelope.example.json and test wire_format_golden.rs (grow this tree per SSOT §8).

CLI: vox emit client, vox dev --target={fullstack,server,client} (compilerd target field), configureVoxApiBase in emitted vox-client.ts. Library mode also emits package.json (library_package_emit.rs).

Section titled “3.3 Recommended wire-format reconciliation”

Status: Option A is applied for OpenAPI (servers[0].url = "") and SSOT §2 path layout (see §3.2.1).

Two valid resolutions for C1/C2 were originally framed as follows (historical):

Option A (preferred): keep code, fix doc. Update wire-format-v1-ssot.md §2 to state that v1 is the implicit version and the canonical layout is /api/query/<name>, /api/mutation/<name>, /api/<name> (server fns). Change OpenAPI emit to servers[0].url = "" (or omit servers entirely, which is OpenAPI-3-valid and falls back to the spec URL).

Option B: change code, keep doc. Insert /v1 into web_prefixes.rs (/api/v1/query/, …); migrate snapshots; provide backward-compat aliases under /api/query/ for one minor.

Either choice requires OpenAPI servers to compose to the same effective URL as the typed client. Add a snapshot test that asserts:

let server = spec["servers"][0]["url"].as_str().unwrap();
let path = spec["paths"].as_object().unwrap().keys().next().unwrap();
let composed = format!("{server}{path}");
assert!(composed.starts_with("/api/")); // and exactly one `/api/` segment
assert!(composed.matches("/api/").count() == 1);

4. Build-target readiness (fullstack | server | client)

Section titled “4. Build-target readiness (fullstack | server | client)”

Reference: phase1-build-targets-spec-2026.md.

CapabilitySpec / docsCode realityVerdict
BuildTarget enumPhase 1 spec §1crates/vox-config/src/config/gamify_web.rs lines 55–93Landed
Vox.toml [build] target = "server"Phase 1 spec §2Test reads_build_target_server_from_vox_toml in config/impl_ops.rs confirms parseLanded
VOX_BUILD_TARGET env varDocumented in gamify_web.rsRead in impl_ops.rs (apply_build_target_env_override); CLI --target still overrides after loadLanded
CLI `—target=fullstackserverclient`Phase 1 spec
vox build codegen branches on targetPhase 1 spec §1commands/build.rs: server → Rust only; client → Library TS (openapi.json, vox-client.ts, package.json, …); fullstack → TS + RustLanded
vox dev --target=…Phase 1 spec §3commands/dev.rs accepts build_target, forwards target on the daemon JSON params; server suppresses browser open by defaultLanded
vox emit clientPhase 1 spec §1.2Cli::Emit + commands/emit.rs — Library TS SDK onlyLanded
vox init --kind=backendPhase 1 spec §1.4Not present in CLI surface.Missing
vox bundle skipping Vite when target=serverImpliedcommands/run.rs resolve_has_frontend honors BuildTarget::Server — works at run-time only.Partial (run only)
Lean Dockerfile for backend-onlyPhase 1 spec §1.5crates/vox-container emits a Rust-first image regardless of target.Out of scope today

phase-numbering-index.md may still read ahead of nuance: core build targets, vox emit client, env VOX_BUILD_TARGET, and vox dev --target are implemented — re-verify that file before treating Phase 1 as incomplete.


ConcernLanguage surfaceCodegen realityNotes
CORSHirCorsPolicy defined in http_ergonomics.rs; attached as HirEndpointFn.cors: Option<HirCorsPolicy> (hir/nodes/decl.rs line 416). HIR lowering populates it (hir/lower/mod.rs line 197).rg CorsLayer|tower_http::cors crates/vox-codegen → 0 hits. Policy is never read by emitters.True positive in v1, sharpened.
Rate limitHirRateLimitPolicy defined; attached as HirEndpointFn.rate_limit line 419.No emission.Same
Auth (@auth(...) / @public)Parsed and validated in vox-compiler/src/typeck.Codegen never emits middleware. No JWT, no session cookie, no header check.
Request ID propagationNoneNone — tracing_subscriber::fmt::init() only (http.rs line 164).New in v2.
Streaming / SSENone in HIR or codegen — rg SSE|EventSource|text/event-stream|axum::response::sse crates/vox-codegen → 0 hitsGenerated handlers always return Json<serde_json::Value>. Long-poll, SSE, chunked — unsupported.New in v2.
File uploads / multipartNonerg multipart crates/vox-codegen → 0 hits.New in v2.
WebSocketsOrchestrator MCP gateway has WS (vox-orchestrator-mcp/src/http_gateway/ws.rs); generated user apps do not.
PaginationConvention-only via @query paramsNone enforced.
Idempotency keysNoneNoneWeb-app archetype coverage doc lists this as a blocker.

The tower-http crate is already used in the workspace (e.g. vox-orchestrator-mcp/src/http_gateway/mod.rs), so emitting CorsLayer/TraceLayer from codegen is a dependency-graph addition, not a new third-party adoption decision.


Severity follows P0 (blocker for an external React+backend team) → P3 (paper cut). Effort is S (≤1 day), M (~1 week), L (>1 week).

IDGapSevEffortOwner crate(s)Evidence
G1OpenAPI servers + paths compose to a duplicate /api/ segmentP0Svox-codegen§3.2 C1
G2SSOT path examples disagree with emitted routesP0Sdocs§3.2 C2
G3Error envelope unimplementedP0Mvox-codegen, vox-actor-runtime§3.2 C3
G4BuildArgs.build_target not threaded into build::run; vox build does not branchP0Mvox-cli, vox-codegenResolved: lanes.rsbuild::run; server/client/fullstack branches in build.rs
G5VOX_BUILD_TARGET env var documented but unreadP1Svox-configResolved: impl_ops.rs (apply_build_target_env_override)
G6vox emit client subcommand does not existP1Mvox-cliResolved: commands/emit.rs
G7Two divergent TS client files (api.ts + vox-client.ts)P1S–Mvox-codegenResolved: §3.2 C4 — only vox-client.ts is emitted
G8CORS/rate-limit lowered into HIR but not emittedP1Mvox-codegen§5 row 1–2
G9vox dev --target=server missingP1Svox-cliResolved: dev.rs
G10OpenAPI omits non-200 responses; no error schemaP1Mvox-codegen, contract IRMitigated: §3.2 C7 — ErrorEnvelope + structured responses
G11OpenAPI parameter encoding lies for composite query typesP1S–Mvox-codegen§3.2 C5
G12No request-id propagation / structured tracing layerP2Mvox-codegen§5 row 4
G13No SSE / streaming / multipart codegenP2Lvox-compiler, vox-codegen§5 rows 5–6
G14Wire-format §8 golden fixtures missingP2Mvox-codegen tests§3.2 C6
G15phase-numbering-index.md overstates Phase 1 completionP2Sdocs§4.2
G16vox init --kind=backend missingP3S–Mvox-cli§4.1 row 8
G17Backend-only Dockerfile laneP3Mvox-container§4.1 row 10

7. Implementation roadmap (step-by-step, agent-followable)

Section titled “7. Implementation roadmap (step-by-step, agent-followable)”

Each milestone lists discrete tasks with a single file as the unit of work wherever possible, plus an acceptance command that returns success/failure unambiguously.

Vox-policy reminders for the implementing agent:

  • Do not add .ps1 / .sh / .py automation; use vox run scripts/<name>.vox. (AGENTS.md §VoxScript-First Glue Code)
  • Do not read secrets via std::env::var; use vox_secrets::resolve_secret(...).
  • Test-first per AGENTS.md §Test-First Policy: write the failing test before adding any new pub fn.
  • Run cargo via the absolute Cargo path on Windows: & "$env:USERPROFILE\.cargo\bin\cargo.exe".

Milestone A — Contract truth (P0; 1 PR; 0.5–1 day)

Section titled “Milestone A — Contract truth (P0; 1 PR; 0.5–1 day)”

Goal: OpenAPI, vox-client.ts, and SSOT all describe the same URL for the same endpoint. Pick Option A (fix doc; minimize code change) unless the team explicitly prefers Option B.

Tasks (Option A):

  1. Edit OpenAPI server URL. In crates/vox-codegen/src/codegen_ts/openapi_emit.rs line 46, change servers to [{ "url": "" }] or remove the key entirely. Update existing test at line 256.
  2. Add a new test openapi_paths_compose_with_one_api_segment in the same file that builds a representative ContractIr, emits, and asserts the composed server + path for each endpoint contains exactly one "/api/" substring.
  3. Update wire-format-v1-ssot.md §2 and §2.1 examples to use the canonical paths emitted by web_prefixes.rs: /api/query/<name>, /api/mutation/<name>, /api/<server>.
  4. Acceptance:
    • cargo test -p vox-codegen openapi_paths_compose_with_one_api_segment
    • cargo test -p vox-codegen --test golden_ts_test (snapshots may need cargo insta accept after review).
    • Manual: npx openapi-typescript dist/openapi.json -o /tmp/api.ts produces a file that calls the same URLs vox-client.ts does.

Milestone B — Error envelope unification (P0; 1 PR; 2–3 days)

Section titled “Milestone B — Error envelope unification (P0; 1 PR; 2–3 days)”

Goal: Every generated handler returns SSOT §6 envelope on failure, on every kind (@query, @mutation, @server), with the same shape.

Tasks:

  1. Add a runtime helper crate vox-http-envelope (or place inside an existing utility crate) that defines:
    pub struct ErrorEnvelope { pub ok: bool /* always false */, pub code: String,
    pub message: String, pub request_id: Option<String>,
    pub details: Option<serde_json::Value> }
    pub fn err_response(status: StatusCode, code: &str, msg: impl Into<String>) -> Response;
    Add a From<E> for any vox-db / runtime error you want to map.
  2. Modify emit_server_fn_handler (http.rs line 362). Replace today’s two endings (Json(serde_json::Value::Null) and Err(e) => Json(serde_json::json!({"error": e.to_string()}))) with calls into the helper.
  3. Modify emit_query_fn_handler (line 419) similarly: when query parse fails or handler body returns Err, map to envelope. Currently the function silently coerces missing params to Value::Null; add a structured 400 instead with code = "BAD_REQUEST" and details = { param: "<name>" }.
  4. Modify emit_route_handler (line 337) for raw http routes — same envelope.
  5. Add an OpenAPI default response in openapi_emit.rs emit_operation so each operation declares default: ErrorEnvelope (and define the schema once in components.schemas.ErrorEnvelope).
  6. Acceptance:
    • New integration test under crates/vox-integration-tests/tests/error_envelope.rs that boots the emitted Axum app for a fixture .vox file, sends a malformed POST, asserts {ok:false, code:"BAD_REQUEST"}.
    • rg "ok\":\\s*false" crates/vox-codegen → ≥1 hit in http.rs`.

Milestone C — Build-target wire-up (P0/P1; 1 PR; 3–5 days)

Section titled “Milestone C — Build-target wire-up (P0/P1; 1 PR; 3–5 days)”

Goal: vox build --target=server skips TS codegen; --target=client skips Rust codegen; --target=fullstack is unchanged. Vox.toml [build] and VOX_BUILD_TARGET are honored.

Tasks:

  1. Read VOX_BUILD_TARGET. Extend crates/vox-config/src/config/impl_ops.rs (the build_target merge sits at line 226) so that after the Vox.toml merge, an environment override is applied. Use the existing vox_config::env_parse::resolve_config_str("VOX_BUILD_TARGET", "") helper from env_parse.rs and parse via BuildTarget::from_str (gamify_web.rs line 71). Keep precedence per gamify_web.rs line 57: CLI flag > env > Vox.toml > default.
    • Test in impl_ops.rs: set env var via std::env::set_var inside a tempdir test, assert override beats Vox.toml. Use a serial-test guard if other tests in the file mutate process env.
  2. Thread BuildArgs.build_target into commands::build::run. Two changes:
    • Rename commands/build.rs::run parameter targetmobile_target to remove the long-standing naming confusion.
    • Add a new parameter build_target: Option<vox_config::BuildTarget> (defaulting to None); when None, fall back to VoxConfig::load().build_target.
    • Update all 8 callers (cli_dispatch/lanes.rs:135, compilerd.rs:203,346,392,437, commands/run.rs:130, commands/test.rs:22, commands/bundle.rs:94).
  3. Branch on build_target in commands/build.rs:
    match resolved_target {
    Server => only run codegen_rust + write Rust files; skip TS files; skip api.ts.
    Client => only run codegen_ts in Library mode + write TS bundle to out_dir; skip Rust write.
    Fullstack => current behavior.
    }
  4. Update commands/run.rs so BuildTarget::Client short-circuits with a friendly error: “client target produces a TS package; use npm publish to ship it”.
  5. Acceptance:
    • cargo test -p vox-cli build_target (new tests).
    • cargo run -p vox-cli -- build examples/golden/crud_api.vox --target=server -o /tmp/server-only produces only files under /tmp/server-only from Rust path; find /tmp/server-only -name "*.tsx" → empty.
    • Same with --target=client → no target/generated/Cargo.toml.
  6. Docs: add a row to docs/src/reference/cli.md and follow the .cursor/rules/cli-command-registry rule (vox ci operations-sync --target cli --write).

Milestone D — vox emit client subcommand (P1; 1 PR; 3–7 days)

Section titled “Milestone D — vox emit client subcommand (P1; 1 PR; 3–7 days)”

Goal: Produce an npm-publishable client package independent of the full-stack build.

Tasks:

  1. Add Cli::Emit { kind: EmitKind, lang: EmitLang, out: PathBuf, source: PathBuf } in crates/vox-cli/src/main.rs (mirror existing Cli::* enum style) and EmitKind = Client (extensible) / EmitLang = Ts (extensible).
  2. New module crates/vox-cli/src/commands/emit.rs that reads source, drives the compiler frontend through pipeline::run_frontend, then calls a new emitter that produces a directory with:
    • package.json (name from Vox.toml [package].name + "-client", version from workspace, exports map for ESM/CJS/.d.ts).
    • src/index.ts re-exporting vox-client.ts body but with BASE taken from constructor parameter, not import.meta.env.
    • src/schemas.ts, src/types.ts.
    • tsconfig.json (consumed only at publish-time; library-mode emit).
    • README.md with one usage snippet.
  3. Reproducibility: the subcommand MUST be byte-deterministic for identical input HIR. Add a snapshot test under crates/vox-codegen/tests/ (or a new vox-codegen-client crate when LoC justifies extraction; layer rules in docs/src/architecture/layers.toml).
  4. Acceptance:
    • cargo run -p vox-cli -- emit client --source examples/golden/crud_api.vox --out /tmp/api-client succeeds.
    • cd /tmp/api-client && pnpm install && pnpm tsc --noEmit succeeds.
    • Running twice yields byte-identical files (diff -r run1 run2 empty).

Milestone E — Lower CORS / rate-limit / auth into Axum (P1; 1 PR; 5–10 days)

Section titled “Milestone E — Lower CORS / rate-limit / auth into Axum (P1; 1 PR; 5–10 days)”

Goal: Decorators a Vox author already writes are honored at runtime by the generated Axum binary.

Tasks:

  1. CORS: in http.rs, iterate module.endpoint_fns and gather distinct cors policies. If any present:
    • Add tower-http = { version = "0.5", features = ["cors", "trace"] } to the generated Cargo.toml (emit/mod.rs emit_cargo_toml).
    • Emit per-route .layer(CorsLayer::new().allow_origin(...).allow_credentials(...)) from the policy.
  2. Rate limit: emit tower::limit::RateLimitLayer (or tower_governor if included) per-route using HirRateLimitPolicy::{window_secs, max_requests, by}. Decline silently for RateLimitBy::ApiKey until header extraction is wired.
  3. Auth: generate a marker middleware that reads the Authorization: Bearer header for endpoints without @public; reject 401 via the §6 error envelope. JWT verification is out of scope here — leave a comment describing the contract for Phase 3 spec.
  4. Acceptance:
    • Snapshot test for a fixture .vox file with @cors(origins=["https://app.example"]) produces CorsLayer::new()...allow_origin(["https://app.example"]) in main.rs.
    • Integration test boots the binary and curl -i -X OPTIONS -H 'Origin: https://app.example' returns access-control-allow-origin.

Milestone F — Collapse the duplicate TS client (P1; 1 PR; 1 day)

Section titled “Milestone F — Collapse the duplicate TS client (P1; 1 PR; 1 day)”

Status: Done (2026-05-11 follow-up): Rust codegen keeps api_client_ts empty; docs point consumers at vox-client.ts (vox-fullstack-artifacts.md); emitted client parses SSOT §6 errors into VoxWireError / VoxApiError.wireError.

Original tasks (historical):

  1. Stop emitting api.ts — enforced via empty api_client_ts in emit/mod.rs.
  2. Migrate import … from "./api" — verify with rg before any golden deletes.
  3. Docs — consolidation called out under Legacy / canonical vox-client.ts in vox-fullstack-artifacts.md.
  4. Acceptance: rg "fn emit_api_client" crates/ → 0; cargo test -p vox-codegen, -p vox-integration-tests green after snapshot updates.

Milestone G — vox dev --target=server (P1; 1 PR; 1–2 days)

Section titled “Milestone G — vox dev --target=server (P1; 1 PR; 1–2 days)”

Status: CLI landeddev.rs forwards target to vox-compilerd and suppresses browser open when server. End-to-end “no Node/Vite” behavior depends on daemon implementation; treat remaining gaps there as a separate verification item.

Milestone H — Streaming / SSE / multipart (P2; multi-PR; >2 weeks)

Section titled “Milestone H — Streaming / SSE / multipart (P2; multi-PR; >2 weeks)”

This is language work (decorators / return types) before codegen, so plan as its own initiative:

  • Add return type Stream<T> to HIR; lower to axum::response::sse::Sse for @endpoint(kind: server) returning Stream<…>.
  • Add @multipart decorator on mutation params; lower to axum::extract::Multipart.
  • Update Contract IR + OpenAPI to express both. Update vox-client.ts to expose a typed EventSource/fetch reader.

Defer until Milestones A–G land.

Milestone I — Observability and request IDs (P2; 1 PR; 2–3 days)

Section titled “Milestone I — Observability and request IDs (P2; 1 PR; 2–3 days)”

Tasks:

  1. Emit tower_http::trace::TraceLayer::new_for_http() and tower_http::request_id::SetRequestIdLayer::x_request_id(MakeRequestUuid) in main.rs.
  2. Thread the request id into the §6 error envelope request_id field via an Axum extractor.

Milestone J — Wire-format goldens + index correction (P2; 1 PR; 1–2 days)

Section titled “Milestone J — Wire-format goldens + index correction (P2; 1 PR; 1–2 days)”

Tasks:

  1. Create tests/golden/wire-format/ with at least: a query with composite query params, a mutation with Decimal body, an error-envelope response.
  2. Add a cargo test --test wire_format_goldens --check flag matching SSOT §8.
  3. Update phase-numbering-index.md so the Phase 1 status reflects what is actually merged after Milestone C.

8. React integration playbook (today, before milestones land)

Section titled “8. React integration playbook (today, before milestones land)”

Practical recipes for a React team that can’t wait:

NeedToday’s path
Typed fetchUse vox-client.ts + VITE_API_URL pointing at the Axum origin. Ignore api.ts.
OpenAPI codegen (Orval / openapi-typescript)After vox build, post-process dist/openapi.json to set servers[0].url = "", then run codegen. Or wait for Milestone A.
TanStack QueryWrap each vox-client call in useVoxServerQuery(['name', ...args], () => name(...args)) from generated vox-tanstack-query.tsx.
AuthTerminate at a reverse proxy (Caddy/Nginx). Verify Bearer tokens upstream; pass-through to Axum.
CORSSame — proxy adds the headers until Milestone E lands.
ErrorsPrefer VoxApiError.wireError when present (parsed §6 JSON). Fall back to responseText for non-envelope bodies.
StreamingNot supported — fall back to polling @query.

What the v1 of this doc got wrong or missed:

  • Missed entirely: the second client emitter crates/vox-codegen/src/codegen_rust/emit/client.rs that produces api.ts separate from vox-client.ts (G7 / C4). v1’s React playbook was therefore incomplete — a React team following v1 might import the wrong file.
  • Understated: v1 framed the OpenAPI server-URL bug as “needs reconciliation”; it is actually a broken composition (duplicate /api/ segment) that breaks every standard OpenAPI client. Sharpened to C1.
  • Understated: v1 said CORS/auth “not emitted”; v2 adds the precise observation that HIR fields exist and are populated at lowering time (HirEndpointFn.cors, .rate_limit) — making this a pure codegen-side gap, much smaller than v1 implied.
  • False positive risk: v1 implied BuildTarget was paper-only; v2 confirms Vox.toml [build] target IS read (test at impl_ops.rs:499). The real bug is narrower: env var unwired and CLI flag dropped.
  • Newly added: SSE / streaming / multipart / request-id / pagination / idempotency — all P1–P2 gaps for a “real” React backend.
  • Newly added: phase-numbering-index.md doc drift (G15).
  • Sharpened verifications: every gap now has a rg/cargo test acceptance command, not just a prose claim.