Experiments
A local-first hypermedia framework for Rust

One binary. One Postgres. Nothing else.

Eigen compiles your application into one static binary and keeps every kind of state in one Postgres. The binary is your web app, your desktop app, your phone app, and your API. Postgres is your database, your job queue, your cron scheduler, your sync log, your transaction engine — and your complete history. The framework supplies everything in between — the sync engine, the reactive UI layer, the workflow engine, the component library — as code inside the binary, not services beside it. Two things, and you can point at both of them.

./crm — one file, a few MB postgres:// — the only stateful thing there is no third thing
The tension nobody resolved

Two good ideas, built on opposite axioms

For a decade, application architecture has forced a choice of religion: effortless server-driven reactivity, or offline-capable client-owned data. The frameworks that give you one are philosophically incapable of the other.

Hypermedia — LiveView, htmx, Datastar
“The server owns the state. The client is a thin terminal.”
Local-first — Electric, PowerSync, Automerge
“The client owns the state. The server is a dumb relay.”
Eigen dissolves the choice: every node is a server. The UI always talks to “the server” — sometimes it's three milliseconds away on loopback.
How it deploys

Three modes. One codebase. Zero forks.

One rule makes it work: rendering never crosses the network — only data does. Whatever binary is closest to the screen renders, against whatever database is closest to it. Connectivity becomes a property of the sync engine, not a mode you write.

--mode=server

The web app & API

Public listener. Serves the hypermedia plane to browsers and the data plane to native peers and integrations. Postgres behind it.

render: remote · data: direct
--mode=native

The app — laptop or phone

The same handlers render into a Tauri 2 shell against a local SQLite replica that syncs in the background. Tauri already ships desktop, iOS, and Android — eigen doesn't build shells, it inhabits them, bound to loopback.

render: local · data: synced
--mode=native, offline

The dead-zone app

Not a mode — a circumstance, and on a phone it's a daily one: the basement, the job site, the plane. Mutations queue, remote changes apply on reconnect. No spinner, because the database never left the device.

render: local · data: queued
On the device — laptop · phone
Tauri 2 shell
system webview — UI only, over loopback
EIGEN BINARY — native mode
resources · typed components · diff engine · workflows (queue offline) · sync loop (pull & queue)
SQLite replica
working set · membership horizon · tokens, never PII · works when it can't sync
the seam — data only
In the cloud — identical nodes
Browsers · integrations
thin HTML clients · API & webhooks
EIGEN BINARY — server mode
same build · same handlers · workflow checkpoints · exactly-once intent replay
Postgres
truth · queue · cron · history = sync log · PII vault — shreddable
HTML never crosses the seam — only data doesone change stream moves state along this axis
Unique feature № 1 — the resource

Declare once. Everything derives.

Every guarantee in this document has to know your schema: sync needs the horizon, the vault needs the PII fields, history needs the tables, forms need the types, policy needs the scope. So eigen has one source of truth: the resource — a declaration the whole framework derives from, the idea Ash proved on the BEAM, translated to Rust's terms.

From one resource! block, cargo eigen derives the SQL schema and its migrations, the history tables, the sync horizon, the intent endpoints and their policy checks, the form components — and the invariants that bind them. In Rust the verifier layer is mostly the compiler: a hallucinated field doesn't need a linter; it doesn't compile. Cross-resource invariants run in cargo eigen check.

Deliberately a closed derivation pipeline, not a meta-framework: a fixed set of derivations done deeply, not an open extension system. That's the honest Rust translation of Ash — and the reason one person can maintain it.

resource! {
  Deal {
    stage: Stage, // history: every version
    amount: Money,
    owner: Pii<Contact>, // vault-routed, never syncs
    company: BelongsTo<Company>,
    horizon: by(Assignment), // who syncs this row
    policy: org_scoped + role(Sales),
  }
}
schematables + migrations, generated and versioned
historyevery version of every row — audit, as-of, and the sync log
sync + policyhorizon membership, intent endpoints, org-scope + RBAC checks
UIform and field components with types, validation, and PII masking built in
Unique feature № 2 — the reactive layer

Typed components, diffed on the server, patched in bytes.

Handlers never emit markup. They emit typed semantic components — and because the tree is typed, eigen does at compile time what LiveView does on the BEAM: split every component into static parts (sent once) and dynamic bindings (tracked). When deal.stage changes, the patch on the wire is a few bytes, not a re-rendered fragment. Rust doesn't have this today; eigen is where it gets built.

Eigen owns the patch protocol end to end. The v1 client runtime is the vendored Datastar client — MIT, ~15 KB, pinned, shipped from the binary, audited once — replaced by eigen's own smaller runtime once production usage has written the real spec. Owning both ends of the wire makes the dependency a file in the repo, not a vendor.

