concile
Reference

FAQ

Straightforward answers about what concile is, what it isn't, and what we haven't built yet.

Is concile production-ready?

If you're running a single node, absolutely! We use the reactive engine, client SDK, dashboard, and CLI end-to-end all the time.

This covers a ton of features. You get schemas, typed queries, mutations, and action functions. We've got transactional execution running through a DatabaseAdapter seam, plus WebSocket reactive subscriptions that update precisely by index range rather than refreshing whole tables. On top of that, there are optimistic updates, a durable offline outbox, concile dev/serve, single-binary compilation, and a fully functional docker compose up for self-hosting. Everything ships and is thoroughly tested through the actual CLI server, not just with simple unit mocks.

Our optional components are built with the same rigorous approach. Authentication, notifications, scheduling, crons, durable workflows with saga or compensation, change triggers, and authorization are all exercised through actual entrypoints rather than just in-process tests.

Scaling across multiple nodes is a bit newer and falls under a different license. Things like concile serve --fleet (which gives you Tier 2, Postgres-backed write scale-out and live failover) and the Cloudflare-native multi-shard router are ee/ licensed packages, rather than part of the FSL-1.1-Apache-2.0 core. Check out Can I self-host for free? below for more details.

These features definitely work and have solid benchmarks (take a look at How fast is it?), but they are the newest additions to our system. They just haven't seen as much production time as our core single-node setup.

If you are picking up concile today, we recommend starting with single-node self-hosting using either SQLite or Postgres. It is a very safe and heavily tested default that works great for most apps. You can always upgrade to the fleet later if you hit a write-throughput limit or genuinely need failover capabilities.

Check out What is concile? for the complete overview, and look at What's deferred below to see exactly what we haven't gotten around to building yet.

What's deferred or not shipped yet

We want to be totally transparent. You won't find any of these listed as shipped features elsewhere in our documentation.

SQLite vs Postgres: which should I use?

You have two great storage backends to choose from, and they both sit behind the same DatabaseAdapter or DocStore seam. The engine itself doesn't directly import either driver, so switching between SQLite and Postgres is as simple as flipping a flag. You won't ever need to change your application code to swap them.

This is our zero-config default. It's an embedded, MVCC, single-writer store that requires absolutely nothing external to run. We think it is the perfect choice for concile dev and for self-hosted single-node production environments. It is noticeably faster for single-node setups because you skip the network round trips and the fsync-per-commit costs. It runs entirely in-memory and is CPU-bound.

concile serve --dir concile

Both options use a single writer, and this is by design rather than a limitation of Postgres. We purposely commit exactly one mutation at a time on either backend. This keeps conflict validation incredibly cheap and ensures that the commit log's timestamp ordering actually makes sense. Trying to add concurrency just introduces latency because clients end up queueing behind the single writer instead of improving throughput. When we tested the same insert workload, here is what we found:

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

As you can see, Postgres is about ten times slower than SQLite in this specific scenario because it is bound by fsync. The real bottleneck for commits is syncing to the disk, not the engine itself. If you want to scale your writes beyond a single writer, your best bet is to shard or add fleet nodes at Tier 2 as mentioned below. You definitely shouldn't try adding more threads against a single connection.

The best part is that neither backend ever requires an app-schema migration! Both are completely physically schemaless. The tables, fields, and indexes you define in schema.ts simply live as data inside a few fixed internal tables, specifically an append-only MVCC log containing documents, indexes, and some bookkeeping data. These internal tables never change shape as your schema evolves. You will never have to run CREATE TABLE, ALTER TABLE, or write any migration files for either store. Check out our Postgres guide for the full technical breakdown, including our group commit feature. Group commit is turned on by default for Postgres and gives a nice 39 to 58 percent throughput boost, but we leave it off by default for SQLite because it actually slows things down when there is no fsync cost to absorb.

How does concile compare to Convex?

