Concepts
Documents, versions, labels and trees
WREN (usewren) is a versioned JSON store that can also serve static sites. Four ideas make that work: nothing is overwritten, labels are pointers you move, trees map URLs to documents, and the version a visitor sees is decided when they read, not when you write.
The model
Everything in WREN is a document: a JSON object or a binary file (HTML, CSS, an image) with a stable id. Each write adds a version. Labels such as published or preview point at one version of a document. A tree gives documents URL paths like /index.html.
current_version are pointers into an append-only history. Here visitors with a published filter see v2, while the latest write is v4.Documents and versions
- Every write creates a version.
POST /api/v1/{collection}creates a document at version 1.PUT /api/v1/{collection}/{id}(JSON, or multipart for files) adds version n+1 to the same document. UsePUTto update; a secondPOSTcreates a separate document. - Versions are immutable. List them with
GET …/{id}/versions, read one with…/versions/{n}, compare two with…/diff?v1=&v2=. - Rollback moves forward.
POST …/{id}/rollback/{n}writes a new version with the content of version n, so the history stays intact. current_versionis the newest version. Reads without a label return it.- Writes are transactional. A version, its label change or its tree assignment either commits fully or not at all.
Natural keys. If your data has its own identifier (a tournament id, a URL slug), declare it as the collection's naturalKey and write with PUT /api/v1/{collection}/by-key/{value}. That call creates the document the first time and adds a version afterwards, so re-running an import never creates duplicates.
Labels
A label is a named pointer from one document to one of its versions. Each document has at most one version per label name, and you can invent any names you like: published, preview, staging, v1.
- Set or move a label with
POST /api/v1/{collection}/{id}/labels {"label":"published","version":2}. Leave outversionto point at the current version. - Read through a label with
?label=published. A document without that label is treated as not there (404), so readers never fall back to a draft. - Labels are per document, not global. "The site's published version" means "every document in the tree, read through its
publishedlabel". Promote moves that label for a whole tree at once.
Trees
A tree is a named set of paths, each pointing at a document: PUT /api/v1/tree/mysite/index.html {"documentId":"7f3a…"}. This is how a collection of files becomes a website, and how JSON documents get readable URLs.
- A path points at a document, not a version. The version is picked when someone reads it (next section). That is why a deploy can upload new versions without changing what visitors see.
- One document can appear at several paths and in several trees.
- Removing a path (
DELETE /api/v1/tree/…) leaves the document and its history alone. - Reading a path returns the file's bytes. With
Accept: application/jsonyou get the node instead: document id, collection and children.
How a URL resolves to a version
The same rules apply to collection reads (GET …/{collection}, …/{id}, …/raw, _query). On public URLs the rule's label filter always wins, so adding ?label=draft to a public link can't reveal unpublished work.
Public vs private
Nothing is public until you say so. Making something public means adding a permission rule for the principal * (anyone) on a tree or collection:
POST /api/v1/permissions
{"principal":"*", "resource":"tree:mysite", "access":"read", "labelFilter":"published"}| URL family | Shape | Auth | Org comes from |
|---|---|---|---|
| Private API (read + write) | /api/v1/{collection}…, /api/v1/tree/… | Bearer key or cookie | the key / session |
| Public data (read-only) | /api/v1/orgs/{slug}/{collection}… | none | {slug} in the URL |
| Public site (read-only) | /orgs/{slug}/tree/{tree}/{path} | none | {slug} in the URL |
- Public URLs are read-only and ignore any auth header. Write through the private API.
- Access is granted per whole tree or collection. To keep a page private, put it in a different tree.
- Find your slug with
GET /api/v1/me. Everything public is listed at/api/v1/projectsand/orgs/{slug}/llms.txt.
Deploy, preview, promote
Put the pieces together and you get a release workflow without a staging server. Files and JSON data go through it the same way.
preview. Promoting moves published. The public rule's labelFilter: "published" is what makes visitors wait for step 3.wren deploy ./dist --tree mysite --label preview --public # first time: creates the public rule with labelFilter "published" # check the preview with your key: curl -H "Authorization: Bearer $WREN_API_KEY" https://<host>/api/v1/tree/mysite/index.html?label=preview wren promote mysite --from preview # go live wren promote mysite --label published --from v41 # roll back: point published at an older release label
No labelFilter, no preview. If the public rule has no label filter, visitors always get current_version, so every deploy is live immediately and labels don't change what they see.
Why promote is atomic
wren promote calls POST /api/v1/tree/{name}/_promote {"label":"published","from":"preview"}. The server finds every document in the tree, picks the version that carries from (or the current version), and moves the label on all of them inside one database transaction:
- No half-promoted site. A reader sees either the old set of labels or the new one, never
index.htmlfrom the new release withapp.jsfrom the old one. - All or nothing. If any document can't be promoted (for example, you lack write access to one of the collections), nothing changes.
- Cheap. Only label rows change. No bytes are copied, so promoting a large site takes about as long as promoting a small one.
- Reversible. Old versions are still there. Rolling back is another promote that points
publishedat earlier versions.
Because a label is per document, you can promote a single document too (POST …/{id}/labels). Tree promote is the "release everything together" button.
CDN caches. Public responses can be cached for up to 60 seconds before they're revalidated. WREN purges affected URLs on promote when a purge backend is configured. Otherwise, allow a minute, or add ?v=<hash> to asset URLs.
Collections vs trees
A collection is where documents live and how you query them: list, filter with where, project with select, aggregate with _query, or precompute with materialized queries. A tree is how you address documents by path. The same document can be queried in its collection and served at a tree path.
- Site files: a binary collection (
mysite-assets) plus a tree (mysite).wren deploysets up both. - Structured data: a JSON collection with a schema and natural key, read by your pages through the public data URL or with
wren.js. - Both together: put a few JSON documents in the site's tree too (e.g.
/data/standings.json), and one promote releases pages and data at the same moment.
Next steps
- Case study: a public tournament tracker: how these ideas hold up in a real project, as built and as we'd build it today
- Deploy tutorial: deploy, preview and promote a site in ten minutes
- API reference and llms.txt: every endpoint, in a form both people and agents can use