And the runtime is latency-adaptive — the one thing no generic library will ever ship. On loopback, round trips are free and optimistic UI machinery simply turns off. Over WAN, local-echo signals cover the gap. Same components, same protocol, tuned to how far away “the server” happens to be.

// the handler emits meaning, not markup
#[component]
fn deal_card(d: &Deal) -> View {
  card((
    badge(d.stage), // dynamic: tracked
    label("Amount"), // static: sent once
    money(d.amount), // dynamic: tracked
  ))
}
 
// stage → ClosedWon: the patch is ~12 bytes
// v1 wire: owned protocol · vendored client applies it
browsersbyte-sized patches over SSE — the hypermedia plane
webviewssame patches over loopback — optimistic UI unnecessary
native, latersame patch semantics, a JSON payload dialect — SwiftUI/Compose reconcile
Every production server-driven UI dies offline — the composing server is across the network. Eigen's composing server is in your pocket. Server-driven UI that survives a dead zone is a category of one.
The component vocabulary

A closed set of components — because the native lane demands it.

Eigen ships a standard vocabulary — roughly twenty-five semantic components: page, nav, form, field, table, list, card, dialog, badge, and kin — headless, themed by CSS variables, composed into your own components freely. This is not a styling opinion; it's load-bearing architecture. The future native renderers can only reconcile a vocabulary they know. A closed, versioned component set is what makes “same handlers, native widgets later” a promise instead of a hope.

The vocabulary compounds with the resource: form(deal) derives its fields from the declaration — types drive inputs, constraints drive validation, and a Pii<> field renders masked automatically, because the component knows what the schema knows. The escape hatch is honest: raw HTML is allowed anywhere, and marks that subtree HTML-only — visibly opting it out of the native lane, a trade you make on purpose, per subtree, never by accident.

Unique feature № 3 — sync

The sync log is a table you already have.

Sync engines are company-sized because they bolt onto arbitrary schemas they don't control — generic replication decoding, bucket abstractions, their own storage. Eigen owns the schema, and eigen already keeps history — every version of every row, sequenced. The change stream isn't a new subsystem; it's an indexed query against the history tier. One table, three features: audit, time travel, sync.

The replica is a projection, not a peer. Down: cursor-based delta pulls, each batch applied in one SQLite transaction. Up: durable intents — the same form submit, queued, intent ID = workflow ID, so retries are no-ops. The device never merges; the server replays intents in order, policy decides, and a rejected intent comes back as UI, not a silent conflict.

Horizons are membership tables (org, assignment, territory) — not arbitrary predicates — so revoking access naturally emits delete markers into the stream: the hardest problem in partial replication becomes ordinary rows. And the v1 rule a general engine can't afford: when in doubt, resnapshot. Replicas hold a working set, not a warehouse.

-- the entire down-sync, conceptually
select * from changes
 where seq > $device_cursor
   and horizon @> $device_scopes
 order by seq limit 500;
 
-- live: the SSE channel the UI already holds
-- carries "changes available" — one socket, both planes
downscoped snapshots + delta pulls — checkpoint-consistent by construction
updurable intents, idempotent by workflow ID — server-authoritative replay
revocationmembership change = delete markers in the stream — offline devices catch up honestly
migrationsversion handshake; v1 answer is resnapshot — brutal, correct, then refined
Unique feature № 4 — the transaction model

Workflows that survive crashes, deploys, and airplanes

A workflow is ordinary code, checkpointed step-by-step in Postgres — the durable-execution pattern DBOS proved, implemented natively in eigen. Crash, redeploy, or kill the process — on restart it resumes from the last completed step. Steps that touch the database commit their effect and their checkpoint in one transaction: exactly-once, guaranteed by Postgres itself.

No orchestrator cluster. No Temporal server on the critical path. Durable execution is a library inside the binary — checkpoints are just rows.

And because enqueueing a workflow is a database write — and eigen already syncs database writes — a follow-up scheduled offline at 30,000 feet lands in the queue when you do. Devices enqueue; servers execute. A step with real-world effects only ever runs where the truth lives.

// a drip sequence, durable by construction
async fn follow_up(wf: &Wf, deal: DealId) -> Result<()> {
  wf.step(send_intro(deal)).await?;
  wf.sleep(days(3)).await?; // survives deploys
  let ok = wf.recv::<Approval>().await?; // waits for a human
  if ok { wf.step(send_contract(deal)).await?; }
  Ok(())
}
sync ingestionclient intent ID = workflow ID — retries are no-ops
recoveryany server node resumes any workflow from its last step
queues & cronrows in Postgres — concurrency, priority, dedup
offlineworkflows scheduled on a plane run when you land
Simplicity, measured in deletions

Infrastructure you will never run

