helipod
Reference

FAQ

Honest answers about what helipod is, what it isn't, and what isn't built yet.

Is helipod production-ready?

For a single node, yes. The reactive engine, client SDK, dashboard, and CLI are real, and they're used end-to-end.

That includes schema plus typed query, mutation, and action functions; transactional execution through a DatabaseAdapter seam; WebSocket reactive subscriptions with index-range-precise invalidation (not table-level); optimistic updates; a durable offline outbox; helipod dev/serve; a single-binary compile; and a working docker compose up self-host. All of it ships and is exercised by tests that run through the real CLI server, not just unit mocks.

The full opt-in component set is built the same way: authentication, notifications, scheduling and crons, durable workflows with saga/compensation, change triggers, and an authorization component are all proven through the shipped entrypoints, not in-process harnesses alone.

Multi-node scale is newer, and it lives under a separate license. helipod serve --fleet (Tier 2, Postgres-backed write scale-out with live failover) and the Cloudflare-native multi-shard router are ee/-licensed packages, not the FSL-1.1-Apache-2.0 core. See Can I self-host for free? below.

They're functional and benchmarked (see How fast is it? below), but they're the newest part of the system and haven't run in production nearly as long as the single-node core.

If you're choosing helipod today, start with single-node self-hosting (SQLite or Postgres). It's the safe, well-exercised default for most apps. Reach for the fleet only once you've actually measured a write-throughput ceiling or need failover.

See What is helipod? for the full pitch and What's deferred below for what's honestly not built.

What's deferred / not shipped

None of these are documented as shipped features anywhere else in these docs.

SQLite vs Postgres: which should I use?

Both are opt-in, drop-in storage backends behind the same DatabaseAdapter/DocStore seam. The engine never imports either driver directly, and switching between them is a flag, never an application-code change.

The zero-config default: an embedded, MVCC, single-writer store with nothing external to run. It's the right choice for helipod dev, and for self-hosted single-node production too. It's measurably the faster single-node store, since there's no network round trip and no fsync-per-commit cost: it's in-memory and CPU-bound.

helipod serve --dir helipod

Both are single-writer, and that's architectural, not a Postgres-specific limitation. Exactly one mutation commits at a time on either backend, because that's precisely what makes conflict validation cheap and the commit log's timestamp ordering meaningful. Concurrency adds latency (clients queue behind the one writer), not throughput. Measured on the same insert workload:

concurrent clientsSQLite ops/sSQLite p50 / p99Postgres ops/sPostgres p50 / p99
144,5160.019 / 0.042 ms4,5160.211 / 0.585 ms
846,5530.019 / 0.041 ms4,6171.643 / 2.678 ms
6446,1570.019 / 1.527 ms4,47214.018 / 20.209 ms

Postgres is roughly an order of magnitude slower than SQLite here, and it's fsync-bound: the commit's real cost is the disk sync, not the engine. To scale writes past one writer, you shard (or add fleet nodes at Tier 2, see below). You never add threads against one connection.

Neither backend needs an app-schema migration, ever. Both are physically schemaless: your schema.ts tables, fields, and indexes live as data inside a small, fixed set of internal tables (an append-only MVCC log: documents, indexes, plus bookkeeping tables) that never change shape as your schema evolves. There's no CREATE TABLE, no ALTER TABLE, no migration file to write or run, on either store. See Postgres for the full mechanics, including group commit (a free +39 to 58% Postgres throughput win, on by default there and off by default on SQLite where it's a net loss with no fsync to amortize).

How does helipod compare to Convex?

