External Frontend Interop Plan (2026)
External Frontend Interop Plan (2026)
Section titled “External Frontend Interop Plan (2026)”Phase numbering: This plan uses the frontend interop phase sequence (Phases 1–5). For the other two sequences, see phase-numbering-index.
Premise
Section titled “Premise”Historically Vox supported one shape: full-stack co-generation of a Vite/React frontend and an Axum backend from the same .vox source, with @island as the bridge primitive for sprinkling React into the generated tree. As of 2026-05-03, @island is retired: the compiler, CLI, templates, contracts, examples, and docs no longer reference it; Vox lowers component declarations directly to plain React/TSX that any external frontend imports.
This plan expands the model in two directions without removing what works:
- Vox’s GUI authoring stays a first-class language feature. The
componentkeyword and TS/React emission remain. Vox is still capable of being the whole stack. - Bidirectional Vox↔React component interop becomes first-class. A Vox component can import and render a React component from a
.tsxfile; an emitted Vox component is a normal React component that any external React app can import.@islandis retired because proper component-level interop subsumes it. - Backend-only mode is added. A user with an existing React/TS frontend can use Vox purely as the API server, consuming a typed client SDK and standards-based schemas (OpenAPI / JSON Schema), with no Vite or React generation involved.
The two modes share one substrate: the wire-format SSOT, the OpenAPI/JSON Schema emitters, the auth/ops stdlib, and the Axum backend. The full-stack mode adds the GUI emission and component interop on top.
Non-goals
Section titled “Non-goals”- Removing the integrated frontend pipeline. It stays.
- Inventing a new client framework. We integrate with what React users already use (openapi-typescript, RTK Query, TanStack Query, Orval, tRPC adapters).
- A generalized Node↔Vox FFI. The optional WASM-from-Node bridge is scoped to pure Vox computations, not request handling.
Decisions baked into this plan
Section titled “Decisions baked into this plan”- Retire
@island. Replaced by general bidirectional component interop in Phase 5. No need for an island-specific bridge once Vox components and React components can reference each other directly. - Keep
component,routes, and the Vox→TS/React emission. These are language features. - Backend-only mode is additive. Adding
--server-onlydoes not require deleting--full-stack.
Phase 1 — Add backend-only mode; split emit targets
Section titled “Phase 1 — Add backend-only mode; split emit targets”Goal: Make vox build honor an explicit emit target so backend-only and full-stack are both first-class. Add a publishable TS client SDK as a standalone artifact. The full-stack pipeline keeps working unchanged.
Scope:
- New explicit target flag:
vox build --target=server | fullstack | client. Default chosen by the project manifest (Vox.toml [build] target = "..."); no silent behavioral change.--target=serverskips Vite, skipspnpm install, skips React asset generation. Emits the Axum crate only.--target=fullstackis the current behavior, preserved.--target=clientemits the TS client SDK only (see below).
- New subcommand
vox emit client --lang=ts --out=./api-clientproduces a self-contained, npm-publishable package:- Own
package.jsonwithname,version,exports(ESM + CJS +.d.ts). - Zero imports from
vox-actor-runtime, internal Vox surfaces, or the full-stack client emit. Lives in its own crate so the dependency graph is clean. - Emits: types, optional Zod validators (flag), a fetch client class. Configurable
baseUrland a pluggablefetch(so users can wire RTK Query / TanStack Query / their own auth interceptor). - Reproducible output: identical input HIR → byte-identical files. Golden tests pin this.
- Own
vox dev --target=server— dev-loop that doesn’t touch a frontend. Hot-reloads the Axum binary.- Project bootstrap:
vox init --kind=backendproduces a manifest withtarget = "server"and no React/Vite scaffolding. Existing--kind=applicationstill produces a full-stack project. - Update Dockerfile and
vox deployto honor the manifest target — server-only deployments produce a leaner image with no Node/pnpm layer. - Add backend-only golden examples. The existing full-stack goldens (blog_fullstack.vox, dashboard_ui.vox) stay; new ones (
backend_only_crud.vox,backend_only_auth.vox) demonstrate the server target.
Deliverables: target-flag plumbing, vox emit client subcommand, vox dev --target=server, new init kind, manifest schema update, backend-only goldens, CLI reference updates.
Risks: Two emit paths drift over time (full-stack client vs. standalone SDK emit produce different shapes). Mitigation: both consume the same OpenAPI artifact from Phase 2; the full-stack client becomes a thin specialization once Phase 2 lands.
Phase 2 — Wire format SSOT and standards-based schema emit
Section titled “Phase 2 — Wire format SSOT and standards-based schema emit”Goal: Make the contract between Vox backends and external frontends explicit, versioned, and consumable by every TS/React tool that exists.
Scope:
- Wire-format SSOT doc at
docs/src/architecture/wire-format-v1-ssot.md. Pin:Decimal→ string (already in code; codify).BigInt→ string. Decision rationale: JSON Number loses precision past 2^53.- Date/Time → RFC 3339 strings (UTC). No raw epoch ints.
Option<T>→ presence-or-absent JSON key (notnull), with explicit override decorator for null-distinguished APIs.- Sum types →
_tag-discriminated objects (already in code; codify and freeze the tag-key name). - Versioning:
wire-format-v1, semver discipline, breaking-change protocol.
- OpenAPI 3.1 emit:
vox emit openapi --out=./openapi.yamlover theHirEndpointFnset. Path, method, params, request/response schemas, error envelopes. This single artifact unlocks openapi-typescript, Orval, RTK Query, Postman, Insomnia, and more. - JSON Schema 2020-12 emit per Vox
type. Useful in isolation for validation pipelines that don’t want OpenAPI’s full surface. - Golden compatibility tests: a directory of fixture
.voxtypes and the exact expected JSON wire bytes. Any future change to the wire format must update goldens explicitly. Sits alongside the existing Zod/TS goldens. - Deprecate the bespoke
vox_client.rsemit path in favor of routing all TS-client generation through the OpenAPI artifact (Phase 1 client emit becomes a thin wrapper invoking openapi-typescript-codegen internally, or vendoring its templates).
Deliverables: SSOT doc, two new emit subcommands, golden suite, deprecation note for the legacy client emitter.
Phase 3 — HTTP ergonomics as decorators
Section titled “Phase 3 — HTTP ergonomics as decorators”Goal: Express the things real backends need (explicit routes, methods, CORS, auth, rate-limits, path params) without inventing new bare keywords. Per AGENTS.md §Grammar Unification, new behavior goes on decorators.
Scope:
- Extend
@endpoint:Path params extracted by name, type-checked against the function signature at compile time. Query strings remain implicit for trailing scalar params (or explicit via@endpoint(method: GET, path: "/users/:id")fn get_user(id: UserId) to Result[User]@query_param). - New decorators:
@cors(origins: ["https://app.example.com"], credentials: true)— module-scoped or per-endpoint.@auth(scheme: bearer)— declarative; resolves to a Tower middleware in the generated Axum crate. Composable with custom auth functions.@rate_limit(per: "1m", max: 60, key: by_ip)— emits atower_governor(or equivalent) layer.@public/@authenticated/@role("admin")— guard groups.
- Compile-time route conflict detection (already partially present in
routes.rsnear line 70; extend to handle path-param overlaps). - OpenAPI emit (Phase 2) reflects all of the above as
securitySchemes,x-rate-limit, CORS notes, and proper path-paramparameters. - Update auth-pattern golden examples to use the declarative form; keep one manual example as an escape hatch.
Deliverables: Decorator additions to compiler, middleware emission in generated Axum crate, doc page on HTTP ergonomics, OpenAPI integration.
Phase 4 — Auth library and operational primitives
Section titled “Phase 4 — Auth library and operational primitives”Goal: Make a “real production backend” achievable in Vox without leaving the standard library.
Scope:
- JWT verification primitive in
vox-stdlib, with key resolution through Clavis (AGENTS.md §Secret Management). RS256/ES256/HS256 supported; JWKS fetch with caching. - Session store abstraction over
@table. Default schema,verify_token() -> Result[Session],revoke(), idle and absolute timeouts. - Health and observability endpoints:
/healthz,/readyz— auto-mounted, opt-out via flag./metrics— Prometheus text format, opt-in.- Structured logging (JSON) with request-id, span context. Opt-in via
@traceon endpoint or module.
- Durability resolution (audit complete — see durability-runtime-audit-2026.md):
@scheduledand@durable: Remove from the public grammar in the next release; retain as reserved identifiers. Re-introduce each when a real runtime implementation lands. The HIRschedule_intervalandDurabilityKindfields stay as internal metadata.actorkeyword: Retain. Handler-splitting HIR work is real. Document the current limitation (no auto-mailbox wiring) explicitly and provide a manual pattern golden example.workflow/activitykeywords: Remove from the public grammar alongside@durable. They currently compile identically tofnwith no semantic difference.- Decision record required: one ADR covering the deprecation cycle and the re-introduction criteria for each feature.
- CORS / rate-limit defaults chosen to be safe for backend-only deployments (CORS off by default; must be opted in per origin).
Deliverables: stdlib auth module, session table/library, ops endpoint mounting, durability ADR (removes @scheduled/@durable/workflow/activity from public grammar), actor limitation doc + golden example.
Phase 5 — Bidirectional Vox↔React component interop (@island retired 2026-05-03)
Section titled “Phase 5 — Bidirectional Vox↔React component interop (@island retired 2026-05-03)”Status update (2026-05-03):
@islandis retired across the workspace. The remaining Phase 5 work is the bidirectional import bridge: Vox-sideimport_reactfor consuming React components, and emitted-component packaging so Vox components are first-class npm-importable React components.
Goal: Make the Vox GUI language and the React ecosystem into peer citizens. A Vox component can use any React component; an emitted Vox component is a normal React component any external React app can use.
Scope:
- Vox imports React (Vox source uses React components):
- New import form:
import react MyButton from "../ui/MyButton.tsx"(or equivalent — exact syntax in this phase’s sub-spec). - Type bridge: import the component’s TS prop types via either (a) reading the
.tsx/.d.tsdirectly, (b) a generated.vox.types.jsonsidecar, or (c) avox import-typesstep that produces a Vox type alias. Prefer (a) when feasible. - In the emitted TS output, the import passes through unchanged — no marshaling layer, the React component is rendered as-is.
- Props passed from Vox to the React component are serialized through the wire-format SSOT (Phase 2) when they cross a serialization boundary; in-process they pass as native JS values.
- New import form:
- React imports Vox (external React app uses emitted Vox components):
- Vox component compilation produces
.tsxfiles that are first-class React components: realexport, real prop type aliases, no hidden runtime dependency onvox-actor-runtimefor component code. - Output directory has a generated
package.jsonso it can be a workspace package (or published to npm) and consumed via standardimport { MyVoxComponent } from "@myorg/vox-ui". - Re-emit-stable: re-running the compiler produces a clean diff. Developers should generally not hand-edit emitted files (the source of truth is the
.voxsource); a designated// vox:user-editzone or sidecar override file is the escape hatch — exact mechanism in the sub-spec. - Emitted components are typed against the Phase-1 client SDK, so a button bound to a mutation gets the typed call for free.
- Vox component compilation produces
- Retire
@island:- The decorator and its codegen path are deleted from the compiler. Document in the migration ADR.
- All island use sites in goldens/examples are rewritten to use the new bidirectional interop (typically: a Vox component that imports a React component, or vice versa).
- The
routesblock stays — it remains the way Vox authors a route tree. componentkeyword stays — it remains the Vox UI authoring primitive.
- Optional WASM-from-Node bridge (lower priority; can defer to 5b):
- npm package
@vox/wasi-runtimethat loads a Vox-compiled.wasm(the existing--isolation wasmartifact, seewasi.rs) and exposes typed exported functions to Node. - Use case: Node worker calling pure Vox computations in-process. Not for HTTP request handling — that path stays Axum.
- Defer N-API/cdylib indefinitely unless concrete pull emerges.
- npm package
- Tutorials in
docs/src/tutorials/:- “Use a React component from Vox” — bidirectional interop, Vox-side.
- “Use a Vox component from a React app” — bidirectional interop, React-side.
- “Bring your own React frontend” — end-to-end backend-only path:
vox init --kind=backend, write endpoints,vox emit openapi, consume from Vite/Next/Remix, deploy viavox deploy.
Deliverables: import-react syntax + type bridge, emitted-component packaging, @island removal commit + ADR, three tutorials, optional WASM-from-Node package, sub-spec for the import-types and re-emit-stability mechanisms.
Cross-cutting concerns
Section titled “Cross-cutting concerns”- Versioning:
wire-format-v1is independent of Vox compiler version. Breaking changes require a new major (v2) and a parallel-emit grace period. - Documentation governance: all five phases produce docs that go through Documentation governance — auto-indexed via
vox-doc-pipeline, never hand-edited indexes. - Telemetry: every new emit subcommand emits
vox.script.*-class events for observability (AGENTS.md §VoxScript-First Glue Code). - Security: auth and CORS defaults must fail closed. CORS must reject by default;
@authmust reject by default; rate-limit decorators must be additive, not subtractive. - Migration support: ship
vox migrate drop-island(Phase 5) — rewrites@islanduse sites to the bidirectional import form — andvox migrate wire-format(Phase 2 → future v2) so users are never stranded. - Emitted component code is generated, not authored. Per the project’s “auto-generated docs” policy, emitted
.tsxfiles should not be hand-edited; the.voxsource is canonical. Any escape-hatch user-edit zones must be explicitly delimited so the compiler can preserve them across re-emits.
Sequencing and dependencies
Section titled “Sequencing and dependencies”Phase 1 ──► Phase 2 ──► Phase 3 ──► Phase 4 │ └────────────────────► Phase 5Phase 1 unblocks everything because the --target=server split is a prerequisite for offering backend-only at all. Phase 2 must precede Phase 3 because route decorators are only useful if their semantics show up in the OpenAPI artifact. Phase 5 depends on Phase 2 (typed prop bridges and the wire-format SSOT for cross-component data) but is otherwise orthogonal to Phases 3 and 4 and can run in parallel.
What this plan does not yet decide
Section titled “What this plan does not yet decide”- The exact syntax for
import react ...and the type-bridge mechanism for React component props (Phase 5 sub-spec). - The escape-hatch mechanism for user edits in emitted
.tsxfiles (Phase 5 sub-spec; default stance is “don’t hand-edit”). - Specific OpenAPI tooling: vendored templates vs. shelling out to
openapi-typescript-codegen(Phase 2 implementation choice). - The durability runtime’s true status —
@scheduled/@durable/actor/workflow/activity wiring (Phase 4 audit will produce the answer).
Each of these is flagged in its phase as an explicit follow-up.