helipod
The codebase

The monorepo

A tour of the packages, components, and ee/ modules that make up the Helipod repo, and how they depend on each other.

Helipod lives in one repository. That's not an accident: it's the same "everything in one place, split into small pieces" idea as the reactive engine itself.

This page is the map. It covers what's in the repo, why it's split the way it is, and the one rule that keeps it from turning into a ball of mud as it grows.

If you're about to open a PR, read this first. It'll save you from importing the wrong thing.

The layout at a glance

Think of the repo as four buckets, each with a different job:

BucketWhat lives thereHow manyOpt-in?
packages/The engine, the client SDK, and the CLI32 packagesNo, this is the product
components/Self-contained mini-backends (auth, scheduler, ...)6 packagesYes, per project
ee/packages/Paid, scale-out features under a separate license3 packagesYes, and separately licensed
apps/ + examples/The dashboard app, and runnable sample apps1 app, 4 examplesn/a

apps/dashboard is the live data browser, logs viewer, and function runner you get with every helipod dev/serve. examples/ (chat, auth-demo, offline-demo, optimistic-demo) double as end-to-end integration tests that run against the real CLI. A regression in wiring the pieces together shows up there, not just in unit tests.

Here's the real packages/ list, as of this writing, grouped by role:

values
errors
id-codec
index-key-codec
docstore
docstore-sqlite
docstore-postgres
docstore-do-sqlite
docstore-d1
objectstore
objectstore-fs
objectstore-s3
transactor
query-engine
executor
sync
receipts
runtime-embedded
runtime-cloudflare
component
storage
blobstore
blobstore-fs
blobstore-s3
blobstore-r2
client
cli
codegen
admin
deploy
vite
test

That's 32 packages. Most are small on purpose, a few hundred lines each. A small package is easy to read start to finish, and it's also the only thing a dependency-direction rule (below) needs to check.

A note if you've read the old design docs

An earlier internal design note (docs/dev/architecture/foundation/monorepo-tooling-skeleton.md) describes a single @helipod/contracts package holding all the cross-package interfaces. That package was never built. The repo evolved differently as it grew. What actually shipped:

  • The storage interface (DocStore) lives in packages/docstore, not contracts.
  • The value/validator/schema system (Value, v, defineSchema) lives in packages/values.
  • The error hierarchy (HelipodError) lives in packages/errors.

If you're reading an internal doc that mentions @helipod/contracts, mentally substitute the real names above. The idea (pure, dependency-free contracts at the bottom of the graph) is exactly what shipped, just under different package names.

The mental model: layers, bottom to top

The single most useful thing to internalize about this repo is that packages form layers, and a layer may only depend on the layers below it, never above. Once you know the layers, you can guess which package a piece of code lives in before you even open a file.

