-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Summary
Extend the #[webtau::command] macro system so that the same Rust game logic can be compiled and deployed to multiple execution backends — not just Tauri (desktop) and WASM (browser), but also:
- Axum — standalone HTTP API server
- SpacetimeDB — server-authoritative reducers (SpacetimeDB executes Rust directly)
- Future targets — Cloudflare Workers, headless CLI, etc.
The user writes their game logic once. A project-level target configuration controls which backends the macro generates code for.
Why this matters
Today #[webtau::command] generates exactly two cfg branches:
// crates/webtau-macros/src/lib.rs — generate_all()
let inner = generate_inner(def); // __webtau_<name> — pure logic
let native = generate_native(def); // #[cfg(not(wasm32))] → #[tauri::command]
let wasm = generate_wasm(def); // #[cfg(wasm32)] → #[wasm_bindgen]This is perfect for "Tauri or browser?" but the real question games need answered is broader:
- Main thread or worker?
- Immediate command or async job?
- Local-only or remote-capable?
- Client authority or server authority?
Real-world production games built on gametau have already demonstrated this need concretely. A simulation-heavy game might need:
- An authority service (standalone Axum) for heavy computation like route solving or physics
- A game-state authority (SpacetimeDB) for economy, fleet, contracts, multiplayer
- Local client (Tauri + WASM) for rendering and immediate interactions
All three consume the same core/ crate logic. The difference is the execution wrapper. That wrapping is exactly what the macro already does for two targets — this proposal extends it to more.
What the DX should look like
The command stays the same
#[webtau::command]
fn compute_routes(state: &GameState, origin: BodyId, dest: BodyId) -> Vec<RouteOption> {
// pure game logic — no framework dependencies
state.route_solver.find_routes(origin, dest)
}Project configuration selects targets
# Cargo.toml or gametau.toml
[webtau.targets]
tauri = true # desktop — #[tauri::command] wrapper
wasm = true # browser — #[wasm_bindgen] wrapper
axum = true # standalone API — axum handler generation
spacetimedb = false # SpacetimeDB reducer — opt-inThe macro generates backend-specific wrappers
For Axum (behind cfg(feature = "webtau_axum")):
// Generated: axum handler with JSON extract + shared state
async fn compute_routes(
State(state): State<Arc<RwLock<GameState>>>,
Json(args): Json<ComputeRoutesArgs>,
) -> Result<Json<Vec<RouteOption>>, AppError> {
let guard = state.read().await;
Ok(Json(__webtau_compute_routes(&guard, args.origin, args.dest)))
}For SpacetimeDB (behind cfg(feature = "webtau_spacetimedb")):
// Generated: SpacetimeDB reducer
#[spacetimedb::reducer]
fn compute_routes(ctx: &ReducerContext, origin: BodyId, dest: BodyId) -> Vec<RouteOption> {
let state = ctx.db.game_state().find(/* ... */);
__webtau_compute_routes(&state, origin, dest)
}The inner function is always the same
The existing __webtau_<name> pattern already isolates pure logic from framework glue. Each new backend is just another generate_* function in the proc macro that wraps the same inner function differently.
Architecture
The macro already has the right shape for this:
┌─── generate_native() → #[tauri::command] ← exists
│
generate_all() ────├─── generate_wasm() → #[wasm_bindgen] ← exists
│
(inner fn) ├─── generate_axum() → axum handler ← NEW
│
└─── generate_stdb() → #[spacetimedb::reducer] ← NEW
Each generator is gated on a cargo feature flag. If the feature is not enabled, no code is emitted.
What needs to change in the macro crate
- Feature-gated generators —
generate_axum(),generate_stdb(), etc. - State access pattern per backend — each backend has its own state extraction idiom
- Serialization boundary per backend — Axum uses
serde_json, SpacetimeDB has its own serde, WASM usesserde_wasm_bindgen - Async support — Axum handlers are async; the inner function can stay sync with
spawn_blockingor the macro can generate the async boundary
What needs to change in the webtau crate
- Optional dependencies behind feature flags:
axum,spacetimedb,tokio - State management helpers per backend (like
wasm_state!but for Axum AppState and SpacetimeDB ReducerContext) - Manifest generation (Macros/tooling: emit command metadata and optional typed clients from webtau commands #139) — machine-readable API surface for typed client generation
What needs to change on the TypeScript side
- Provider adapters that can talk to Axum HTTP endpoints instead of (or alongside) Tauri IPC and WASM direct calls
- Runtime configuration so the same frontend can target local WASM, local Tauri, or remote HTTP depending on environment
Relationship to the API-kind split (#156)
Issue #156 proposes separating command/query/job/authority as distinct API kinds. That work is complementary:
- This issue adds new compilation targets (where does the code run?)
- API design: separate command, query, job, and authority semantics #156 adds new semantic kinds (what is the code doing?)
Together they form a matrix:
| Tauri | WASM | Axum | SpacetimeDB | |
|---|---|---|---|---|
| command (local mutation) | yes | yes | yes | yes |
| query (read-only) | yes | yes | yes | yes |
| job (async heavy work) | — | worker | yes | yes |
| authority (shared state) | — | — | yes | yes |
A command might target all four backends. An authority probably only targets Axum or SpacetimeDB. The kind informs which targets make sense.
Implementation phases
Phase 1: Axum backend
- Add
generate_axum()to the proc macro - Feature-gated behind
webtau_axum - Generates async axum handlers with
State<Arc<RwLock<T>>>extraction - Generates a
Routerregistration helper - Prove it with a standalone API binary that reuses an existing game core crate
Phase 2: Target configuration
- Define the
[webtau.targets]configuration surface - Wire feature flags to the configuration
- Emit only the backends the project has enabled
Phase 3: SpacetimeDB backend
- Add
generate_stdb()to the proc macro - Feature-gated behind
webtau_spacetimedb - Generates
#[spacetimedb::reducer]wrappers - Map state access to SpacetimeDB table/context model
Phase 4: TypeScript provider adapters
- HTTP provider adapter for Axum backends
- SpacetimeDB client provider adapter
- Runtime target switching in the webtau TS package
Phase 5: Manifest + typed clients (#139)
- Machine-readable command manifest from macro metadata
- Generated TypeScript client per backend
- OpenAPI spec generation for the Axum surface
Non-goals
- This is not a "send Rust anywhere magically" framework — each backend has real constraints
- This does not replace the need for backend-specific configuration (Axum routes, SpacetimeDB schema, etc.)
- This does not attempt to abstract away the difference between local and remote execution — that is a conscious architectural choice the developer makes
- This does not try to make every command work on every backend — the API-kind split (API design: separate command, query, job, and authority semantics #156) is how you express which commands belong where
Prior art and cross-references
In this repo
- Brainstorming: evolve gametau from runtime portability to execution-topology portability #154 — execution-topology portability strategy
- API design: separate command, query, job, and authority semantics #156 — command/query/job/authority API-kind separation
- Macros/tooling: emit command metadata and optional typed clients from webtau commands #139 — command metadata and typed client generation
- webtau: add runtime info / capability introspection API #129 — runtime introspection API
- Runtime/tooling: add a headless simulation service mode for automation #145 — headless simulation service mode
Motivation from downstream projects
Production games built on gametau have already encountered the need for:
- A separate Rust authority service (Axum) for computationally heavy work
- SpacetimeDB as canonical game-state authority for economy, fleet, and multiplayer
- Shared HTTP contract schemas consumed by both desktop and web clients
- The same
core/crate powering local client logic and remote authority services
The recurring pattern across these projects is:
gametau should support one typed Rust capability with multiple execution targets — local or remote dispatch depending on target, cost, and authority needs.
This issue is that concept made concrete.