Changelog
This page tracks the release history of Forja. The canonical changelog lives in CHANGELOG.md at the repository root; this page mirrors it.
Forja is a GDPR-first, multi-tenant headless CMS — a Rust (Axum) backend and a React (MUI Material 3) admin SPA. It matured through a private 1.x series; 2.0.0 is the first release published as open source, so the public version history begins at 2.0. The pre-release milestones below condense how the foundation came together; precise dates start at the first public release.
2.1.4 — 2026-08-09
A monorepo-wide dependency sweep (PR #214) — no feature changes, but two security-relevant upgrades and a batch of hardening fixes.
Changed
- jsonwebtoken 9 → 11 (backend). The JWT library had been pinned to 9.x because v10 rejected Clerk session tokens carrying non-string custom header fields (the 2.0.1 outage). v11 defers header-extras deserialization, so unknown fields are tolerated again; the migration is guarded by the existing regression test and ships on the
aws_lc_rscrypto backend already in the dependency tree. - Dependency refresh everywhere. Backend: aes-gcm 0.11 (new RustCrypto AEAD stack, wire format unchanged), infer 0.22, redis 1.5, uuid 1.24, aws-sdk-s3 1.141. npm: tiptap 3.29, MUI 9.3, React 19.2.8, Vite 8.2, Astro 7.2, Playwright 1.62, and all transitive advisory fixes (brace-expansion, fast-uri, postcss, undici, nanoid) —
npm auditandcargo auditare clean, with a newsharp >=0.35.3override in docs closing the libvips CVEs.
Fixed
- React correctness findings from the react-doctor 0.9 upgrade. Impure state updaters in the list-page sort toggle and locale-aware fields were restructured, image-preview object URLs are now created and revoked in effect scope via a shared
useImagePreviewUrlhook, preview windows open withnoopener,noreferrer, and the auth/help/preferences context values are memoized to stop needless re-renders of every consumer.
2.1.3 — 2026-07-13
Added
- Maintenance mode is readable by the sites that need it. The admin could put a site into maintenance mode, but the flag was only visible through the master-key sites overview — an SSR frontend, which holds a Viewer-tier key, had no way to see it, so the toggle was decorative for templates.
GET /sites/{id}/settings/publicnow carriesmaintenance_mode, and because the settings save already invalidates the response cache, flipping the toggle in the admin propagates immediately: a frontend can swap the whole site for a maintenance page while you work.@forjacms/clienttypes it as optional, so consumers stay compatible with older backends.
2.1.2 — 2026-07-13
A security fix: the anomaly detector was auto-blocking healthy API keys.
Fixed
- API keys are no longer auto-blocked for being used normally. The anomaly detector compared a key's current traffic against the key's own 7-day average, so a low-traffic site ended up with a ceiling of a few dozen requests per hour — and an ordinary site build, which fetches content for every page at once, tripped it. Because the block was permanent and a blocked key serves no traffic, the enforced downtime dragged the average down further, lowering the threshold and making the next block arrive sooner: unblocking the key made the next failure more likely. Volume spikes are now alert-only by default (the quota already caps volume, so blocking on a spike added no protection and only caused outages), spike thresholds have sane floors, and time spent blocked can no longer drag a key's baseline down. A key that starts erroring at scale — a genuine sign of compromise — is still blocked, and site owners and admins are now notified in-app when that happens instead of discovering it through a failed deploy. Operators who want the old behavior can set
APP__SECURITY__ANOMALY_BLOCK_ON_VOLUME_SPIKE=true.
2.1.1 — 2026-07-13
UX follow-up to the 2.1.0 UI Strings module, from first-use feedback.
Changed
- UI Strings are edited in a dialog, like the rest of the admin. Creating and editing a string now opens a popup with the key and per-locale values behind locale tabs (missing/outdated status chips included), saving everything in one request; the separate detail pages are gone. Clearing a translation removes it on save, the default-locale value stays required, and viewers get the same dialog read-only.
- The UI Strings list is searchable and paginated. A key search (case-insensitive, combines with the missing/outdated filter) and pagination make sites with large dictionaries navigable.
2.1.0 — 2026-07-13
Four features driven by consumer feedback from a live production site: a UI-strings module for localized interface chrome, first-class legal-document references in navigation, a public site-settings read, and completed menu localization.
Added
- UI Strings module. A site-scoped, localized key→value dictionary for interface chrome — the "min read" labels, footer texts, aria-labels, and empty-state copy that used to be hard-coded English in the site template. The admin gains a UI Strings page with per-locale coverage at a glance (missing and outdated filters); editing the default-locale value automatically marks other locales as outdated. Consumers fetch
GET /sites/{id}/strings?locale=— a flat map with one value per key, resolved through the standard locale fallback chain — or callforja.strings(locale)in the SDK. The astro-blog template now renders all of its chrome through this system, shipping translatable defaults for 11 locales, so a site can relabel or translate its entire chrome from the CMS without touching template code. Keys are dot-namespaced and capped at 500 per site. - Legal documents are first-class navigation targets. Navigation items can now reference a legal document directly (
legal_document_id) instead of embedding a fragile free-text/legal/{cookie_name}URL that broke on rename — and, before this release, was broken even without one: admin-created legal documents had no slug at all and were unreachable via/legal/{slug}. Legal documents now always carry a canonical per-site slug (auto-derived from the document type, editable until first publish, then locked), existing NULL slugs are backfilled, and existing/legal/…links are converted to real references in-migration. The public navigation tree resolves the slug through the version chain, so publishing a new version never breaks footer links; items whose target disappears are hidden publicly and flagged as repairable broken links in the admin. - Public site-settings read. New
GET /sites/{id}/settings/publicreturns a deliberately small allowlist — contact email, theme colors, SEO title template and default description — to Read-tier API keys, withforja.site.getSettings()in the SDK. The raw settings endpoint (operational configuration: CORS origins, quotas, retention) remains Admin-only. - Menu localization, end to end. Menus always stored per-locale display names, but the admin never wrote them and the SDK never exposed them. The menu dialog now edits display names per locale,
forja.navigation.getMenuWithTree(slug, { locale })fetches a menu and its tree together with the name resolved for a locale, and the astro-blog footer heading comes from the CMS menu name with a chrome-string fallback. - Per-locale section items. Section item collections (feature lists, testimonials, FAQ entries, stats, pricing tiers) were single-language. Section localizations now carry an optional per-locale items override — the editor's locale tabs gain "localize these items" (copies the default as a starting point) and "remove localization" (falls back again) — and
GET /pages/{id}/sections?locale=resolves the right items server-side, so consumers keep readingsettings.itemsunchanged. The astro-blog template also renders section items for the first time: they were silently dropped before because array props can't ride custom-element attributes. - Site code injection actually renders. The template now injects the site's head/footer HTML snippets (analytics etc.) from the CMS — the SDK supported reading them, but no template ever output them.
- Localized translations can be removed again. Menu display names and UI-string translations were upsert-only; updates now accept
removed_locale_ids, and clearing a value in the admin removes the translation (the UI-strings default locale stays protected).
Fixed
- SDK
getCodeInjection()works with Read keys again. It called the Admin-only raw settings endpoint and 403'd in production for every Read- and Write-tier key despite documenting Read-tier access; it now uses the Viewer-tier site-context endpoint, which carries the same fields. - Navigation and menu edits reach the public API without a publish. Menu, item, and localization mutations now invalidate the response cache directly; previously only the publish pipeline did, so edits lagged behind by up to the cache TTL.
- Locale-filtered navigation trees no longer blank missing titles. Requesting a tree in a locale that lacks a translation for an item now falls back to the site-default title instead of rendering an untitled item.
- Purging a page no longer errors on navigation items that pointed to it. The item survives as a repairable broken link instead of violating a database constraint mid-purge.
- The astro-blog preview flow fetched settings it could never read (Admin-only endpoint, plus fields that don't exist in the response) — it now reads the public settings shape.
- SDK navigation types match the wire again:
localizations,updated_at,legal_document_id, andlegal_slugare typed on menu and tree responses. - One live legal version is now enforced by the lifecycle, not one endpoint. Publishing through the generic content lifecycle (including creating a document directly as Published) previously bypassed the supersede step, allowing two live versions of the same document; the invariant now fires on every publish path.
- Navigation items validate their targets belong to the site. A Write key could point a nav item at another site's page or legal document, producing dead links that leaked the foreign slug; both references are now rejected (
422), and the public tree is site-scoped as defense in depth. - Forms pages are fully localized. The public form and self-service lookup pages carried hard-coded English chrome — including messages inside inline scripts ("Submitting…", validation and network errors); everything now flows through the UI-strings system with translatable defaults in 11 locales.
- Validation failures are documented as 422 everywhere. Several write endpoints still documented 400 for body-validation errors while returning 422; the OpenAPI spec now tells the truth.
2.0.7 — 2026-07-12
A follow-up to the 2.0.6 legal-versioning work: legal documents now keep a single live version instead of showing every version as its own row.
Fixed
- Legal documents keep one live version at a time. After 2.0.6, editing and re-publishing a legal document left both the old and new version in the list, each marked "Published", with no way to tell which was actually served or to switch between them. Publishing a version now supersedes (archives) the previously-published version in its chain, so the Legal list collapses to one row per document and exactly one version is ever live. Rolling back is just re-publishing an older version — it becomes live and the newer one is superseded; nothing is lost either way. The list shows a
v{n}badge on documents that have history, and the version panel marks the live version and explains how to roll back.
2.0.6 — 2026-07-12
GDPR data-subject-request tooling for operators, a rebuilt legal-document versioning flow, and two admin bug fixes.
Added
- Self-service and operator DSR tooling (GDPR Art. 17 & 20). Account deletion now erases every identity-bearing built-in field — media uploads (
uploaded_by), AI-usage attribution (actor_id), and moderation records join the existing content/membership/audit erasure, all in one transaction — so no attributable data survives a deletion. Self-service and the banned-user purge run the same erasure. The data export (GET /auth/export) gainedmediaandai_usagesections. System admins can now fulfil DSRs on behalf of a user:GET /admin/users/{id}/exportandDELETE /admin/users/{id}/account(sole-owner-guarded, master/system-admin only), surfaced as "Export data (GDPR)" and "Delete account (GDPR)" actions on System → Users. Every DSR action — self and on-behalf, export and delete — writes an audit row (Art. 30).
Fixed
- Legal document versioning works end-to-end. Creating a new version no longer produces an orphan: the version keeps the document's identity (cookie name) instead of a
_copyrename, and the public by-slug resolver now returns the currently-published version of a chain — so publishing a new version supersedes the old one at the same URL while the old version is preserved. Editing a published legal document is now safe: the backend rejects in-place edits of published documents (LEGAL_PUBLISHED_IMMUTABLE), and the admin transparently forks a new draft version on edit, so the published record is never silently overwritten. - Assets → Documents no longer crashes to the error boundary. A document detail arriving without a localizations array threw during render (
Cannot read properties of undefined) and blanked the entire page; a single non-conforming document now degrades to a filename fallback instead. The same guard was applied to the document edit dialog. - Create Project wizard no longer dead-ends on the last step. Clicking Create with an invalid field (most reproducibly, a non-Latin title that slugifies to an empty slug) silently did nothing; the wizard now renders an editable slug field, validates per step, and jumps back to the step that owns the first invalid field so the error is visible.
2.0.5 — 2026-07-12
Completes the 2.0.4 release. The 2.0.4 publish was interrupted: @forjacms/sections failed its pre-publish test gate, and the v2.0.4 tag was consumed by the immutable-releases protection while re-targeting the release, so it cannot be reissued. @forjacms/client and @forjacms/analytics 2.0.4 reached npm; 2.0.5 is the first complete release of the 2.0.4 changes across all packages.
Fixed
@forjacms/sectionstest suite under Vite 8.1. Vite 8.1 stopped inheriting the rootoxctransformer config into Vitest sub-projects, so Stencil's classichJSX factory was no longer applied and every spec render failed ("The tag name provided (undefined) is not a valid name") — which blocked the package from publishing. The classic-JSX config is now set per project invitest.config.ts; remove once@stencil/vitestapplies it per project upstream.
2.0.4 — 2026-07-12
A bug-fix release for the Legal Documents admin list, with matching client-SDK support for the new list filters and a broad dependency refresh under the hood.
Fixed
- Legal document status filters, the Active/Archived tabs, and Archive now actually work. The legal list endpoint ignored status filters entirely and the admin silently dropped them, so the status chips and tabs were decorative and archiving a document appeared to do nothing (the change persisted but stayed visible). The endpoint now supports
status,exclude_status, andexclude_document_type— filtering the rows and the pagination count identically — and the documents table gained a Status column so changes are visible. CookieConsent documents are excluded server-side instead of being stripped client-side after pagination, which also fixes the off-by-one pagination totals ("1–4 of 4" while showing 3 rows).
Added
@forjacms/client: legal list filters.forja.legal.list()now acceptsstatus,excludeStatus, andexcludeDocumentType, and the legal list response type carries the document'sstatus,slug,version, and publish window, matching what the API returns.
Changed
- Dependency refresh. Backend on Rust 1.97 + edition 2024 and sqlx 0.9; monorepo npm packages batch-bumped (react-router 8, tower-http 0.7 on the backend, react-doctor 0.7.6); the Astro blog template migrated to Astro 7 with
@astrojs/node11 (Node ≥ 22.12). All audits clean.
2.0.3 — 2026-06-14
A bug-fix release clearing the admin-UI backlog: density-aware tables, the global save-bar migration, and the last read-only enforcement gaps for viewer/guest roles.
Fixed
- Compact density now applies to every table. The four drag-to-reorder tables (Portfolio CV + Projects, Navigation, Social Links) ignored Preferences → Density and stayed at a fixed height; they now track the toggle like every other table. Rows also grow to fit tall cell content — the Members inline role editor and the Sites name+slug and storage columns — instead of clipping it on Compact.
- A single Save control per content page. Blog, page, and legal detail pages rendered two Save buttons firing the same save; the editor-toolbar duplicates are removed and the floating global save bar is now the one Save, with stable end-to-end hooks. Collection type and entry forms join the same global save system (unsaved-changes navigation guard + change count), and a dead, unrouted legal-detail page was deleted.
Security
- Read-only (viewer/guest) roles can no longer reach write actions that bypassed the UI guard. Drag-to-reorder on media and document cards, and the Trash restore action, were ungated; all now require write permission. A viewer-walk end-to-end check asserts that no write controls are reachable across the content pages, complementing the existing API-side RBAC.
2.0.2 — 2026-06-14
Hotfix for a login regression introduced in 2.0.1.
Fixed
- Authenticated requests were rejected after login, locking everyone out of the dashboard. The jsonwebtoken 9→10 upgrade in 2.0.1 changed JWT header parsing: v10 deserialises unknown header parameters into a flattened
extras: HashMap<String, String>and rejects any non-string value. Clerk session tokens carry an integer custom header field, sodecode_headerfailed for every real token and every/api/v1/auth/*call returned401. Pinned jsonwebtoken to 9.x (which ignores unknown header fields) and added a regression test, so a future bump cannot silently break login again.
2.0.1 — 2026-06-14
A maintenance release: it completes the GDPR transparency surface, hardens the engineering foundation laid in 2.0.0, brings the end-to-end suite back online as a CI gate, and refreshes every dependency to clear all outstanding security advisories.
Added
- PII inventory on the Profile page. A "what we store about you" view (GDPR Art. 15) backed by
GET /auth/pii-inventory, listing each identity-bearing field with its purpose, legal basis, retention behaviour, and the caller's live record count. - Built-in PII classification. A registry declaring purpose, legal basis, and retention behaviour per identity field; it auto-generates the Records of Processing (RoPA) entries, drives a configurable data-retention cap, and is kept honest by account-erasure parity checks.
- End-to-end critical-journey CI gate. The Cucumber/Playwright suite runs its
@criticaljourneys — login, blog publish through to the public content-API view, and document management — on every pull request, with the full suite scheduled nightly. A failed scenario retries once before failing the check.
Changed
- Typed query-key factory. Admin data fetching now routes through one centralized, lint-locked TanStack Query key factory with per-site cache invalidation, replacing ad-hoc inline keys.
- Zero inline SQL in handlers. All database queries live in the repository/model layer; handlers stay thin.
- Broader behavioural test coverage for previously untested hooks and shared components.
- Pinned React Doctor in CI — the admin quality gate runs a version-pinned CLI instead of tracking an upstream marketplace action.
- Dependency refresh across the whole monorepo to the latest compatible versions (MUI 9.1, Tiptap 3.26.1, jsonwebtoken 10, and patch/minor bumps throughout).
Fixed
- Site members could not save localized content. The blog/page/legal localization endpoints gated on API-key permission tiers, so a signed-in editor was rejected before their site role was ever checked; authorization now resolves by site role.
- First localization save failed. The admin dropped the locale id when creating a content localization, so the first save of any new post errored.
- Render-phase side effects (ref writes and
setStateduring render) removed from Social Links and the cookie-consent page. - Test setup hardening so service mocks cover every module, removing a class of flaky/false-negative admin tests.
Security
- Cleared a high-severity esbuild advisory (GHSA-gv7w-rqvm-qjhr) by forcing the patched release in the Astro reference template, and a moderate joi advisory in the docs site.
- jsonwebtoken upgraded to v10 with an explicit crypto provider.
npm auditandcargo auditare clean across every package.
2.0.0 — 2026-06-05
The first open-source release of Forja: a multi-tenant, GDPR-first headless CMS with a Rust (Axum) backend, a React (MUI Material 3) admin SPA, and provider-agnostic integrations. Everything below is shipped and exercised end to end — backend, admin, client SDK, web-component library, and reference template.
Architecture
- Rust backend on Axum. An async HTTP service backed by PostgreSQL (SQLx, compile-time-checked queries, additive-only migrations) and Redis. Thin handlers over an explicit service/repository layering, with stable, documented error codes on every failure path and an OpenAPI spec generated from the code.
- React admin SPA. A Material 3 ("expressive") dashboard built with MUI,
React Hook Form, and TanStack Query, served from the backend at
/dashboard/. A composable list-page design system underpins every management screen, with a command palette (Cmd/Ctrl-K), a global save bar with per-field revert and a navigation guard, and light/dark theming. - Multi-tenant by construction. Every site is an isolated tenant; content, members, API keys, and quotas are scoped per site. Slug uniqueness is enforced per-site, never globally.
Content & authoring
- Core content types — blogs, pages (section-composed), legal documents, navigation menus, taxonomy (tags/categories), and media — with full CRUD, version history, scheduled publishing, and an audit trail.
- Publish lifecycle. A single transactional create/update path with a publish gate that requires only each site's default locale to be filled; readers fall back to the default for any locale left untranslated.
- Trash & restore. Content soft-deletes to a per-site Trash, is restorable for 30 days, then auto-purges — uniformly across every content type.
- Portfolio module — projects, CV entries, and skills, with localizations, media, links, and per-locale list endpoints.
- Forms module — structured submission collection with typed, validated, translatable fields; copy-on-create templates; a triage inbox with status workflow, notes, and CSV export; self-service reference codes; an hourly GDPR retention worker; and PII-safe webhook payloads.
- Collections (custom content types) — site owners define their own translatable content types from the admin with no code, migration, or deploy. Entries ride the same content spine (inheriting lifecycle, versioning, audit, and webhooks), schemas are stored as data rather than DDL, and every field declares its PII status, purpose, legal basis, and retention.
Compliance & privacy
- GDPR by construction. PII fields are encrypted at rest (AES-256-GCM), stripped from public read APIs, redacted by role, retention-purged on schedule, and erasable per subject with an audit record.
- Records of Processing (Art. 30). A site's Collections schema auto-generates a RoPA export.
- Runtime-config GDPR Imprint (Impressum) served from environment configuration, so no operator PII is committed to the repository.
- Privacy-first bot protection. Self-hosted ALTCHA proof-of-work is the default — challenges issued and verified in-process, no third-party request, no cookies, with single-use replay protection — alongside an optional remote-vendor mode.
Security & access control
- Role-based access control with a clear permission hierarchy, scoped per site and enforced on every mutation.
- Dual authentication — site-scoped API keys and Clerk-issued JWTs — resolved through one actor seam.
- API-key quotas & rate limiting with configurable per-key burst caps and calendar quotas, backed by Redis.
- Request validation enforced repo-wide: invalid bodies return
422with field-level detail. - Audit logging across membership changes, ownership transfers, and content mutations.
Performance
- Response cache. A short-lived, Redis-backed read-through cache sits after authentication on the public content surface; writes invalidate the affected site's cache through a single publish chokepoint, so reads stay fast without serving stale content.
Integrations & delivery
- Provider-agnostic AI. A sealed adapter seam over OpenAI-compatible chat APIs powers admin authoring assists (translate-from-default, vision tagging/alt-text) with selectable provider presets — never a hardcoded vendor.
- Webhooks with HMAC-SHA256 signing, retries, and delivery tracking; in-app notifications; and RSS 2.0 feeds for blogs.
- URL redirect management (301/302/307/308) with single-path lookup for SSR middleware.
- Media library with local-filesystem or S3-compatible storage, image processing (thumbnails, optimization, focal point), and folder organization.
@forjacms/client— an Angular/TypeScript SDK generated from the OpenAPI spec, covering the full public surface including a genericcollections(key)resource.@forjacms/sections— a framework-agnostic Stencil web-component library (Hero, Features, CTA, Gallery, Contact, Newsletter, Portfolio, and more) with an auto-generated React wrapper, HTML-sanitized by default.- Astro reference template (
templates/astro-blog/) — a build-memoized blog/portfolio site that renders pages, Portfolio, legal documents, forms, and page Collections from the API.
Internationalization
- 11 locales — Arabic, Austrian German, German, English, Spanish, French, Italian, Dutch, Polish, Portuguese, and Ukrainian — across the admin UI and visitor-facing content, with a build-time key-parity check and full RTL support.
Infrastructure
- Multi-stage Docker build on a hardened Debian base; Docker Compose for local development (PostgreSQL, Redis).
- GitHub Actions CI: backend build/clippy/test, admin build/typecheck/test, frontend health gate, and an OpenAPI-drift check.
- Railway deployment with health-check-driven zero-downtime rollout and graceful shutdown.
Development history
The 0.x milestones below condense the early build-out; development then continued through a private 1.x series before the 2.0.0 open-source release. Exact dates were not preserved; phasing is relative.
0.6.0 — Late development
- Collections — compliance-grade custom content types stored as data on the content spine, with per-field PII classification, legal basis, retention, encryption at rest, and auto-generated Art. 30 RoPA export.
- Welcome rebrand & GDPR Imprint — a plain-language, compliance-first signed-out experience and a runtime-config Impressum.
- Documentation, screenshots, and an agent-agnostic repository.
0.5.0 — Mid-to-late development
- Forms module — structured, translatable submission collection with templates, a triage workflow, reference codes, retention worker, and PII-safe webhooks.
- Self-hosted ALTCHA as the default, privacy-first bot-protection provider.
- API-key quotas & response cache — enforced quotas, configurable burst caps, and a Redis-backed read-through cache.
- Multi-tenant slug uniqueness rescoped from global to per-site; multi-replica worker safety.
- AI feature rollout behind a provider-agnostic adapter seam.
0.4.0 — Mid development
- Backend HTTP layer consolidated on Axum, unifying extraction, error handling, and routing.
- Material 3 admin redesign ("expressive") with dashboard statistics and a global save system replacing autosave.
- Security defaults hardening and a monorepo dependency refresh.
0.3.0 — Early-to-mid development
- Portfolio module — projects, CV entries, and skills.
- Web-component Sections library plus the expanded
@forjacms/clientSDK and server-side locale filtering. - Observability foundation — request-scoped tracing, structured JSON logs.
- SEO, RBAC depth, and user moderation, navigation redesign, draft preview, focal-point media, and content Trash/restore.
0.2.0 — Early development
- Multi-site content management — blogs, pages, legal documents, navigation menus, taxonomy, and media, with i18n, scheduled publishing, RSS feeds, and URL redirects.
- React admin dashboard with MUI, a block editor, a drag-and-drop menu builder, a command palette, and a language switcher.
- Webhooks (HMAC-signed, retried, tracked), in-app notifications, audit logging, and API-key + Clerk JWT authentication.
0.1.0 — Project foundation
- Initial Rust backend over PostgreSQL (SQLx migrations) and Redis, with role-based access control, OpenAPI/Swagger documentation, and Redis-backed rate limiting.
- S3-compatible and local media storage with image processing.
- The first Astro-based reference template.
- Docker and Docker Compose for local development, and the initial CI pipeline.