Every box below is a thing most stacks deploy, version, secure, monitor, and pay for. In eigen, each one is a table, a query, or a library call — inside the two things you already have.

Redis

→ postgres — caching is a solved read; state was never Redis's job

Message broker

→ postgres — LISTEN/NOTIFY and durable queues, transactional with your data

Orchestrator cluster

→ the binary — durable execution is a library, not a Temporal deployment

Cron service

→ postgres — scheduled workflows are delayed rows

Job runner

→ the binary — every server node is already a worker

Webhook retrier

→ workflows — delivery is a step; steps retry and checkpoint

Sync service

→ the binary — the history tier is the sync log; the loop ships inside

Analytics warehouse

→ postgres — partitioned history + framework-refreshed rollups; dashboards read summaries

Auth service

→ postgres — identity is a core resource: orgs, users, roles, sessions, policy

Frontend framework

→ the binary — typed components diffed server-side; a vendored ~15 KB runtime applies patches

Frontend build pipeline

→ gone — no bundler, no node_modules, no second deployable drifting out of sync

ORM + admin scaffold

→ the resource — schema, queries, forms, and policy derive from one declaration

The mental model

Postgres for what's true now — and for what's ever been.

The routing rule isn't hot-versus-cold storage — it's read intent. A live page asks “what's true right now, and let me change it”: a point read and a write, against indexed rows. A report asks “what happened, over time”: a scan, against partitioned history tables and framework-refreshed rollups in the same Postgres. Dashboards read the small summary, never the raw stream.

Most databases are amnesiac by construction: every UPDATE overwrites the evidence. Eigen never runs a destructive update against truth — the change stream keeps every version of every row, time-partitioned, in the database you already run. So “as of” stops being a forensic project and becomes a WHERE clause — and the same rows are the sync log the replicas pull from. Audit, undo, as-reported reporting, and offline sync aren't four systems. They're one tier, read four ways.

-- the account, exactly as it stood that day
select * from accounts as of '2025-03-01'
  where id = 4812;
 
-- everything that changed, and when
select * from accounts.history
  where id = 4812 order by valid_from;
as-of state“what did the record say on March 1” — free from the log
change diffevery version, with who and when
replayrebuild any projection, dashboard, or replica from history
two clockstransaction-time automatic · valid-time when you model it
Trust, baked into the schema

Replay everything. Forget anyone. Carry nothing.

An immutable history and “delete my data” look like a contradiction. Eigen resolves it in the schema: a Pii<> field never enters the history stream — it lives in a mutable, keyed vault in Postgres; everything downstream carries only the key reference. And because the sync log is the history stream, the protocol is structurally incapable of shipping PII to a device — not filtered out; never present.

Online, actuals stream from the vault at render time and are never written to the replica; offline, the same field — the same derived form component — renders its abstraction: a label and ••••. The field tech in the dead zone sees the job, the schedule, the notes. The customer's phone number was never on the phone.

Erasure means destroying the key — crypto-shredding. History still replays perfectly; personal values come back null. And because no device ever persisted plaintext, erasure has no stragglers in the field. GDPR-compliant event sourcing goes from oxymoron to checkbox.

resource! { Account {
  stage: Stage, // history: every version
  owner: Pii<Contact>, // key reference only
} }
 
// right-to-erasure, in one line
vault.shred(owner_key).await?;
// history intact · PII unrecoverable
the historyreferences + non-personal facts, immutable — and it's the sync log
the vaultpersonal data, mutable, per-subject keys — server-side only
the replicatokens, never actuals — offline renders the abstraction
erasureshred the key — replay returns null, and no device ever held a copy
Why Rust

Memory safety is a feature you sell.

This binary is a long-lived daemon on your customers' hardware, holding their business data, parsing input from the network, for years between updates. In an unsafe language that is a CVE surface; in safe Rust the whole bug class is gone — an answer a B2B security questionnaire accepts. No GC, so no pauses under ten thousand open SSE streams.

Rust also supplies the two machines eigen is built from: proc macros — the compile-time metaprogramming that makes the resource derivation pipeline and the static/dynamic component split possible, with the compiler itself as the first verifier — and a mature crate floor: tokio + axum on the server plane, sqlx against Postgres, rusqlite over the replica, serde at every boundary, Tauri 2 as the shipped shell on desktop, iOS, and Android. The runtime dependency surface is your binary and Postgres. Everything else is a build-time artifact you pin, audit, and own.

Electron~150 MB
Tauri~4 MB
Eigen~8 MB
Tauri ships the shell — your backend still lives somewhere else. Eigen's 8 MB is the whole product: app + server + SQLite + sync + workflows + UI runtime, statically linked into the shell Tauri provides.
Who it serves

Sized for almost everyone

