Skip to content

Macro target backends: ship #[webtau::command] to Axum, SpacetimeDB, and beyond #175

@devallibus

Description

@devallibus

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:

  1. An authority service (standalone Axum) for heavy computation like route solving or physics
  2. A game-state authority (SpacetimeDB) for economy, fleet, contracts, multiplayer
  3. 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-in

The 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

  1. Feature-gated generatorsgenerate_axum(), generate_stdb(), etc.
  2. State access pattern per backend — each backend has its own state extraction idiom
  3. Serialization boundary per backend — Axum uses serde_json, SpacetimeDB has its own serde, WASM uses serde_wasm_bindgen
  4. Async support — Axum handlers are async; the inner function can stay sync with spawn_blocking or the macro can generate the async boundary

What needs to change in the webtau crate

  1. Optional dependencies behind feature flags: axum, spacetimedb, tokio
  2. State management helpers per backend (like wasm_state! but for Axum AppState and SpacetimeDB ReducerContext)
  3. 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

  1. Provider adapters that can talk to Axum HTTP endpoints instead of (or alongside) Tauri IPC and WASM direct calls
  2. 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:

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 Router registration 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

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions