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.
Not built. There's no .searchIndex()/.vectorIndex(), and no builder for either is in progress.
helipod migrate flags a Convex app's use of either as unsupported in its migration report
rather than pretending to translate it. See
Migrate from Convex. If your
app needs search today, reach for an external service.
User functions run in-process today, through an inline executor. The syscall boundary, where every
ctx.db call crosses as a JSON operation name plus a JSON argument into a host-side kernel, is
deliberately designed to be isolate-ready: the same boundary would work unchanged if the guest side
became a real V8 isolate with its own globals. But that swap hasn't happened. Don't run untrusted,
multi-tenant code on a shared deployment expecting hard sandboxing. The inline executor trusts the
code it runs.
helipod serves plain HTTP. Put a reverse proxy (nginx, Caddy, Traefik) in front for HTTPS. This is true on every deploy path except the Cloudflare Worker paths, where Cloudflare itself terminates TLS at the edge.
Reading a private file is a bearer-token model today: whoever holds a valid getUrl link can read
it until it expires. The storage serve endpoint has an internal checkRead(identity, id) seam
reserved for finer-grained rules, but it's intentionally unwired. There's no
per-(table, id, identity) read-permission primitive in the engine to plug into it yet. If you need
finer rules, check ownership yourself in the function that hands out the file's id or url.
Within @helipod/workflow: a workflow can't start and await another workflow as one of its own
steps. There's no built-in UI to step through or visualize a run's journal (query
workflow:status or browse the workflows/steps/events tables via the dashboard instead). And
a workflow's registered handler/step sequence is fixed for the deployment's lifetime, so changing it
while runs are in-flight against the old sequence throws a journal-mismatch error rather than
silently reinterpreting history.
The object-storage substrate (no database at all: an object store as the write-only durable log,
local SQLite as queryable state) has its core shipped and E2E-proven on both filesystem and real
MinIO, and its tail items have shipped since: a multi-shard writer (helipod serve --object-store <url> --shards N, one node owning all N lanes), read replicas (--replica, with
--writer-url to forward writes to the writer), a background GC driver (sweep cadence via
HELIPOD_OBJECTSTORE_GC_MS), and an offline reshard tool (helipod objectstore reshard).
The remaining gap is spreading shard ownership across multiple Tier-3 nodes. A multi-shard writer owns every lane itself. You can add replicas for reads, but you can't split those N shards across several writer nodes the way the Tier-2 Postgres fleet does.
Not a feature gap so much as a platform mismatch worth flagging here too: Cloudflare stops a
Container about 5 seconds after its last request, so ctx.scheduler/crons/triggers/the storage
reaper don't fire on that path. The Cloudflare DO-native path doesn't have this problem. See
Can I run it on Cloudflare? below.
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 helipodThe opt-in alternative for when you want a managed, externally-durable database (backups,
replicas, monitoring you already run) under the identical engine: --database-url postgres://…
(or HELIPOD_DATABASE_URL), no code changes. It's also the required shared substrate if you're
headed toward the multi-node fleet (see How fast is it?
below).
helipod serve --dir helipod --database-url postgres://user:pass@host:5432/dbBoth 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 clients | SQLite ops/s | SQLite p50 / p99 | Postgres ops/s | Postgres p50 / p99 |
|---|---|---|---|---|
| 1 | 44,516 | 0.019 / 0.042 ms | 4,516 | 0.211 / 0.585 ms |
| 8 | 46,553 | 0.019 / 0.041 ms | 4,617 | 1.643 / 2.678 ms |
| 64 | 46,157 | 0.019 / 1.527 ms | 4,472 | 14.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 upbrings 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, notconvex/*. helipod is its own product, not a Convex account or drop-in replacement.helipod migrate --from convexis 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:
| metric | helipod | Convex |
|---|---|---|
| reactive propagation p50 (50 subscribers) | 8.6 ms | 13.4 ms |
| reactive propagation p99 | 13.7 ms | 27.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 migratein andhelipod migrate export/importyour 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.
Does it do full-text or vector search?
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:
| axis | headline 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 fingerprints | 99.3% smaller for unchanged subscriptions |
| concurrent subscribed connections, one sync node | 10,000 clean at 7.69 KB/connection |
| Cloudflare DO-native vs Containers write latency | 133 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.
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).
This path has a correctness gap worth knowing about before you reach for it.
It runs the exact same portable helipod serve image you'd run under Docker anywhere else,
fronted by a stateless Worker, with R2 as the object-store source of truth. Container disk is
ephemeral, so a plain SQLite deployment here would silently lose data on every restart.
--object-store is not optional on this path.
Scheduled functions, crons, triggers, and the storage reaper do NOT fire on this path. Cloudflare
stops a container about 5 seconds after its last request (the same mechanism that gives
scale-to-zero economics), so a ctx.scheduler.runAfter(300_000, …) call schedules work into a
process that won't exist five seconds later. This is invisible until someone notices their emails
never sent. Use this path only for a genuinely request-driven app that doesn't depend on any of
those.
Measured, both real
| DO-native | Containers | |
|---|---|---|
| write latency | 133 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.tsandhelipod/functions run unchanged onhelipod 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 importpulls 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.jsonCaveats 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?
- New to helipod: What is helipod? → Quickstart.
- Coming from Convex: Migrate from Convex.
- Choosing storage: Postgres.
- Going past one node: Scaling.
- Deploying on Cloudflare: Cloudflare.
- Looking for exact signatures: Configuration and CLI.