Serializable by construction
Mutations run one at a time inside a SQLite transaction. No conflict detection, no retry loops, no write skew to reason about.
BurrowDB is a reactive backend in a single Bun process. You write queries, mutations and actions in TypeScript, and every screen that depends on the data gets fresh results over WebSocket. It runs in a Bunny Magic Container, keeps the data in Bunny Database, and speaks the Convex API.
// burrow/messages.ts export const list = query({ args: { channel: v.string() }, handler: (ctx, { channel }) => ctx.db.query("messages") .withIndex("by_channel", (q) => q.eq("channel", channel)) .collect(), }); export const send = mutation({ args: { channel: v.string(), body: v.string() }, handler: (ctx, args) => ctx.db.insert("messages", args), }); // src/Chat.tsx: live, no refetch const messages = useQuery(api.messages.list, { channel });
The hard parts of a reactive backend become plain code when a single server holds the data. That is the trade: one node, one region, and far fewer moving parts.
React hooks or a plain client. Subscribes to queries, calls mutations.
One Bun process in a Magic Container. Runs your functions under one lock and keeps a local SQLite copy for reads.
The durable copy. A mutation counts only once it is written here.
Mutations run one at a time inside a SQLite transaction. No conflict detection, no retry loops, no write skew to reason about.
While a query runs, BurrowDB records the index ranges it read. After a commit it re-runs only the live queries a write touched, and every result it pushes carries the same version, so a screen never mixes old and new.
Each mutation reaches Bunny Database in one atomic batch before its promise resolves. If that fails, nothing changes. The local copy is rebuilt at startup, so there is no volume to manage.
A project keeps its backend in one folder. Every exported function becomes an endpoint, typed end to end.
messages.ts exports list, the client calls api.messages.list.v.*; ids are checked against the database.// burrow/schema.ts export default defineSchema({ messages: defineTable({ channel: v.string(), author: v.string(), body: v.string(), edited: v.optional(v.boolean()), }).index("by_channel", ["channel"]), });
// burrow/messages.ts export const recent = query({ args: { channel: v.string() }, handler: (ctx, { channel }) => ctx.db .query("messages") .withIndex("by_channel", (q) => q.eq("channel", channel)) .order("desc") .take(50), }); export const send = mutation({ args: { channel: v.string(), body: v.string() }, handler: async (ctx, args) => { const id = await ctx.db.insert("messages", { ...args, author: "me" }); // written in the same transaction: exists only if send commits await ctx.scheduler.runAfter(0, internal.notify.push, { id }); }, });
// src/Chat.tsx export function Chat({ channel }: { channel: string }) { const messages = useQuery(api.messages.recent, { channel }); // live const send = useMutation(api.messages.send); if (messages === undefined) return <Spinner />; return ( <> {messages.map((m) => <Message key={m._id} {...m} />)} <Composer onSend={(body) => send({ channel, body })} /> </> ); }
The public API mirrors Convex: query, ctx.db.query().withIndex(), v.*, _generated. Moving a project is an import, a flag and a deploy.
Export from Convex, then load the snapshot. Documents keep their ids, and stored files come along.
$ npx convex export --path snap.zip \ --include-file-storage $ burrow import snap.zip
Flag the app "convex": true and your functions keep importing convex/*. The Vite plugin does the same for the front end.
// host.json
"shop": { "project": "../shop",
"dir": "convex", "port": 3301,
"convex": true }
One image for one app or several. Run it on Magic Containers and point your domain at it.
$ burrow bundle host.json --push # then: Magic Containers → your image
e-croatie.com moved from Convex to BurrowDB with its backend unchanged apart from the imports.
A consistent snapshot every day to a private storage zone, kept 30 days. burrow restore brings one back.
One port per app, one domain per port. Commits are compare-and-set, so moving an app to another container never loses a write.
/_burrow/health for the platform; add ?check to probe the database, the storage zones and the age of the last backup.
Bytes in a private Bunny storage zone, served through signed links. Metadata lives in the database like any document.
OpenID Connect and custom JWT providers. @convex-dev/auth runs as is: sign-up, sign-in, token refresh.
Scheduled mutations run exactly once, scheduled actions at most once. Crons in UTC, jobs survive restarts.
Webhooks and REST endpoints with httpRouter. Bearer tokens reach ctx.auth like everywhere else.
After a restart, clients reconnect and re-subscribe without a line of code on your side.
One process is a choice. Here is what it costs today, so you can decide before you move.
If the server stops, requests wait until it restarts. Clients reconnect on their own. Put the container and the database next to your users.
Each commit is a round trip to Bunny Database: roughly 150 to 200 writes per second per app today. Grouped writes are next on the list.
Databases are capped in size during the beta (1 GB today). BurrowDB runs its own daily backups on top.
No. BurrowDB is an independent project that implements the same public API, so Convex projects can move without a rewrite. Convex is a trademark of its owner.
In production, the data lives in Bunny Database. The server itself is a Bun process in a container; Magic Containers is where we run and test it. In development, everything sits in a local SQLite file.
With the container and the database in the same region, a mutation commits in about 4 ms on the server. Reads come from the local SQLite copy. Measured from Slovenia against a production deployment, HTTP actions answered in 33 ms at the median.
burrow import reads a Convex export: every document keeps its id, so references between tables still hold, and stored files are imported with their metadata.
Nothing committed is lost: a mutation only resolves once Bunny Database has it. While the process restarts, clients wait, then reconnect and re-subscribe. The local copy is rebuilt from the database at startup.
BurrowDB is in early access. Tell us about your project and we will help you move it.
Tell us what you run on Convex today. We will tell you what moves as is, what needs work, and help you through the first deploy.