A single well-provisioned Postgres sustains tens of thousands of workflow steps per second. The median SaaS never sees one percent of that. Vertical SaaS, B2B platforms, internal tools, ops software — and above all the field teams that lose signal daily and get told “offline is on the roadmap” by every vendor they evaluate. This architecture covers the overwhelming majority of software businesses with two deployed things and one on-call runbook.

Scale up by buying a bigger Postgres — the most boring, best-understood move in the industry. Scale out by adding identical stateless binaries. You will exit this envelope around the time you can afford a platform team to build the next one.

exists
Tauri 2

Desktop, iOS, and Android shells — shipped, stable, someone else's roadmap. Eigen binds to loopback inside them; the shell workstream is deleted.

exists
tokio · axum · sqlx · rusqlite · serde

The server plane, both databases, every boundary — the most exercised crates in the ecosystem.

exists
Postgres primitives

Logical sequencing, LISTEN/NOTIFY, partitioning, transactional DDL — the sync log, live channel, and history tier are queries, not services.

vendored
Datastar client (transitional)

MIT, ~15 KB, pinned and shipped from the binary — the v1 patch applier. Replaced by eigen's own runtime once production usage writes the spec. A file in the repo, not a vendor.

prior art
Ash · LiveView · DBOS

The resource-derivation idea, the static/dynamic diff split, and durable execution over Postgres tables — proven elsewhere, implemented natively here. Credits, not dependencies.

eigen
The resource pipeline

resource! + cargo eigen: schema, migrations, history, horizons, intents, policy, forms — one declaration, a closed set of derivations, the compiler as first verifier.

eigen
The reactive layer

Typed component tree, compile-time static/dynamic tracking, byte-sized patches, an owned wire protocol, latency-adaptive delivery. LiveView's crown jewel, transplanted to Rust.

eigen
The component vocabulary

~25 semantic, headless, themeable components — closed and versioned because the native lane requires it; forms derived from resources, PII masked by construction.

eigen
The sync loop

History-native deltas down, durable intents up, membership horizons, resnapshot as the correctness escape hatch. Small because the schema is owned — the flagship, not the 70%.

eigen
The workflow engine

Checkpoints as rows, effect + checkpoint in one transaction, queues and cron as delayed rows — devices enqueue, servers execute.

eigen
The PII vault + offline abstraction

Crypto-shredding as a schema primitive; actuals render server-fed and never persist on a device — masked is what offline looks like.

eigen
Identity, in the core

Orgs, users, roles, sessions — not a bundled app, but the resources horizons, policy, and the vault are structurally built on. The only scope the framework ships.

What we won't pretend
  • The sync loop's correctness burden is ours now. Resumable cursors, exactly-once apply, tombstone compaction, fleet schema migration — bounded by owning the schema and by the resnapshot rule, but owned, tested red-path-first, and shipped before anything else gets clever.
  • The browser runtime is deferred on purpose. DOM morphing that preserves focus, selection, and IME state is years of quirk archaeology — so v1 vendors the Datastar client and eigen's own runtime is built later, against a red-path suite harvested from production, not from imagination.
  • A closed vocabulary is a real constraint. Raw HTML is always available — and visibly exits the native lane for that subtree. The trade is explicit, per subtree, never accidental.
  • Not a business-in-a-box. Identity ships in the core because the guarantees are built on it. Billing, CRM, and support are apps you build on eigen — extracted into packs later by the Rule of Three, if ever. The fastest way to ship zero products is to preset every scope before one exists.
  • Offline means masked PII. The dead-zone app shows the work, not the personal actuals — the design consequence of a vault no device ever holds. A workflow that genuinely needs actuals offline is a scoped, expiring exception to design deliberately — never a default.
  • Workflows are for mutations, never reads. Each step costs a Postgres write. The render path stays out of the engine — a design rule, not a tip.
  • History lives in Postgres, and Postgres has a ceiling. Partitioning + rollups carry most products indefinitely; past ~100M+ raw history rows with ad-hoc scan appetite, a columnar engine is a per-product bolt-on decision — the stack does not carry a warehouse for the day you might need one.
  • Erasure is as final as your backup posture. The key table rides outside the history stream and is built to be excluded from long-lived backups; a shredded subject lingers in cold backups exactly as long as your stated retention. Name it.
  • Mobile means a shell, and shells have landlords. Store review policies, background sync at the OS's pleasure — design for foreground sync, queue the rest. New native widget kinds, when that lane opens, ride a store release.
  • Rust taxes you at compile time, not at 3am. Build times, proc-macro debugging, and the learning curve are real. That's the trade for a memory-safe daemon deployed on hardware you don't control — and a framework whose first verifier is the compiler.
The pitch, in one sentence

Your web app, desktop app, mobile app, API, UI runtime, sync layer, queue, cron, history, and transaction engine are two things. You can point at both of them.