We intentionally modeled our reactive system directly after Convex's public architecture. It works brilliantly. A query records precisely which index ranges it read, a mutation saves a write set, and a subscription only bothers to re-run when a committed write intersects with what it was already looking at. This single elegant mechanism gives you OCC serializability alongside realtime updates, completely eliminating manual cache invalidation. Where we really stand apart is what you can build and do with it:

  • We built it to be self-hostable from day one. You don't have to rely on a managed cloud. Just run docker compose up and you will have the engine, database, and dashboard running together in one container on your own hardware.
  • You get pluggable storage. You can pick SQLite or Postgres, rather than being locked into a single proprietary database.
  • You can compile everything into a single binary with concile build. It neatly packs the engine, your app, and the dashboard into one executable, which is super handy if you are distributing apps via Electron or Tauri.
  • We use native @concile/* imports instead of convex/*. concile is a standalone product, not just a Convex account or a simple drop-in replacement. If you are coming over, you can use concile migrate --from convex as your starting point, which you can read about in Migrate from Convex.
  • We've actually pushed past some of Convex's shipped features. Today, concile includes durable workflows with saga or compensation capabilities, a native Postgres storage adapter, and a durable offline mutation outbox that supports client-supplied IDs.

Two API differences you should know before porting code

First, you won't find ctx.db.patch(...). Instead, you just read the document, use a spread to merge your changes, and then call ctx.db.replace(id, { ...doc, ...changes }). If you use concile migrate, it will actually flag every .patch(...) call and show you this exact fix.

Second, a mutation's client-side promise resolves the moment it commits, rather than waiting for Convex's later flicker-free gate. When you await send(args), it resolves as soon as the server response arrives to confirm the commit, not when an authoritative push finally overtakes the optimistic layer. We did this on purpose. Resolving at the gate-time has some rough edges. A simple transport drop could turn a successfully committed mutation into a rejected promise, and a lost gating frame with no further traffic could leave a promise hanging endlessly. We wanted to avoid those headaches entirely. In real-world usage, this rarely makes a difference because your optimistic update renders instantly when you call the mutation anyway. However, if you are porting code that awaits a mutation and then reads from a local cache, you should remember that our guarantee is simply "committed," rather than "your optimistic guess has been definitively superseded." Check out Optimistic updates for our full reasoning and a couple of side effects you might notice.

We don't just make claims, we measure them. We ran a benchmark on the exact same substrate, putting both backends in Docker containers on the same host and driving them with their native WebSocket clients using identical test code against a matched app:

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

We are right in the same ballpark, and in this specific test, we are running on par with or even slightly faster than the commercial Rust reference we modeled ourselves after. Take a look at Performance for our full scorecard and the caveats that come with it. It's important to note that concile is a clean-room build that we developed by studying Convex's publicly available architecture documentation. We aren't a fork, and we definitely didn't decompile Convex's code.

How does concile compare to Firebase or Supabase?

Firebase and Supabase are both very different types of reactive backends.

  • Firebase bases its realtime model heavily on security rules. These rules directly control client access to raw documents, so there is no transactional function layer sitting between a write and the database. This means your authorization logic gets locked away in a rules DSL rather than living as ordinary code that you can easily unit test.
  • Supabase essentially wraps Postgres in a ring of about a dozen microservices, including things like PostgREST, Realtime, GoTrue, Storage, and Studio. They tie it all together with row-level security and use a WAL-tailing realtime server that runs as a single-threaded path.
  • With concile, all the reactivity stems from one simple mechanism running in a single process. We use deterministic TypeScript functions that record read and write sets, which then intersect at commit time. You won't find a restrictive rules DSL here. Authorization is just normal function code that you can optionally compose using row policies from @concile/authz. You also don't have to manage a massive fleet of services because the entire backend is just one process.

Then there is the deployment side. You can deploy concile anywhere that runs a container, a binary, or a Cloudflare Worker. You just don't get that level of portability with Firebase, which is strictly Google-only. In reality, you don't really get it with Supabase either, since running a dozen-service self-hosted setup is overwhelmingly heavy compared to our simple docker compose up.

Can I self-host for free?

Absolutely! We license concile under FSL-1.1-Apache-2.0, which is the Functional Source License and happens to be the exact same one Convex uses. This means you are completely free to use, modify, and self-host the software at any scale on your own infrastructure. The license only restricts one specific thing, which is offering concile as a competing hosted service. Furthermore, every release automatically converts to plain Apache 2.0 two years after it ships.

It is free forever. This isn't a trial, and we don't do bait-and-switch tactics.

  • Single-node self-host. You get the full engine, including functions, reactivity, workflows with saga, storage, scheduling, actions, httpAction, the Postgres adapter, the single-binary build, and the dashboard. It is fully production-ready for the vast majority of apps.
  • Deploy anywhere. You can run it on your own hardware, a VPS, your cloud of choice, Docker, or even an air-gapped server. It never phones home.
  • Data and code portability. We use plain HTTP and open formats. You can effortlessly move your data in with concile migrate and pull it right back out using concile migrate export and import. You are never locked in.

So what is gated? Multi-node write scale-out. Things like concile serve --fleet with Postgres-backed @concile/fleet and the Cloudflare-native multi-shard router under @concile/runtime-cloudflare-shard live in a separate ee/ directory. They fall under a different commercial license, following the open-core pattern used by GitLab and n8n, rather than using a viral SSPL-style copyleft license.

Right now, we are letting everyone use both of these enterprise features in production entirely for free, with no license key required. Our current goal is to build a great community, not to maximize revenue. The plan for the future is to introduce a paid license key to unlock scale and enterprise capabilities once the demand is there. Even then, the key will only unlock capabilities, never restrict where you deploy. You will always run it on your own infrastructure. We won't force you into a managed cloud, we won't charge metered usage, and we won't do phone-home verification. You will simply use a signed key that checks itself offline at boot, just like n8n and GitLab do.

Not right now. Check out What's deferred above. We deliberately avoid listing search as a feature in our docs, and when you run concile migrate, it will actually flag any Convex .searchIndex(...) or .vectorIndex(...) calls as unsupported instead of faking a translation. If your app genuinely needs search today, you will want to hook up an external service or write a custom adapter.

How fast is it? Let's talk real numbers.

Every performance metric we share is hand-transcribed straight from our runnable benchmark harness in the benchmarks/ directory. We always measure both sides of any comparison under the exact same conditions. Here are the highlights:

metricheadline 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

If you want the complete scorecard covering write throughput, sharding, fleet scale-out, the offline outbox, and Docker capacity tiers, check out Performance. We lay out the honest caveats behind every single number and give you the commands to reproduce them yourself.

What runtime does it use?

Bun is our primary runtime for things like concile dev, serve, and the single-binary compile with bun build --compile. However, Node is fully supported for running the engine, including npm packages and our Node SQLite adapter. The engine itself is completely runtime-agnostic behind its storage and runtime seams. Whether you use concile dev or serve, it doesn't care which runtime you choose as long as that seam is satisfied.

Can I run it on Cloudflare?

Yes! You can use @concile/runtime-cloudflare in two distinctly different architectures. The DO-native path is our first-class deployment target. When you run concile deploy --target cloudflare, it automatically reconciles your wrangler.jsonc bindings like the Durable Object class, the SQLite migration, nodejs_compat, and optional R2, then shells out to wrangler deploy to handle it for you. The Containers path is a bit more hands-on, requiring a manual wrangler deploy of the portable concile serve image.

In this setup, a single Durable Object acts as your entire backend. It contains the OCC writer, DO-SQLite storage under ctx.storage.sql, every hibernatable WebSocket, the subscription index, and a wake alarm. Because the writer and subscription index share the same in-process object, a mutation's reactive fan-out is just a simple function call in the same turn. It avoids messy RPC hops that can mess up ordering. This architectural choice is precisely what guarantees our engine's write-serialization and origin-frontier ordering.

You will also be happy to know that scheduled functions, crons, triggers, and the storage reaper all fire flawlessly on this path. We use a DO alarm via ctx.storage.setAlarm to wake the object up from full hibernation and call runtime.fireDueTimers(). This is the biggest advantage over the Containers path.

forwards every request/upgrade to one DO by name calls Client (HTTP / WebSocket) Worker (stateless router) ConcileDurableObject 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

There are a few limits you should keep in mind. You get 10 GB of DO-SQLite storage per object, a flat billed 128 MB of memory, and a soft ceiling of about 200 to 500 writes per second for a write-heavy single DO. For v1, you only get a single global DO since there is no built-in sharding. If you need sharding, that's available as a separate paid package under @concile/runtime-cloudflare-shard.

Real world measurements

DO-nativeContainers
write latency133 ms~1,500 ms

We measured both of these against real Cloudflare and real R2 recently. The DO-native path is roughly 11× faster for writes because it benefits from co-located DO-SQLite writing, whereas the Containers path suffers an R2 CAS round trip on every single commit.

When to pick which

You should almost always default to the DO-native path. The only exceptions are if you desperately need the portable image's storage options like Postgres that DO-native doesn't support, or if you are deliberately building a request-driven app without a scheduler. Check out Cloudflare for complete setup instructions for both paths, including how to handle wrangler.jsonc, region-pinning hints, and R2-backed file storage.

Does concile lock me in?

Not at all. We have two solid reasons why that is true rather than just a marketing slogan:

  • The exact same app code runs absolutely everywhere. Your schema.ts and concile/ functions run without a single modification whether you are on concile dev, using concile serve with SQLite or Postgres, running a compiled single binary, operating a multi-node fleet, or deploying to either Cloudflare path. Scaling up to a tiered architecture only requires changing deployment configurations and adapters. You never have to touch the functions you wrote.
  • Your data is explicitly portable. You can easily run concile migrate export and concile migrate import to pull a complete point-in-time dump from a running deployment's admin API and push it straight into a fresh one. This pulls every live document, index row, and table-number map. The tool works seamlessly between any two hosts or topologies because every concile store uses the exact same logical MVCC-log shape.
CONCILE_ADMIN_KEY=… concile migrate export --url https://old-host.example.com --out dump.json
CONCILE_ADMIN_KEY=… concile migrate import --url https://new-host.example.com --in dump.json

A few caveats to keep in mind: the import targets a completely fresh deployment, not a merge. It will flat out refuse to run if the target's table numbers don't match the dump's. Right now, it only supports single-shard migration since we haven't built multi-shard migration yet. Also, the dump is a true point-in-time snapshot, not a live streaming copy. Make sure you stop writes on your source for a clean cutover. Since different physical topologies like a Postgres fleet versus a Cloudflare DO-native host naturally store data differently, this export and import step is your bridge between them. Data doesn't magically teleport, but we make sure it is never trapped.

On the licensing side, you get single-node self-hosting, deploy-anywhere flexibility, and total data and code portability for free, forever, under the FSL. There is no trial period and absolutely no bait-and-switch. Take another look at Can I self-host for free? above for the details.

What do you mean by "component"?

A component is simply an opt-in, composable piece of server-side functionality. Things like authentication, notifications, scheduling and crons, durable workflows, change triggers, and authorization are all components. You add them to your project just by listing them in your concile.config.ts, using functions like defineAuth() or defineScheduler(). The beautiful part is that the core engine handling schemas, queries, mutations, and reactivity has no idea what auth or notifications even are.

When you add a component, it brings in its own namespaced tables. This means a table like scheduler/jobs will never accidentally collide with your app's own jobs table. Components also provide a handy ctx.<name> facade in your handlers, internal modules, and sometimes a background driver. We don't hide any of this behind an obscure init wizard. You get to compose exactly the components your app needs, and a component can even declare a dependency on another one, like how defineWorkflow() requires "scheduler". These are all resolved seamlessly at compose time. Head over to the Components overview for a deeper dive.

Where should I go next?

On this page