Offline-First with Embedded SQLite
CrateStack’s embedded backend,cratestack-rusqlite, runs the same .cstack schemas you use on the server against a local SQLite database. As of 0.3.0 it ships from one source to three deployment targets:
- Native mobile (iOS, Android via FFI /
flutter_rust_bridge) - Native desktop (Windows, macOS, Linux)
- Browser via
wasm32-unknown-unknownwith OPFS-backed persistence
RusqliteRuntime, the same delegate API, the same .cstack schema — only the FFI backend swaps per target (libsqlite3-sys on native, sqlite-wasm-rs on wasm32). This is handled transparently by rusqlite 0.39.
The architecture is “Rust as real frontend, UI as UI-only”: Rust owns state, persistence, and business logic on the device or in the browser tab; the UI (Flutter, React, Solid…) talks to Rust over FFI or
wasm-bindgen. The same .cstack schema definition compiles to both the server (Postgres via cratestack-sqlx) and the embedded target (SQLite via cratestack-rusqlite).What Changes Per Target
The shared dialect-agnostic primitives (filter AST, order AST, value types,
ModelDescriptor) live in cratestack-sql — all backends consume them.
Add the Dependency
The umbrellacratestack crate already re-exports the SQLite backend types you need. For a regular native build:
Browser-specific build prerequisites
sqlite-wasm-rs (used by rusqlite 0.39 on wasm32) compiles SQLite’s C source to WebAssembly via cc-rs. That requires a wasm-capable clang on PATH — Apple’s stock Xcode clang does not include the wasm32 backend.
- macOS
- Linux
- Emscripten
Schema
Useprovider = "sqlite" on the datasource block. Everything else in your .cstack schema works the same:
The embedded runtime ignores
auth blocks and @@allow / @@deny policies at SQL render time. They still parse and validate; they just don’t gate reads or writes. Declare them as if you were on the server — your schema stays portable.Minimal Setup — Native
Minimal Setup — Browser (OPFS)
OPFSSyncAccessHandle is only available inside a Dedicated Worker per the W3C spec. Install the VFS once inside the worker before opening the database; subsequent RusqliteRuntime::open(filename) calls automatically route through it.
OpfsOptions lets you tune the SAH pool directory, initial capacity (handles allocated up-front, one per open DB file + journal), and the clear_on_init flag (useful for “logged out, clear data” flows).
Main-thread code can still use RusqliteRuntime::open_in_memory() for ephemeral state when OPFS isn’t available.
Filtering, Ordering, Paging
The fluent delegate API mirrors the server side. Field accessors are emitted byinclude_embedded_schema! per model:
where_expr, and, or, and relation filters from cratestack-sql all work here.
Storage Class Choices
Embedded apps live or die by data fidelity, so the binding layer commits to canonical representations rather than relying on SQLite’s loose typing:
The DDL generator declares every column with
BLOB affinity to preserve those storage classes. TEXT or NUMERIC affinities would silently convert numeric-looking text and lose Decimal precision; BLOB is the only affinity that respects what you bind.
Soft Delete and Audit
@@soft_delete works embedded: DELETE calls become UPDATE of the soft-delete column, and every find_* automatically filters rows where the soft-delete column is non-null. @@audit and @@emit are currently no-ops in include_embedded_schema! — the local-journal / local-event-bus implementations land in a follow-up release (they need a sync-engine design pass first).
The FFI Boundary
The embedded runtime is sync, so it slots into FFI bridges (e.g.flutter_rust_bridge on native, wasm-bindgen in the browser) without an async runtime to drag along. The boundary helpers in cratestack_rusqlite::ffi give you a small envelope to encode requests and responses across the language gap:
dispatch(...) function lives in your app crate — it knows the specific model types and routes each (model, kind) pair to the right delegate call. See cargo run --example sqlite_ffi_dispatch -p cratestack in the framework repository for a complete template.
Examples in the Framework Repo
Cargo-native examples (run withcargo run --example <name> -p cratestack):
sqlite_quickstart— smallest working program: open in-memory DB, bootstrap one table, CRUD a row.sqlite_offline_first— file-backed DB, two models,Decimalmoney preserved exactly, filtering and partial updates.sqlite_ffi_dispatch— the JSON-bytes FFI dispatcher you’d wrap withflutter_rust_bridge.
examples/ — pick the one that matches your deployment shape:
If you’re embedding the SQLite path inside a tokio process (server, daemon, anything async), read Async I/O with Embedded SQLite next — it covers the
spawn_blocking seam that the daemon and webhook examples exist to demonstrate.
Limits Today
The embedded backend is intentionally focused. The following are not implemented in 0.3.0:- policy enforcement at the storage layer (by design — see above)
@@auditand@@emit— directive parses, codegen is no-op, follow-up release will add the local journal + event bus- idempotency middleware (HTTP-layer concept; embedded apps already control retries)
- generated HTTP routes (no transport in the embedded macro)
- richer migration tooling (apps typically use
CREATE TABLE IF NOT EXISTS+ ad-hocALTER TABLEmigrations)
Read Next
- Field Attributes for the schema surface available on every target
- Scalars including the
Decimalprecision contract that round-trips through SQLite TEXT storage - Quickstart for the server-side path