Skip to content

Tour of Reef

Reef is a small Linear-style issue tracker: sign up, create a workspace, and get a kanban board with drag-and-drop, an issue panel, invitations with three roles and a time-travel slider. Every change reaches every open tab within about a second, and each workspace is its own database. This page maps each screen to the code behind it; it works as a first page.

Reef lives in the Ramose repository rather than on npm, so this one starts from a clone:

Terminal window
git clone https://github.com/tvanhens/ramose && cd ramose && bun install
bun run dev:reef

Wait for three URLs — the Ramose server on :1337, the sign-in Worker on :1338, the app on :5173 — then open http://localhost:5173 and Create account. It is verified on the spot, with no email. Then New workspace, name it, and Add sample issues.

To build your own app instead, nothing is cloned: Getting started installs Ramose from npm.

FileLinesWhat it is
src/domain/schema.ts76what exists — the schema
src/domain/policy.ts77who may — the permissions
src/domain/queries.ts95what screens read
src/infra/resources.ts74the Ramose server, declared
src/infra/api.ts139the sign-in Worker (Better Auth)
rank.ts · roles.ts · shared.ts · alchemy.run.ts219drag ordering, role names, shared constants, the stack

680 lines by wc -l, comments included — everything in src/domain/ and src/infra/, plus the deploy file. Zero lines of WebSocket server, REST endpoints, migrations, auth middleware or refetch code.

Reef's sign-in card headed Welcome back, with email and password fields over a soft glow, above Live, Multi-tenant and Time travel notes
Better Auth owns identity. Ramose only verifies the sign-in token it mints. · src/infra/api.ts:100-119

Better Auth (the sign-in library) owns accounts; Ramose verifies the sign-in token — a signed token the server checks on every request and never issues (glossary). Two plugins do it:

examples/reef/src/infra/api.ts:100-119
jwt({
jwt: {
issuer: REEF_AUTH.issuer,
audience: REEF_AUTH.audience,
expirationTime: `${REEF_AUTH.ttl}s`,
},
}),
// …
ramoseToken({
auth: REEF_AUTH,
policy: compiledPolicy(),
classOf: orgClassOf(),
}),

Sign in and roles

Reef's workspace picker listing two workspaces, Coral Reef Divers and Kelp Forest, each labelled with its own database name, above a New workspace form
Each workspace is its own database. Create runs install() from the browser. · src/app/mutations.ts:28-45

A workspace is one customer’s isolated data — one database (glossary). Create installs the schema into a fresh database and seeds it:

Ramose is built on Effect. In React you rarely see it — the hooks run it for you; inside db.transact you write yield*. (Effect in five minutes)

