Skip to content
Reef's board: four columns of issue cards, labels, assignees, and a live pill.
Reef, the example app this site is built around: a multi-tenant, realtime issue tracker. Its whole backend — schema, rules, queries, deploy — is under 700 lines. Tour of Reef →

Ramose — the typed, realtime database for Cloudflare

Describe your data in TypeScript. Queries update themselves in every open tab. Each user sees only what your rules allow. One deploy, into your own Cloudflare account.

The schema is TypeScript

Reef’s whole data model is one file. Fields have types, references point at other records, and a list is just cardinality: “many”. Your app, your rules and your deploy import it — no codegen, no migration step.

Define your data →

examples/reef/src/domain/schema.ts:29-46 (annotated)
export const Issue = Ramose.Namespace("issue", {
title: Ramose.Attr(Schema.String),
status: Ramose.Attr(Schema.String),
priority: Ramose.Attr(Ramose.Long),
// …
assignee: Ramose.Attr(Ramose.Ref(() => User)),
labels: Ramose.Attr(Ramose.Ref(() => Label), { cardinality: "many" }),
privateNote: Ramose.Attr(Schema.String, {
doc: "visible to the admin class only",
}),
});
// issue.add(Issue.priority, "high") does not compile:
// priority is a number.

One query, always current

The board is one query, read with useLive. Drag a card in one window and every other window moves it too — no WebSocket code, no refetch anywhere in Reef.

Ramose is built on Effect. In React you rarely see it — the hooks run it for you. Effect in five minutes →

Live queries →

Two Reef windows stacked; a card dragged to Done in the top window moves in the bottom one too
The live pill pulses on every update. · src/app/screens/BoardScreen.tsx:230-234
examples/reef/src/domain/queries.ts:66-68 · app/screens/BoardScreen.tsx:230-232
export const boardQuery = Ramose.query(Issue)
.orderBy(Issue.rank, "asc")
.select(boardShape);
// …
const db = useDb(slug, Reef);
const board = useLive(db, boardQuery);

Permissions are part of the database

One policy says who may read and write each field. The buttons are just polite — the server refuses the write.

Because the policy and the shapes your screens read are both values, Ramose.Policy.compile(policy, { pulls }) checks them against each other at deploy time — tighten a rule a screen depends on and the deploy fails, rather than that screen quietly emptying for one customer.

Ramose verifies sign-in tokens; it never issues them. Bring Better Auth (Reef does), Clerk, Auth0, WorkOS — any provider that publishes signing keys.

Permissions →

examples/reef/src/domain/policy.ts:49-61 (comment simplified)
issue: {
read: P.allow(anyone),
create: P.allow(editor),
add: P.allow(ownIssue),
retract: P.allow(ownIssue),
retractEntity: P.allow(ownIssue),
preset: [P.preset(Issue.creator, P.principal)],
attrs: [
// Only admins receive this field; the server strips it for others.
P.attr(Issue.privateNote, { read: P.allow(admin) }),
],
},
A viewer's board: red toast retract denied on :issue/status; the card snapped back
A viewer forced a drag. The server answered Unauthorized; the card snapped back. · src/app/screens/BoardScreen.tsx:241-245

Also built in

One database per customer, for free

Typing a workspace name in Reef creates a fresh, isolated database from the browser. No deploy, no migration, no per-customer server.

Reef's workspaces screen with a New workspace form
One install, one write, one new database. · src/app/mutations.ts:33-36

One database per customer →

Nothing is overwritten

Drag the slider and the board becomes what it was after write number t. Same query, one extra argument, nothing copied.

Reef's board with the time-travel slider open
Pick a version; the board re-reads at it. · src/app/screens/BoardScreen.tsx:448-453

Time travel →

Instead of what?

The honest trade against what you’d use otherwise:

Instead ofSame ideaWhat’s different, and what you give up
Convexqueries that re-run themselvesruns in your Cloudflare account; a database per customer is a function call. No hosted dashboard, scheduled functions or file storage.
Supabaserules enforced by the databaserules per field, deny by default, checked against your screens at deploy. No SQL; bring your own sign-in and file storage.
Instant / Firebaseread and write from the browser, it stays live, per-user rulestyped schema in TypeScript; one deploy you own. No console, no managed infra, no offline SDK.
D1 + Drizzle, or a Durable Object + SQLitethe same Cloudflare primitives, your accountalready assembled: live fan-out, per-customer routing, permissions, history. You give up SQL.
Postgres + an ORMtyped accessno migration rewrites old data; live queries and history come standard. No SQL, no planner, no cross-database joins.

What Ramose does not have: SQL, or joins across databases · a hosted dashboard · its own sign-in · file storage · scheduled jobs.

Full comparison →

How it works

Three parts, one deploy.

writes

One writer per database

Writes commit in order; no conflicts, no “eventually”.

storage

Storage keeps every version

A write is durable before your call returns; nothing is rewritten, so yesterday is still readable.

reads

Reads run next to your users

On copies, so a slow read never blocks a write.

Pre-release·no Ramose bill: it runs in your Cloudflare account, so you pay Cloudflare for what you use·669 tests run on every pull request and on master·built in the open — GitHub

Try it in two lines

Install it, describe your data, watch every tab keep up.

Try it
mkdir my-todos && cd my-todos
bun add ramose react react-dom

Fifteen minutes from an empty folder to a realtime todo app running on your laptop — no Cloudflare account needed. Getting started writes every file with you. Want to read a finished app instead? Reef is an issue tracker in 680 lines.