Templates & versions
How templates are registered, versioned, and why versions are immutable.
A template is a registered React component that consumes JSON as props. It
has a stable slug (and an id) within your organization. Each push of that
component creates a new, immutable version of the template.
Registration and versioning
Registering the same slug twice does not create a second template — it
creates the next version:
renduo push Invoice.tsx --slug invoice # → templateId, version 1
renduo push Invoice.tsx --slug invoice # → templateId, version 2Every push bakes a self-contained HTML shell (React inline, your bundle
inline, assets baked in) and stores it in R2 under the new version. The new
version becomes active, and the previous active version is marked
disabled. Generation resolves the active version by default.
Versions are immutable
Once a version is baked, its bytes never change. This is the guarantee that makes regenerating documents reproducible (ADR-011 §3.9): a document generated from version 3 last year regenerates byte-for-byte identical today.
Pinning an exact version
You can generate against a specific historical version instead of the active one:
{
"templateId": "…",
"version": 2,
"payload": { "…": "…" },
"mode": "sync"
}Both active and disabled versions are resolvable for generation — a
disabled version is simply superseded, not broken.
Rolling back to a previous version
Pinning a version fixes one call. When a freshly pushed template turns out to
be wrong, what you actually want is to make a previous version the default again
for everyone. That is a rollback: the version you name becomes active, and
the one that was active becomes disabled.
curl -X POST https://api.renduo.dev/v1/templates/$TEMPLATE_ID/versions/2/activate \
-H "Authorization: Bearer $RENDUO_API_KEY"{
"id": "…",
"slug": "invoice",
"activeVersion": 2,
"previousActiveVersion": 3
}The swap happens in a single transaction, so there is never a moment where your
template has two active versions or none. Requires the templates:write scope.
Three things worth knowing before you wire this into a pipeline:
- It is idempotent. Activating the version that is already active succeeds and changes nothing — a retried CI job will not fail on the second run.
- You address the version by its number, the same integer you see in
renduo templates listand in the dashboard. There is no separate version id to look up. - Rolling back does not delete anything. Version 3 is still there, still
disabled, still resolvable if you pin it explicitly. Rolling forward again is the same call with a different number.
Only versions that finished publishing (active or disabled) can be
activated. A pending or failed version never became usable, so activating
one returns 409 VERSION_NOT_ACTIVATABLE rather than silently promoting a
broken build.
You can also do this from the dashboard — Templates → your template → Reactivate on any disabled version. The API exists so a rollback can be a step in CI instead of something you do by hand in a browser during an incident.
Asset immutability (the three lines)
Assets (fonts and images) are baked into the version, not linked from it. When you push a template that declares an asset, Renduo inlines those bytes into that version's shell. Three consequences follow, and they are deliberate:
Re-uploading an asset does not change templates you already published. A document generated from version 3 last year regenerates byte-for-byte identical today. To use a new version of a font, publish a new version of the template —
renduo pushwith the same slug.
Deleting an asset does not break anything you already published. It only stops you from declaring that asset in future versions. Existing versions keep rendering with the file they were built with.
This is the opposite of how most asset systems behave ("I updated the logo, it changes everywhere"), and it is why "I changed the font and the PDF still looks the same" is not a bug — it is the reproducibility policy working.
Status lifecycle
| Status | Meaning |
|---|---|
pending | The version was created but its R2 upload has not been confirmed yet. |
active | The version is baked, uploaded, and is the default for generation. |
disabled | A newer version replaced it. Still resolvable for generation, and can be made active again with a rollback. |
failed | The upload to R2 failed; the version is broken and never becomes active. |
Limits
- Free plan: 5 templates per organization (counting new slugs only).
- Paid plans: unlimited templates.
- Bundle size: each uploaded bundle is capped (
MAX_BUNDLE_SIZE_KB); the baked shell is separately capped at 8 MB, which is what your declared assets count against.
See also
- Component-first model — how registration and baking work.
- Assets — fonts and images, and their immutability.
- Plans & limits — the limits table.
- API reference — Templates
- API reference — Activate a template version — the rollback endpoint.