Skip to main content

Runtime Configuration and Backends

7. Getting Started & Execution Guide

Follow these steps to build, configure, and execute the application with various database engines across different WASI-compliant WebAssembly runtimes.

⚙️ Prerequisites

Ensure you have the following installed on your developer machine:
  • Rust Toolchain: Stable release (Rust 1.93.0+ or similar)
  • WASM Target: rustup target add wasm32-wasip2
  • Fermyon Spin CLI (for Spin runtime): brew install fermyon/tap/spin
  • Wasmtime CLI (for bare WASM runtime): brew install wasmtime
  • cargo-leptos: cargo install --locked cargo-leptos

🔑 Environment Setup (.env)

Before running the application, configure your databases. We provide a complete template. Copy the example file to initialize your config:
cp examples/counter-app/.env.example examples/counter-app/.env
Open examples/counter-app/.env and inspect the configuration variables. The .env.example template is tracked by version control as a reference, enabling seamless collaboration and automated testing across local and cloud environments:
# Supported make backends: sqlite, postgres, neon, supabase, turso, mysql, redis
DATABASE_BACKEND=sqlite

# Realtime transport: off, polling, redis
REALTIME_BACKEND=off
REDIS_CHANNEL=counter-events

# make derives DATABASE_URL/DATABASE_AUTH_TOKEN from the backend-specific
# values below before launching Spin or Wasmtime. Do not set those internal
# runtime values here for normal counter-app workflows.

# =========================================================================
# 1. PostgreSQL Settings (Local PostgreSQL)
# =========================================================================
POSTGRES_URL=postgresql://postgres:postgres@localhost:5432/postgres

# =========================================================================
# 2. Neon Settings
# =========================================================================
NEON_DB_URL=

# =========================================================================
# 3. Supabase Settings
# =========================================================================
SUPABASE_URL=
SUPABASE_SECRET_KEY=

# =========================================================================
# 4. LibSQL / Turso Settings (HTTP API)
# =========================================================================
TURSO_URL=
TURSO_AUTH_TOKEN=

# =========================================================================
# 5. MySQL Settings
# =========================================================================
MYSQL_URL=mysql://user:password@127.0.0.1:3306/counter_app

# =========================================================================
# 6. Redis Settings (experimental event store and realtime notifications)
# =========================================================================
REDIS_URL=redis://127.0.0.1:6379

🔀 Advanced Enterprise Multi-Backend Architecture

Our Leptos application implements a state-of-the-art Multi-Backend Persistence Engine inside src/store.rs. Because our CQRS and Event Sourcing infrastructure depends strictly on framework traits (EventStore, CheckpointStore, Projection), we designed dynamic, runtime-routed wrappers—MultiBackendEventStore<A>, MultiBackendCheckpointStore, and MultiBackendCounterProjection—which inspect the environment at boot-time and execute the correct database operations without modifying a single line of business or component-rendering code.

Supported Database Backends Matrix

Backend Key (db)Connection ModelNetwork ProtocolTarget Runtime CompatibilityUse CasesRealtime Support
sqliteLocal Host-Call or JSON filesSpin SQLite host calls; Wasmtime /data mountFermyon Spin & WasmtimeLow-latency local dev, edge microservices, zero-dependency local testingYes (SSE polling or Redis wake)
postgresDirect Socket PoolTCP Socket streamFermyon Spin & WasmtimeClassic high-throughput self-hosted PGYes (SSE polling or Redis wake)
neonStateless HTTP SQLJSON over HTTP (WASIp3)Wasmtime & Fermyon SpinServerless cloud databases with cold-start mitigationYes (SSE polling or Redis wake)
supabaseStateless RESTJSON REST over HTTP (WASIp3)Wasmtime & Fermyon SpinRapid prototyping, managed Supabase database integrationYes (SSE polling or Redis wake)
tursoHrana ProtocolPipeline HTTP (WASIp3)Wasmtime & Fermyon SpinGlobally distributed SQL, SQLite-at-the-edge (Turso)Yes (SSE polling or Redis wake)
redisAsync Redis commandsRESP TCP under Wasmtime, Spin Redis outbound and optional Redis Trigger under SpinWasmtime & Fermyon SpinExperimental event persistence, checkpoints, and realtime notificationsYes (via PubSub / SSE)
mysqlDirect SocketTCP stream on Wasmtime; Spin SDK MySQL host API on Spin; native driver on non-WASI targetsWasmtime, Fermyon Spin, and native targetsHigh-throughput self-hosted or cloud MySQLYes (SSE polling or Redis wake)

