Changelog

Hand-curated. We'll auto-generate from git tags once we cut the first public release.

2026-05-11

Clerk Billing + force sign-out

  • Migration 0004 adds users.plan + users.plan_synced_at; plan policy lives in a single app/services/plans.py module.
  • Clerk webhook handles subscription.created / .updated / .active → set plan from items[0].plan.slug; subscriptionItem.canceled / .ended / .expired / .abandoned → revert to starter; subscription.pastDue audit-only.
  • Plan gates by capacity (templates, renders/month, storage, requests/min) — every plan can render all four formats. Starter: 5 templates · 100 renders · 100 MB · 60 r/min. Growth: 100 · 500 · 25 GB · 600 r/min. Scale: unlimited · 5 000 · 250 GB · 6 000 r/min. Exceeding any cap returns 402 with error.code=plan_limit_exceeded.
  • /v1/me returns the caller's plan + resolved limits — the dashboard's source of truth.
  • /admin/users/{id}/force_sign_out now actually revokes Clerk sessions via the Backend API (per the clerk-backend-api skill); returns attempted / revoked / failed counts.
  • Clerk's <PricingTable /> renders on /dashboard/settings — checkout runs in Clerk's drawer, your card never touches our servers.
  • Admin user detail shows current plan + last billing-sync timestamp.

2026-05-11

PPTX rendering

  • /v1/render now supports .pptx templates — per-run Jinja substitution preserves font, size, color and other run-level formatting.
  • Walker recurses into group shapes and table cells; each table cell's text frame is substituted independently.
  • Auto-collapse: when PowerPoint splits a tag across multiple runs at a formatting boundary, runs are merged into the first one before substitution.
  • Placeholder schema detection works for pptx (zip-scan picks up ppt/slides/*.xml).
  • Combine with ?format=pdf to render a deck and get a PDF in one call.

2026-05-11

PDF output via Gotenberg sidecar

  • /v1/render now accepts ?format=pdf — converts the rendered docx/xlsx through a Gotenberg/LibreOffice sidecar.
  • New service in docker-compose (gotenberg:8) with healthcheck + hardened flags; backend depends on it being healthy.
  • Settings: GOTENBERG_URL (empty = feature disabled, 501 returned) + GOTENBERG_TIMEOUT_SECONDS.
  • Gotenberg outage → 502 with the underlying reason; bad doc_type → 501; pdf source passes through unchanged.
  • Try Render UI grows a 'Download as PDF' button for non-pdf templates.

2026-05-11

Per-template version history

  • Every upload, file-replace, and restore now writes an immutable snapshot to a new `template_versions` table.
  • GET /v1/templates/{id}/versions — list snapshots newest-first with size, sha, change note, is_current.
  • POST /v1/templates/{id}/versions/{version}/restore — non-destructive: copies the named snapshot to the canonical slot and records a new version.
  • Restore re-verifies sha256 against storage before committing — tampering fails closed with a 502.
  • Template detail page grows a Version history panel with one-click restore.
  • Migration 0003 adds the table; existing render integrations need no changes — `/v1/render` still reads from templates.storage_key.

2026-05-11

Local feature-complete (B-tier)

  • Redis-backed rate limiter + API-key verify cache (skip argon2 on the hot path; falls back to in-memory when REDIS_URL is unset).
  • Structured JSON error envelope on every error — { error: { code, message, request_id, details }, detail } — `detail` kept as alias.
  • /v1/usage (aggregate counters, by-doc_type, top-10 templates) and /v1/render-logs (cursor-paginated audit).
  • Idempotency-Key on /v1/render — same key replays the prior response with `Idempotent-Replay: true` and skips re-billing.
  • Hard-delete cron (`python -m app.jobs.cleanup` / `make cleanup`): 5 sweeps gated by retention settings.
  • DB-backed admin settings (migration 0002) — rate limits, size caps, retention overridable from /admin/settings.
  • xlsx row loops: `{%row for x in xs %}` in any cell duplicates that row per iteration; styles preserved.
  • Admin user detail page with suspend / promote / force-revoke-keys / force-sign-out actions.
  • Suspended-user banner across the dashboard so 423 Locked never silently breaks the UI.
  • Pre-commit hooks (`make hooks`): ruff + standard hygiene + pre-push frontend typecheck.
  • Removed /pricing (Clerk Billing handles it via the avatar menu).

2026-05-11

Local feature-complete preview

  • Per-template versioning groundwork: PATCH /v1/templates/{id} (rename) and PUT /v1/templates/{id}/file (replace file in place).
  • csv + lines Jinja filters and an html filter for HTML-in-payload fields.
  • Sandboxed Jinja2 renderer with XML autoescape — Word can no longer be tricked into a parse error by stray <p> tags in user data.
  • Real Word hyperlinks for <a href=…> (http/https/mailto/tel only).
  • /dashboard/templates/[id] — Try Render with JSON editor, inline preview via mammoth.js, rename and replace.

2026-05-11

Renderers and isolation

  • docx render shipped (docxtpl + Jinja2 SandboxedEnvironment).
  • xlsx render shipped (per-cell substitution; formulas preserved).
  • Opt-in subprocess isolation per render (memory ulimit + SIGKILL after 15s).
  • Auto-detected placeholder schema on upload — drives the Try Render editor pre-fill.
  • Documentation: /docs explains Writing templates (table loops, html filter, csv/lines, XML autoescape).

2026-05-11

Bootstrap

  • Full API-key system end-to-end: create, rotate, revoke, hard-delete revoked keys.
  • Admin surface: users / templates / audit / settings with role-gated routes.
  • Templates: upload, list, get, delete, soft-delete, MIME-sniffing + macro rejection.
  • /dashboard and /admin route trees with Clerk session protection.
  • /docs and /api-reference (ReDoc against live OpenAPI).
  • /pricing wired to Clerk Billing (visible only when signed out).