examples/reef/src/app/mutations.ts:32-40
Effect.gen(function* () {
yield* db.install();
yield* db.transact(function* (tx) {
const user = yield* tx.entity();
yield* user.add(User.sub, me.id);
yield* user.add(User.name, me.name);
yield* user.add(User.email, me.email);
for (const seed of SEED_LABELS) {
const label = yield* tx.entity();

Opening one mounts a provider keyed by its name; switching swaps the connection:

examples/reef/src/app/App.tsx:146-150
<RamoseProvider
key={open.workspace.slug}
url={RAMOSE_URL}
token={open.workspace.token}
>

One database per customer

Reef's board: four columns of issue cards with labels and assignees, a live pill in the header
One live query, sorted by rank; the columns are rows.filter(status). The pill counts updates the server pushed. · src/app/screens/BoardScreen.tsx:230-234

The board is one live query — a query that re-runs itself whenever the database changes (glossary):

examples/reef/src/domain/queries.ts:66-68
export const boardQuery = Ramose.query(Issue)
.orderBy(Issue.rank, "asc")
.select(boardShape);
examples/reef/src/app/screens/BoardScreen.tsx:230-234
const db = useDb(slug, Reef);
const board = useLive(db, boardQuery);
const people = useLive(db, peopleQuery);
const labels = useLive(db, labelsQuery);

Live queries

Two Reef windows stacked; a card dragged in the top one moves in the bottom one
One drag, two facts written — and every other window on that workspace follows within about a second. · src/app/screens/BoardScreen.tsx:264-267

A drag is one write of two facts: new status, new position. The old status is replaced automatically. (A write is one all-or-nothing change — Ramose calls it a transaction; glossary.)

examples/reef/src/app/mutations.ts:113-122
export const moveIssue = (
db: ReefDb,
issueId: number,
status: Status,
rank: number,
) =>
db.transact(function* (tx) {
yield* tx.add(issueId, Issue.status, status);
yield* tx.add(issueId, Issue.rank, rank);
});

Then the write reaches the Ramose server, which checks the caller’s role against the rules and commits. Every open tab hears that the database moved and re-runs its live queries. Nothing in Reef subscribed to anything. (The server is the one Cloudflare Worker that serves all your databases; Ramose’s code calls it the peer — glossary.)

Write data

The issue side panel: title, status, priority, assignee, label chips, description, admin note and a comment thread
Status, assignee and labels come from the live board row; title, description and note ride one live pull. · src/app/components/IssueDetail.tsx:258-266

The panel reads one record with usePull — by id, with the fields you choose (glossary). A per-issue live query loads the comments:

examples/reef/src/app/components/IssueDetail.tsx:258-266
const extra = usePull(db, { id: issueId }, issueExtraShape).rows ?? null;
useEffect(() => {
if (extra === null) return;
setTitleDraft(extra.title);
setDescriptionDraft(extra.description ?? "");
setNoteDraft(extra.privateNote ?? "");
}, [extra]);
const comments = useLive(db, commentsQuery, { issueId });

The board’s shape is the fields you ask for (glossary). It follows references, so a card gets creator, assignee and labels inline:

examples/reef/src/domain/queries.ts:24-34
export const boardShape = {
id: Issue.id,
title: Issue.title,
status: Issue.status,
priority: Issue.priority,
rank: Issue.rank,
createdAt: Issue.createdAt,
creator: Issue.creator.select(personShape),
assignee: Issue.assignee.select(personShape).optional,
labels: Issue.labels.select(labelShape),
} as const;

Read data

The Invite dialog with an email field and the role select open on viewer
Roles live in Better Auth; the token carries one; the policy enforces it. · src/domain/policy.ts:26-32

A role is admin, member or viewer — a name carried in the token that the rules refer to (glossary). The policy names them once:

examples/reef/src/domain/policy.ts:26-32
const anyone = P.or(P.class("admin"), P.class("member"), P.class("viewer"));
const editor = P.or(P.class("admin"), P.class("member"));
const admin = P.class("admin");
/** `member` may touch an issue they created; `admin` never reaches the rules. */
const ownIssue = P.and(P.class("member"), P.eq(Issue.creator, P.principal));
const ownComment = P.and(P.class("member"), P.eq(Comment.author, P.principal));
A viewer's board with a red toast reading retract denied on :issue/status
A viewer dragged a card. The server refused; the toast is all the UI does. · src/app/screens/BoardScreen.tsx:241-245
examples/reef/src/app/screens/BoardScreen.tsx:243-245
const { run } = useTransact({
onError: (error) => toast("error", errorMessage(error)),
});

Permissions

The issue panel as a member: the Admin note field shows a masked-for-member tag and an empty box
Members never receive the note. The server drops the field itself. · src/domain/policy.ts:56-60

One field has a narrower read rule, so shapes ask for it as optional — asking for it as required fails at deploy time:

examples/reef/src/domain/queries.ts:40-46
export const issueExtraShape = {
title: Issue.title,
description: Issue.description.optional,
// Read-masked for member/viewer (policy.ts): must be `.optional`, so for
// them the row survives and the field is simply absent.
privateNote: Issue.privateNote.optional,
} as const;
examples/reef/test/policy.test.ts:70-75
test("a masked attribute pulled as required is a compile error", () => {
const badShape = { note: Issue.privateNote };
expect(() =>
Ramose.Policy.compile(policy, { pulls: [...allShapes, badShape] }),
).toThrow(/privateNote/);
});

Permissions · Policy

Reef's board under a blue Time travel bar reading db.asOf(27) of 42, the header pill paused, and fewer cards in every column than the live board
The same board query, read as of an earlier version. Nothing was copied. · src/app/screens/BoardScreen.tsx:448-460

Every write gets a version number t, in order (glossary). The slider picks one; the same boardQuery runs against db.asOf(t), and db.history lists deleted issues too:

examples/reef/src/app/screens/BoardScreen.tsx:448-454
const maxT = useBasis(db);
const [scrubbed, setScrubbed] = useState<number | null>(null);
const t = scrubbed ?? maxT;
// Until the basis lands, read the live view — the same rows the board
// already shows — so the hook order never varies.
const past = useQuery(t === undefined ? db : db.asOf(t), boardQuery);
const everything = useQuery(db.history, everyIssueEverQuery);
examples/reef/src/domain/queries.ts:84-88
/** Over `db.history` this also returns issues that no longer exist. */
export const everyIssueEverQuery = Ramose.query(Issue).select({
id: Issue.id,
title: Issue.title,
});

Time travel

One file declares the server, its storage and the sign-in Worker; bun run dev:reef runs it locally.

The Ramose server, declared
examples/reef/src/infra/resources.ts:37-52
const Store = Cloudflare.R2.Bucket("Store");
const Transactor = Cloudflare.DurableObject("TransactorDO", { className: "TransactorDO" });
const Replica = Cloudflare.DurableObject("QueryReplicaDO", { className: "QueryReplicaDO" });
export const RamoseWorker = Cloudflare.Worker("Peer", {
main: import.meta.resolve("ramose/worker"),
compatibility: { date: "2026-03-17", flags: ["nodejs_compat"] },
dev: { port: DEV_PEER_PORT },
env: {
STORE: Store,
TRANSACTOR: Transactor,
REPLICA: Replica,
...Ramose.authEnv({
policy: compiledPolicy(),
auth: REEF_AUTH,
internalSecret: process.env.RAMOSE_INTERNAL_SECRET,
  • A WebSocket server, or any pub/sub fan-out
  • REST endpoints for issues, comments, labels
  • Migrations, or an ORM model
  • Auth middleware — the server verifies every request itself
  • Per-customer provisioning — a workspace is ramose.db(name).install()
  • Refetch or cache-invalidation code — there is none in Reef

Smaller examples: examples/todos, the todo list you build in Getting started, and examples/kv-style (Use it from a Worker).