⚡ Overcoming WASM Sandbox Limits: Stateless Outbound HTTP

When compiling applications to WebAssembly targets, traditional blocking socket connection pools (such as those used by tokio-postgres or standard native SQLite engines written in C) are strictly incompatible with the isolated, single-threaded sandboxed environment of a WASM component. To solve this compilation and runtime block, our Multi-Backend Engine employs a stateless, highly optimized Outbound HTTP Bridge powered by the WASIp3 HTTP Component Model standards. When a database request is sent to Neon, Supabase, or Turso, the store adapter converts the SQL query and its parameterized arguments into a payload format (e.g., the JSON-based Hrana pipeline protocol for Turso/LibSQL or the HTTP SQL Endpoint format for Neon), dispatches it via a single non-blocking HTTP POST, collects the response, and translates the rows back into DDD event envelopes.

Sequence Flow: Outbound HTTP Database Query

Here is a simplified look at how the MultiBackendEventStore leverages WASIp3 Outbound HTTP helpers to communicate with external SQL APIs:
// A look under the hood of src/store.rs:
pub struct MultiBackendEventStore<A> {
    _phantom: PhantomData<fn() -> A>,
}

impl<A> EventStore<A> for MultiBackendEventStore<A>
where
    A: Aggregate + 'static,
    A::Event: serde::Serialize + serde::de::DeserializeOwned,
    A::Id: serde::Serialize + serde::de::DeserializeOwned,
{
    type Error = EventStoreError;

    fn load(&self, aggregate_id: &A::Id) -> Result<Vec<EventEnvelope<A::Event, A::Id>>, Self::Error> {
        let backend = get_backend();
        
        match backend.as_str() {
            "sqlite" => {
                #[cfg(runtime_spin)] {
                    let store = SpinSqliteEventStore::<A>::new("default");
                    store.load(aggregate_id)
                }
                #[cfg(not(runtime_spin))] {
                    // Under Wasmtime, fallback to JSON Flat-File Store mounted at /data/
                    let store = JsonFileEventStore::<A>::new("/data");
                    store.load(aggregate_id)
                }
            }
            "postgres" => {
                #[cfg(feature = "postgres")] {
                    let store = PostgresEventStore::<A>::new(get_postgres_url());
                    store.load(aggregate_id)
                }
                #[cfg(not(feature = "postgres"))] {
                    Err(EventStoreError::Backend("Postgres feature not enabled".to_string()))
                }
            }
            "neon" => {
                // Execute stateless queries over Outbound HTTP SQL API
                let url = get_postgres_url();
                let sql = "SELECT ... FROM events WHERE aggregate_id = $1";
                let rows = block_on(execute_neon_query(&url, sql, vec![aggregate_id.to_string()]))?;
                deserialize_postgres_rows(rows)
            }
            "turso" | "libsql" => {
                // Public backend name is "turso"; "libsql" remains an internal
                // compatibility branch used by runtime/store internals.
                // Execute Hrana pipeline over Outbound HTTP
                let url = get_turso_url();
                let token = get_turso_auth_token();
                let sql = "SELECT ... FROM events WHERE aggregate_id = ?";
                let result = block_on(execute_hrana_query(&url, token.as_deref(), sql, vec![aggregate_id.to_string()]))?;
                deserialize_sqlite_rows(result.rows)
            }
            _ => Err(EventStoreError::Backend(format!("Unsupported database backend: {}", backend))),
        }
    }
    
    // Similarly dispatched for `append()` and `load_global_after()`...
}