Renduo

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 2

Every 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 list and 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 push with 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

StatusMeaning
pendingThe version was created but its R2 upload has not been confirmed yet.
activeThe version is baked, uploaded, and is the default for generation.
disabledA newer version replaced it. Still resolvable for generation, and can be made active again with a rollback.
failedThe 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

On this page