The reactive model is directly inspired by Convex's public architecture on purpose. A query records a read set as precise index ranges, a mutation commits a write set, and a subscription re-runs only when a committed write set intersects its read set. One mechanism gives you both OCC serializability and realtime, with zero manual cache invalidation. The differences are in what you can do with it:

  • Self-hostable by design, with no managed-cloud dependency. docker compose up brings up the engine, database, and dashboard in one container on your own infrastructure.
  • Storage is pluggable (SQLite or Postgres), not fixed to one proprietary database.
  • A single-binary compile (helipod build) embeds the engine, your app, and the dashboard in one executable, useful for Electron/Tauri-style distribution.
  • Native @helipod/* imports are canonical, not convex/*. helipod is its own product, not a Convex account or drop-in replacement. helipod migrate --from convex is the on-ramp (see Migrate from Convex), not the identity.
  • helipod has already grown past Convex's shipped feature set in some areas: durable workflows with saga/compensation, a Postgres storage adapter, and a durable offline mutation outbox with client-supplied ids all ship today.

Two API divergences worth knowing before you port code

There is no ctx.db.patch(...). Read the document, spread-merge the changes yourself, and call ctx.db.replace(id, { ...doc, ...changes }). helipod migrate flags every .patch(...) call it finds with this exact fix.

A mutation's client-side promise resolves at commit, not at Convex's later flicker-free gate. await send(args) resolves the moment the server's response arrives, meaning the mutation has committed, not at the point where an optimistic layer is provably superseded by an authoritative push. This is a deliberate departure. Gate-time resolution has sharp edges: a transport drop can turn a committed mutation into a rejected promise, and a lost gating frame with no follow-on traffic can leave a promise hanging forever. This protocol doesn't want to inherit those edges. In practice this rarely matters, since your optimistic update already renders synchronously the moment you call the mutation. But if you're porting await-then-local-cache-read code, know the guarantee here is "committed," not "your optimistic guess has been definitively superseded." See Optimistic updates for the full reasoning and the two documented residuals that fall out of it.

Measured, not just claimed. Here's a same-substrate benchmark (both backends as Docker containers on the same host, driven by their own native WebSocket clients through identical measurement code) against a matched app:

metrichelipodConvex
reactive propagation p50 (50 subscribers)8.6 ms13.4 ms
reactive propagation p9913.7 ms27.3 ms

Same order of magnitude, and in this same-substrate test, on par with or ahead of the commercial Rust reference it's modeled on. See Performance for the full scorecard and its caveats. helipod is a clean-room build studied against Convex's open, published architecture documentation, not a fork or a decompilation of Convex's code.

How does helipod compare to Firebase/Supabase?

Both are different kinds of reactive backend:

  • Firebase's realtime model is security-rules-based. Rules gate raw document access directly from the client, and there's no server-side transactional function layer standing between a write and the database it lands in. Your authorization logic lives in a rules DSL, not in ordinary code you can unit test.
  • Supabase's is Postgres plus a ring of roughly a dozen microservices (PostgREST, Realtime, GoTrue, Storage, Studio, and more) wired around row-level security, with its WAL-tailing realtime server as a single-threaded path.
  • helipod's reactivity comes from one mechanism, in one process: deterministic TypeScript functions with recorded read/write sets, intersected at commit time. There's no rules DSL to gate access (authorization is ordinary function code, optionally composed via @helipod/authz's row policies), and no fleet of services to run. One process is the whole backend.

There's also the deploy axis. helipod deploys anywhere you can run a container, a binary, or a Cloudflare Worker, a portability story that doesn't apply to either Firebase (Google-only) or, in practice, Supabase's own hosted product (a dozen-service self-host is heavy compared to a single docker compose up).

Can I self-host for free?

Yes. helipod is licensed FSL-1.1-Apache-2.0 (the Functional Source License, the same license Convex itself uses). That means free to use, modify, and self-host, including at scale, on your own infrastructure. The license forbids exactly one thing: offering helipod itself as a competing hosted service. Each release converts to plain Apache 2.0 two years after it ships.

Free forever, not a trial, no bait-and-switch:

  • Single-node self-host. The full engine (functions, reactivity, workflows plus saga, storage, scheduler, actions, httpAction, the Postgres adapter, the single-binary build, the dashboard). Production-usable for the large majority of real apps.
  • Deploy anywhere. Your box, your VPS, your cloud, Docker, an air-gapped server. No phone-home, ever.
  • Data and code portability. Plain HTTP, open formats, helipod migrate in and helipod migrate export/import your data out. You are never trapped.

What's gated, and when: multi-node write scale-out, meaning helipod serve --fleet (@helipod/fleet, Postgres-backed) and the Cloudflare-native multi-shard router (@helipod/runtime-cloudflare-shard), lives in a separate ee/ area under a different commercial license, not FSL, following the GitLab/n8n open-core pattern (an ee/ folder, not SSPL-style viral copyleft).

Right now, in this phase, both are free to use in production with no license-key gate at all. The goal of this phase is adoption, not revenue. The plan (not yet executed) is that a future paid license key unlocks scale and enterprise capability once there's real demand for it, and even then, the key unlocks a capability, never a deployment location: you always deploy on your own infrastructure either way. No managed cloud, no metered usage, no phone-home verification: a signed key checked offline at boot, the same model n8n and GitLab use.

Not yet. See What's deferred above. This is intentionally not documented as a feature anywhere else in these docs, and helipod migrate flags a Convex app's .searchIndex(...)/.vectorIndex(...) usage as unsupported rather than pretending to translate it. If your app needs search today, reach for an external service or a custom adapter.

How fast is it? (measured numbers)

Every number helipod quotes is hand-transcribed from a runnable benchmark harness under benchmarks/, measured under the same conditions on both sides of any comparison. The headline results:

axisheadline result
reactive propagation vs Convex (same-substrate, 50 subscribers)8.6 ms p50 vs 13.4 ms
Postgres group commit (on by default there)+39% to +58% write throughput under concurrency
reconnect bandwidth with resume fingerprints99.3% smaller for unchanged subscriptions
concurrent subscribed connections, one sync node10,000 clean at 7.69 KB/connection
Cloudflare DO-native vs Containers write latency133 ms vs ~1,500 ms

For the full scorecard (write throughput, sharding and fleet scale-out, the offline outbox, Docker capacity tiers), the honest caveats behind each number, and the commands to reproduce every one, see Performance.

What runtime does it run on?

Bun is primary: helipod dev/serve, and the single-binary compile (bun build --compile). But Node is fully supported for running the engine (npm packages, a Node SQLite adapter). The engine itself is runtime-agnostic behind its storage/runtime seams. Neither helipod dev nor serve cares which one you're running, as long as the seam is satisfied.

Can I run it on Cloudflare?

Yes, via @helipod/runtime-cloudflare, in one of two genuinely different architectures. The DO-native path is a first-class deploy target: helipod deploy --target cloudflare reconciles your wrangler.jsonc bindings (the Durable Object class, its SQLite migration, nodejs_compat, optional R2) and shells out to wrangler deploy for you. The Containers path is a hand-wired wrangler deploy of the portable helipod serve image.

One Durable Object is the whole backend: the OCC writer, DO-SQLite storage (ctx.storage.sql), every hibernatable WebSocket, the subscription index, and a wake alarm, all in the same object. Because the writer and the subscription index are the same in-process object, a mutation's reactive fan-out is a plain function call in the same turn, not an RPC hop to reorder across. That's what makes the engine's write-serialization and origin-frontier ordering guarantees hold by construction here.

Scheduled functions, crons, triggers, and the storage reaper all fire on this path. A DO alarm (ctx.storage.setAlarm) wakes the object even from full hibernation and calls runtime.fireDueTimers(). This is the key differentiator from the Containers path.

forwards every request/upgrade to one DO by name calls Client (HTTP / WebSocket) Worker (stateless router) HelipodDurableObject runtime.fireDueTimers() OCC writer (the DO's own single-threaded model IS the mutex) DoSqliteAdapter over ctx.storage.sql Hibernatable WebSockets Subscription index Wake alarm

Known limits: 10 GB DO-SQLite storage per object, 128 MB memory (billed flat), a soft ~200 to 500 writes/s ceiling for a write-heavy single DO, and, in v1, a single global DO (no built-in sharding; that's a separate paid-tier package, @helipod/runtime-cloudflare-shard).

Measured, both real

DO-nativeContainers
write latency133 ms~1,500 ms

Both measured against real Cloudflare and real R2 (2026-03-12/16). DO-native is ~11× faster on writes because it's a co-located DO-SQLite write versus an R2 CAS round trip on every commit.

When to pick which

Default to DO-native, unless you specifically need the portable image's storage options (Postgres, for example) that DO-native doesn't offer, or you're deliberately choosing a request-driven, scheduler-free app. See Cloudflare for the full setup of both paths, including wrangler.jsonc, the region-pinning hint, and R2-backed file storage.

Does it lock me in?

No. Two things make that concrete, not just a slogan:

  • The same app code runs everywhere. Your schema.ts and helipod/ functions run unchanged on helipod dev, helipod serve (SQLite or Postgres), a compiled single binary, a multi-node fleet, or either Cloudflare path. Moving up the tiered architecture changes deployment configuration and adapters, never the functions you wrote.
  • Your data moves too, explicitly. helipod migrate export/helipod migrate import pulls a full point-in-time dump (every live document, every index row, the table-number map) from a running deployment's admin API and pushes it into a fresh one. The same tool works between any two hosts or topologies, because every helipod store shares the same logical MVCC-log shape.
HELIPOD_ADMIN_KEY= helipod migrate export --url https://old-host.example.com --out dump.json
HELIPOD_ADMIN_KEY= helipod migrate import --url https://new-host.example.com --in dump.json

Caveats worth knowing: import targets a fresh deployment, not a merge, and refuses if the target's table numbers don't match the dump's. It's single-shard only today (multi-shard migration isn't built). And the dump is a point-in-time snapshot, not a live or streaming copy, so stop writes on the source for a clean cutover. Different physical topologies (a Postgres fleet versus a Cloudflare DO-native host, for example) genuinely store data differently, so this export/import step is how you move between them. Data doesn't teleport, but it's never trapped either.

And on the license side: single-node self-hosting, deploying anywhere, and data and code portability are free forever under FSL, not a trial, not a bait-and-switch. See Can I self-host for free? above.

What does "component" mean here?

An opt-in, composable piece of server-side functionality, such as authentication, notifications, scheduling and crons, durable workflows, change triggers, or authorization, that you add to your project by listing it in helipod.config.ts (for example, defineAuth(), defineScheduler()). The core engine (schema, queries, mutations, reactivity) has no idea what auth or notifications are.

A component adds its own namespaced tables (so scheduler/jobs can never collide with your app's own jobs table), a ctx.<name> facade in your handlers, internal modules, and sometimes a background driver. It's not installed by any init wizard. You compose exactly the components your app needs, and a component can declare a dependency on another (defineWorkflow() requires "scheduler") resolved automatically at compose time. See Components overview.

Where do I go next?

On this page