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.

One document, four versions, three pointers Versions v1 to v4 of index.html sit in a row. The published label points at v2, the preview label points at v4, and current_version also points at v4. Document index.html (id 7f3a…) Every write appends a version. Old versions are never changed or removed. v1Hello v2Hello, world v3Hello, wrld v4Hello, world! published preview current_version A label is one row: (document, label) → version. Moving it never copies data.
Figure 1. Labels and 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. Use PUT to update; a second POST creates 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_version is 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 out version to 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 published label". 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/json you get the node instead: document id, collection and children.

How a URL resolves to a version

Resolving a tree URL A request for a tree path finds the path's document. The version served is: on public URLs, the version named by the public rule's labelFilter, or the current version if the rule has none; on private URLs, the ?label= version if given, otherwise the current version. If the chosen label does not exist on the document, the response is 404. GET …/tree/mysite/index.html path → document 7f3a… Which version? depends on the URL family Public URL /orgs/{slug}/tree/… No auth. Needs a principal "*" read rule. Rule has labelFilter "published"? yes → the version labelled published (none yet → 404, never a draft) no → current_version (latest write) ?label= in the URL is ignored Private URL /api/v1/tree/… Bearer key or session cookie. Your own rule has a labelFilter? yes → that label, always Otherwise ?label=preview given? yes → the version labelled preview no → current_version
Figure 2. The same path can serve different versions to different readers. Visitors get what the public rule allows; you preview other labels through the private API.

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:

http
POST /api/v1/permissions
{"principal":"*", "resource":"tree:mysite", "access":"read", "labelFilter":"published"}
URL familyShapeAuthOrg comes from
Private API (read + write)/api/v1/{collection}…, /api/v1/tree/…Bearer key or cookiethe 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/projects and /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.

Deploy, preview and promote over time Three stages for two documents, index.html and data.json. Before: both at v1, published on v1. After deploy with label preview: both have v2 labelled preview, published still on v1, so visitors see v1. After promote from preview: published moves to v2 on both documents in one transaction. 1 · Live 2 · wren deploy --label preview 3 · wren promote --from preview index.html v1 data.json v1 ◀ published ◀ published index.html v1 v2 data.json v1 v2 ▲ published▲ preview ▲ published▲ preview index.html v1 v2 data.json v1 v2 ▲ published, preview ▲ published, preview Visitors see v1 Visitors still see v1. You check v2 via the private API. Visitors see v2, on every file at the same moment.
Figure 3. Deploying adds versions and moves preview. Promoting moves published. The public rule's labelFilter: "published" is what makes visitors wait for step 3.
bash
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.html from the new release with app.js from 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 published at 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 deploy sets 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