Bending the Fabric of Time v0.1 ▌
One entity. One stream. One writer. Everything that happened — and the present as a side effect.
The event-native database — .warp segments, one static binary, zero dependencies.
Watch the diagram. Every database stores where you are — and then you spend a year bolting on where you've been.
Three ideas. Each one is running live below — poke it. The stream you build in the second station is the one the third station folds.
One account, one document, one agent owns its stream — a single writer, its events totally ordered. No global write lock: entities shard across cores (8 by default), so writers on different shards run in parallel. Same-shard writers serialize briefly; there's no lock to configure and never a global one.
You don't UPDATE a row, you append a fact — into a .warp segment, warp's own append-only format. Press the buttons. Both worlds get the same write.
State is fold(events) — and so is every view. Three read-models, one truth, all live. Drag n and all three agree about the past.
expect: 3 throws on a conflicting write. No lost updates, no locks.
subscribe(cb) pushes live folded state, in-process — no pipeline.
Cross-entity aggregates declared once, materialized, kept live.
Multi-entity transactions with automatic compensation.
stateAt(n) for any n. The whole history is the storage.
erase() rewrites the segment without the entity.
So it's already a warp entity. Frames are events, playback is history(), and dragging the scrubber is literally stateAt(n) — seek to any frame, exactly.
Scroll. The panel follows. No schema.sql, no manual JSON, no stringly-typed event names.
import { warp, reject } from "@geeksquad/warp-embedded" const Account = warp.entity("account", { state: { balance: 0, currency: "USD" }, events: { opened: (s, p: { currency: string }) => ({ ...s, currency: p.currency }), deposited: (s, p: { cents: number }) => ({ ...s, balance: s.balance + p.cents }), withdrew: (s, p: { cents: number }) => ({ ...s, balance: s.balance - p.cents }), }, commands: { // atomic fold → check → emit; a reject writes nothing withdraw: (s, a: { cents: number }) => s.balance < a.cents ? reject("insufficient funds") : [{ type: "withdrew", payload: a }], },})
state is where the fold begins. No schema.sql, no stringly-typed event names — payload types are inferred from the reducers.alice.deposited(...) autocompletes to exactly what an account can do. A typo is a compile error, not a 3am incident. Synchronous — no await, no pool.3.1M reads/sec, a property lookup rather than a query, and they don't degrade while writes are landing.erase() rewrites the segment without the entity — real right-to-be-forgotten, not a boolean column.One M1 (arm64) laptop for every figure below — warp against SQLite, its fair embedded peer. Both crash-durable, both single-op, no app batching. Every number measured this pass, nothing carried over.
synchronous=NORMAL, warp mmap-append). warp memcpy's each event into a mapped segment; SQLite runs a full autocommit per row. Roughly 11×.Measured with real macOS F_FULLFSYNC — a true on-platter flush, not a page-cache promise.
synchronous=NORMAL, ~11× the throughput.| Engine | Commits/sec | Sync |
|---|---|---|
| warp — power-durable (group F_FULLFSYNC) | 1,200,000 | full flush per batch — survives power loss |
| warp — crash-durable (mmap, no fsync) | 1,300,000 | process-crash safe; power loss can lose the tail |
| SQLite (WAL, synchronous=FULL) | 11,500 | fsync per commit |
| warp — strict (F_FULLFSYNC per single event) | ~220 | one full drive-cache flush per event — the honest floor |
Three tiers, and the honest floor. warp's group-commit modes stay over a million/sec — crash-durable (mmap, process-crash safe) and power-durable (F_FULLFSYNC per batch, power-loss safe). But if you force a full drive-cache flush on every single event with no batching, warp drops to ~220/sec — slower than SQLite's per-commit fsync. That's not a mode you'd run; it's published so the durability claim can't be called a bluff. Group commit is the design.
We won't tell you warp is the fastest database, full stop. It isn't a relational engine and doesn't pretend to be.
warp drops ad-hoc SQL — you pre-declare the queries you need as views — and multi-table ACID, leaving you single-entity atomicity plus sagas. In exchange, the ~90% of an app that is entities changing over time, read live, runs an order of magnitude faster, with audit, real-time, history and erasure that the others make you build and then maintain forever.
If you need a six-table join with a window function next Tuesday, keep Postgres for that. For what your product's core loop actually does all day, warp is faster and does more.
| warp | SQLite | LiteFS | |
|---|---|---|---|
| Model | event streams per entity | relational (embedded) | SQLite + replication |
| On-disk format | .warp append-only segments (mmap) | B-tree pages + WAL | SQLite pages over FUSE |
| Crash-durable write | 1.3M/sec | 116K/sec | ≈ SQLite |
| Concurrent writes (8 cores) | 4.1M/sec — sharded | one write lock, no scaling | one primary |
| Reads (folded / cached) | 3.1M/sec | 516K/sec (B-tree SELECT) | read replicas |
| History / audit | built in | DIY (triggers) | DIY |
| Real-time subscriptions | built in | none | none |
| Time-travel | built in | DIY | DIY |
| Physical erasure (GDPR) | built in | VACUUM-dependent | DIY |
| HA / replication | built in — ownership + replicas | none | primary + replicas |
| Ad-hoc SQL / joins | ✗ pre-declared views | ✓ | ✓ |
| Dependencies | zero — one dylib, in-process | libsqlite | Go + FUSE |
One entity lives in exactly one place, owned by one writer — the model Cloudflare made everyone want, minus the proprietary runtime. One primitive, three ways to run it.
Link the engine, store to local .warp files. Zero network, zero processes to supervise. warp.open() and go — no daemon, no server, no connection string.
Add peers. Each node is the single writer for its hash-slice of entities and fire-and-forwards every write to warm replicas over the private network; a failover is instant because the replica already has the events. Ownership routing lives in the SDK — owner_of / isOwner.
Under the hood it's a C-ABI dylib, so any language with an FFI can embed it. Ships today with a full TypeScript SDK; the entity/command API is the same everywhere.
Same job, different engine. LiteFS ships SQLite pages over FUSE. warp has no SQLite in it — it's.warp append-only segments in a single static binary. Four orders of magnitude apart. Per-machine throughput on Fly isn't quoted here; that number goes up when it's measured on the platform, not before.
doc.edited({ span })Users, orgs, sessions, documents, billing. Real-time collaboration and audit trails arrive with the storage engine.
ledger.settled({ batch })An append-only log is a ledger. Sagas are transfers with compensation. History is non-negotiable and already there.
agent.remembered({ ref })Durable, replayable, per-entity streams are the substrate agent memory keeps trying to reinvent badly.
presence.joined({ room })Chat, presence, live dashboards, multiplayer. Subscriptions are a primitive, not a pipeline you operate.
tenant.woke({ region })One binary, one volume, entity-affine, region-routable. Tenants that nap when idle and wake sub-second.
user.erase() // Art. 17Erasure is physical. You answer an Article 17 request with a function call instead of a project.
# in-process, no daemon, no server
npm i @geeksquad/warp-embeddedimport { warp, reject } from "@geeksquad/warp-embedded" const Todo = warp.entity("todo", { state: { title: "", done: false }, events: { created: (s, p: { title: string }) => ({ ...s, title: p.title }), completed: (s) => ({ ...s, done: true }), }, commands: { // validated intent — a reject writes nothing complete: (s) => s.done ? reject("already done") : [{ type: "completed" }], }, }) const db = warp.open({ entities: { todo: Todo }, durability: "crash" }) db.todo("t1").created({ title: "ship it" }) db.todo("t1").complete() // command: atomic, guarded
One entity. One stream. One writer. Nothing to bolt on later.