How Ramose thinks about data
This page is the mental model behind Ramose, in plain words, for someone who has run the Quickstart and wants the guides to make sense. It assumes nothing about databases.
A database is a pile of facts
Section titled “A database is a pile of facts”A record is one thing in the database — an issue, a user (glossary). A field is one named, typed slot on it, like issue.title (glossary). Group the fields and you have a record type — a kind of record, like a table; Ramose calls it a namespace (glossary).
Ramose does not store an issue as a row. It stores facts: issue 17’s title is “Fix login”, issue 17’s status is “todo” (glossary). A record is the set of facts about one id. That is the whole storage model; everything on this page follows from it.
Nothing is overwritten
Section titled “Nothing is overwritten”Move issue 17 to Done and Ramose does not edit the status fact. It adds a new one (status is “done”) and marks the old one as gone. Both stay on file.
A write is one all-or-nothing group of changes; Ramose calls it a transaction (glossary). Every write gets the next version number t (glossary). The picture above is the database at version 9. Read it at version 8 and the status is still “todo”. That is all Reef’s time-travel slider does.
src/app/screens/BoardScreen.tsx:448-454 Reading means asking at a version
Section titled “Reading means asking at a version”Every read happens at a version. You pick which one:
- Now, and again on every change — a live query re-runs itself whenever the version moves and hands your screen new rows (glossary). Reef’s board is one.
- As it was —
db.asOf(t)reads at an earlier version (glossary). Reef’s slider is one. - Including what was removed —
db.historyalso returns facts marked gone (glossary). Reef’s “Deleted, still in history” strip is one.
The query is the same value in all three cases. Time travel shows the code.
A database is a name
Section titled “A database is a name”A database is a named, isolated pile of facts (glossary). ramose.db("coral-team", Reef) names one; nothing is created until the first write lands. Each name has its own writer — the one thing that commits its writes, in order (glossary) — so two databases never wait on each other.
Because a database is only a name, one per customer is a function call, not a deployment. Every Reef workspace is a database. One database per customer walks through it.
Rules live with the data
Section titled “Rules live with the data”The policy is a value that says which roles may read, create, change or remove which fields (glossary). The server checks it on every write and filters every read with it, so a rule holds no matter which screen or script is asking. It is deny by default: forgetting a rule closes a door, never opens one.
Rules are about facts, so they can be precise: in Reef, a member may change an issue they created, and only admins ever receive the admin note. Because the policy and the fields your screens read are both values, the deploy checks them against each other — see the deploy-time check.
The schema is a value
Section titled “The schema is a value”The schema is your data model as one TypeScript value; Ramose calls it a catalog (glossary). It is written into a database as an ordinary write, db.install(). Adding a record type or a field is adding facts about the schema, at a version, like any other write. Nothing rewrites old data, so there is no migration step that can fail halfway. Changing a schema later has the details.
Where the ideas come from
Section titled “Where the ideas come from”These ideas — facts instead of rows, a database you can read at any earlier version, one writer and many readers — come from Datomic, which inspired Ramose. You do not need to know Datomic to use Ramose. If you do know it: Ramose re-homes the shape on Cloudflare, with a typed query builder instead of a query language, no JVM and no create-database call.
If you know Datomic: names, side by side
| Datomic | Ramose |
|---|---|
datom [e a v t] | a fact; the same order on t |
| schema transacted as data | a schema of TypeScript values; db.install() or Ramose.Database writes it, safe to repeat |
d/transact with tx-data | db.transact(function* (tx) { … }) — a generator of add / retract / retractEntity |
| tempids resolved in the transactor | tx.entity() — the new ids are not returned; query for them |
:db.unique/identity, lookup refs | { unique: "identity" }, [User.sub, "…"] |
:db.cardinality/many | { cardinality: "many" } |
d/pull | db.pull({ id }, shape), same nesting; .optional for absence, .orDefault(v) for a pull :default |
| Datalog query | a typed builder — Ramose.query(Issue).where(…).orderBy(…).limit(n).offset(n).one().select(…), run by db.q / db.live; the predicates are listed under Filter. Aggregates / groupBy / .after on the builder; no rules, no string escape hatch |
d/as-of, d/history | db.asOf(t), db.history — pure functions to a read-only view |
| transaction entity | report.txEid |
d/filter | a compiled policy, evaluated per request against the token’s claims |
| peer library, JVM | one Cloudflare Worker you declare, not write |