From the bottom up:

  1. Pure contracts: values, errors, id-codec, index-key-codec. No dependencies on anything else in the repo. These define the vocabulary everyone else speaks: what a Value is, what an Id looks like, how errors are shaped.
  2. The storage seam: docstore (the interface) plus its adapters docstore-sqlite, docstore-postgres, docstore-do-sqlite, and docstore-d1 (the implementations). This is where "which database am I talking to" gets decided, and nowhere else. docstore-d1 is the odd one out worth knowing about: it's the first schema-ful adapter, laying documents out relationally (a real column per field, real CREATE UNIQUE INDEX) on Cloudflare D1, where the other three share the schemaless MVCC-log layout.
  3. transactor: the single-writer transaction manager. It assigns commit order and runs optimistic-concurrency retries.
  4. query-engine: executes queries, tracks what they read, and handles cursor pagination.
  5. executor: the sandboxed runtime that actually calls your query/mutation/action functions.
  6. sync: the reactive tier. It holds live subscriptions, matches a commit's write set against them, and decides who gets pushed an update.
  7. runtime-embedded (and the newer runtime-cloudflare): a host that wires the layers above into one running process, over a chosen transport (loopback, WebSocket, or a Cloudflare Durable Object).
  8. component: the composition layer that lets optional features (below) plug into a running app.
  9. The top: client (SDK + React hooks), cli (the helipod command), codegen (typed Doc/Id/api), admin (the dashboard's API).

components/ (auth, scheduler, workflow, ...) and ee/packages/ (fleet, ...) both sit above this stack. They're built using component and executor, never the other way around.

values, errors, id-codec, index-key-codec docstore + adapters (sqlite / postgres / do-sqlite / d1) transactor query-engine executor sync runtime-embedded component client / cli / codegen / admin components/ (auth, scheduler, workflow, ...) ee/packages/ (fleet, ...)

You don't need to memorize this chain to be productive. But when you're wondering "where would the code for X live," asking "what layer is X" usually answers it in one step.

The golden rule: dependencies only point one way

Here's the rule the layering above exists to enforce, stated as plainly as possible:

The engine imports interfaces. It never imports a specific database, host, or socket.

Concretely: transactor, query-engine, executor, and sync are allowed to import docstore (the interface), but none of them are allowed to import docstore-sqlite or docstore-postgres (the actual drivers) directly. Only a leaf package (something like runtime-embedded, which is responsible for wiring a real app together) is allowed to pick a concrete adapter.

allowed forbidden engine code (e.g. transactor) DocStore interface SQLite / Postgres driver

A leak out of an adapter is a design bug, caught in CI, not a code-review hope.

Why this matters more than it might seem: package boundaries are the tier-split points. Want to run Helipod on Postgres instead of SQLite? That's a new leaf package (docstore-postgres), not an edit to transactor. Want to run on Cloudflare Durable Objects instead of an embedded process? That's a new leaf (runtime-cloudflare, docstore-do-sqlite), not a rewrite of query-engine.

The engine never learns which database or host it's running on. Because it structurally can't learn that (it never imports the packages that would tell it), the "engine is portable" property survives contributors who don't happen to remember the rule.

See Architecture: runtimes for how this plays out across Tier 0 (single binary) and beyond.

A one-line tour of the load-bearing packages

You won't need all of these on day one, but here's what to reach for when you need it:

PackageWhat it's for
valuesThe value system: Value, v validators, defineSchema/defineTable
errorsThe HelipodError hierarchy: every thrown error knows its HTTP status
id-codecEncodes/decodes document ids (Id<"table"> <-> the stored bytes)
index-key-codecOrder-preserving key encoding, the interval-index matcher, and cursors
docstoreThe storage seam: the DocStore interface everything above it relies on
transactorThe single-writer transaction manager (optimistic concurrency, commit order)
query-engineQuery execution and cursor pagination
executorThe sandboxed runtime that calls your query/mutation/action functions
syncThe reactive tier and the WebSocket wire protocol
runtime-embeddedThe Tier 0 host: wires engine + adapter + transport into one running process
componentComposition: namespaced tables, the driver seam, helipod.config.ts wiring
codegenGenerates the typed Doc/Id/api your app code imports
clientThe framework-agnostic client, plus React hooks (useQuery, useMutation)
cliThe helipod command: dev, serve, deploy, build, codegen
deployThe pluggable DeployTarget seam (serve/cloudflare/docker) behind helipod deploy
blobstore + storageThe byte-storage seam and the always-on ctx.storage file API
adminThe API the dashboard app talks to
receiptsThe Receipted Outbox's server-side TTL reaper: sweeps expired dedup rows off DocStore

If a name isn't in this table, it's either a leaf adapter (docstore-postgres, blobstore-s3, ...) whose job is obvious from its name, test-only scaffolding (test), or a tooling integration (vite, the @helipod/vite plugin that boots the helipod dev backend alongside a Vite dev server).

Components: opt-in mini-backends

components/ holds six packages: auth, authz, scheduler, workflow, triggers, notifications. Each is a self-contained feature with its own tables, functions, and (where relevant) background work. None of them are on by default; a project turns one on by composing it in helipod.config.ts. Under the hood, every one of them is built the same way, on top of the component package's composition seam and the executor's function-calling machinery. Nothing about them is special-cased in the engine.

If you want to build your own, Building a custom component walks through the seams (schema, context, modules, driver, boot, httpRoutes) that defineComponent gives you. They're the same ones auth and friends use.

ee/: the reserved paid-scale area

Three packages live under ee/packages/: fleet (distributed Tier 2 scale-out), objectstore-substrate, and runtime-cloudflare-shard. They're source-available, but under a separate commercial license that does not convert to Apache like the rest of the repo does. This is the "gate scale, not self-hosting" line: single-node self-hosting is free forever, and the ee/ code is what a paid license key unlocks on top of it. See Licensing & the ee/ split for the full story.

The tooling

  • Bun workspaces: Bun is both the package manager and the runtime. The workspace globs (packages/*, components/*, ee/packages/*, apps/*, examples/*, plus benchmarks/convex-comparison and benchmarks/runner) and a shared dependency catalog (pinned versions for things like typescript and vitest, so every package asks for the same version) both live in the root package.json. Internal cross-package dependencies use workspace:*, so they always resolve to the local source, not a published version.
  • Turborepo: turbo.json defines the task graph: build, test, typecheck, lint, dev. The build and typecheck tasks declare "dependsOn": ["^build"], where the ^ means "build my dependencies first"; that's what makes bun run build run every package in the right order automatically. test (and test:e2e) instead depend on ["build"], the package's own build, which together with ^build on build itself still guarantees a package's dependencies are built before its tests run.
  • vitest, run under Node: even though Bun is the primary runtime, the test suite runs under Node via vitest. That means a test that only works with a Bun-specific API won't get caught by the normal bun run test, worth knowing if you're testing something Bun-only.

Rebuild after editing a dependency

Cross-package tests import their dependencies from the built dist/ output, not from src/. If you edit packages/values/src/... and then run packages/query-engine's tests without rebuilding, you're testing against the old compiled values. Run bun run build (or bun run --filter @helipod/values build) after editing a package other tests depend on.

Common commands:

bun install                              # bootstrap the whole workspace
bun run build                            # build every package, topologically
bun run test                             # run all tests (vitest, under Node)
bun run typecheck                        # tsc --noEmit across every package
bun run --filter @helipod/values test  # just one package's tests

See Development setup for the full clone-to-running-tests walkthrough, and System design for why the engine is shaped this way in the first place.

On this page