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.
Run it
Section titled “Run it”Reef lives in the Ramose repository rather than on npm, so this one starts from a clone:
git clone https://github.com/tvanhens/ramose && cd ramose && bun installbun run dev:reefWait 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.
The whole backend in 700 lines
Section titled “The whole backend in 700 lines”| File | Lines | What it is |
|---|---|---|
src/domain/schema.ts | 76 | what exists — the schema |
src/domain/policy.ts | 77 | who may — the permissions |
src/domain/queries.ts | 95 | what screens read |
src/infra/resources.ts | 74 | the Ramose server, declared |
src/infra/api.ts | 139 | the sign-in Worker (Better Auth) |
rank.ts · roles.ts · shared.ts · alchemy.run.ts | 219 | drag 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.
Sign in
Section titled “Sign in”
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:
jwt({ jwt: { issuer: REEF_AUTH.issuer, audience: REEF_AUTH.audience, expirationTime: `${REEF_AUTH.ttl}s`, }, }), // … ramoseToken({ auth: REEF_AUTH, policy: compiledPolicy(), classOf: orgClassOf(), }),Workspaces
Section titled “Workspaces”
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)
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:
<RamoseProvider key={open.workspace.slug} url={RAMOSE_URL} token={open.workspace.token} >The board
Section titled “The board”
src/app/screens/BoardScreen.tsx:230-234 The board is one live query — a query that re-runs itself whenever the database changes (glossary):
export const boardQuery = Ramose.query(Issue) .orderBy(Issue.rank, "asc") .select(boardShape); const db = useDb(slug, Reef);
const board = useLive(db, boardQuery); const people = useLive(db, peopleQuery); const labels = useLive(db, labelsQuery);Move a card
Section titled “Move a card”
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.)
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.)
The issue panel
Section titled “The issue panel”
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:
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:
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;Invite a viewer
Section titled “Invite a viewer”
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:
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));
src/app/screens/BoardScreen.tsx:241-245 const { run } = useTransact({ onError: (error) => toast("error", errorMessage(error)), });The admin note
Section titled “The admin note”
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:
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; 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
Time travel
Section titled “Time travel”
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:
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);/** Over `db.history` this also returns issues that no longer exist. */export const everyIssueEverQuery = Ramose.query(Issue).select({ id: Issue.id, title: Issue.title,});The deploy file
Section titled “The deploy file”One file declares the server, its storage and the sign-in Worker; bun run dev:reef runs it locally.
The Ramose server, declared
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,What you didn’t have to write
Section titled “What you didn’t have to write”- 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
Read the source
Section titled “Read the source”src/domain/schema.ts— what existssrc/domain/policy.ts— who maysrc/domain/queries.ts— what screens read
Smaller examples: examples/todos, the todo list you build in Getting started, and examples/kv-style (Use it from a Worker).