Changelog
This page tracks the release history of Forja. For the most up-to-date changelog, see the CHANGELOG.md file in the repository.
v1.5.5 — Forms security audit followups + vendor-agnostic bot protection
Released 2026-05-12
Resolves the five findings from the forms-module security audit and ships real captcha verification for Mandatory-protected forms. Forja stays headless — each site pastes their own captcha provider's siteverify URL + secret. (#608)
Features
- Vendor-agnostic captcha verification. Backend POSTs
secret+response+remoteip(URL-form-encoded) to a per-siteverify_urland trusts the JSONsuccessfield — the contract Cloudflare Turnstile, hCaptcha, reCAPTCHA, and Friendly Captcha all share. Forja has zero vendor-specific code paths. Fails closed on missing config, provider outage, andsuccess: false. /site-settings/formsadmin page. Dedicated route gated onmodules.forms, hosting the bot-protection config panel (provider label, verify URL, encrypted secret with show/hide toggle). Read-only summary when configured; full form when not. Confirms before delete. Translations across all 11 admin locales.- Admin endpoints
GET / PUT / DELETE /sites/{site_id}/bot-protection— permission-gated. Plaintext secret is never read back after write.
Security fixes (audit followups)
- Self-service endpoints are now tenant-scoped.
lookup/get/deleteby reference code now JOIN onsite_idand requireX-Site-Domain. Cross-tenant probes return 404 — no information leak. - Reference-code lookup no longer an enumeration oracle. Inputs are normalized (trim + uppercase) and shape-validated before any DB query. Malformed strings 404 immediately with no DB hit; lowercase user input still matches uppercase-stored codes.
- Submissions drop undeclared JSON keys before insert. The validation engine iterated over declared fields only; extra keys were stored verbatim. Now filtered, closing a storage-pollution / data-shape vector for SDK consumers outside this repo.
- Bot-protection token now verified. Previously a non-empty string passed; now the handler decrypts the site's configured secret and calls the captcha provider before any DB write.
- Redis-absent boot WARN now names the public form endpoints and points operators at
RATE_LIMIT_FAIL_MODE=closed.
Operational
- Triaged Dependabot alert #82 (CVE-2026-25537,
jsonwebtokentype confusion) asnot_used— Forja pins v9.3.1 (per #598, v10 breaks Clerk v2 tokens); the exploit chain depends on v10'sTryParseenum which v9 doesn't contain, andbuild_validation()never enablesvalidate_nbf.
Upgrade notes
One new migration (20240101000061_site_bot_protection.sql). Drop-in: forms with bot_protection = None work unchanged. Forms with bot_protection = Mandatory will return 503 BOT_PROTECTION_NOT_CONFIGURED until a site admin configures a verifier under /site-settings/forms — that's a deliberate fail-closed replacing the previous accept-any-non-empty-string behaviour. Operators on Redis-required deployments should set RATE_LIMIT_FAIL_MODE=closed if not already.
v1.5.4 — Logging polish: access log, health-check skip, slow-request warning
Released 2026-05-12
Five small but high-value observability improvements layered on top of v1.5.3's foundation. Together they make daily log usefulness step-change better. (#605)
Features
- Per-request access log. Every non-health request now emits one
request completedINFO line with full span context (request_id,method,path,status,latency_ms). Previously, successful requests produced no log output at all. - Skip healthy
/healthprobes from the access log — they're typically ~80% of inbound traffic and zero diagnostic signal. Errors on /health still log. - Slow-request WARN above 1000ms with a
slow = truefield, so they're alertable in any aggregator without a dedicated query. tracing-errorErrorLayer registered (phase 1) — enablesSpanTrace::capture()for future error-chain context. ApiError adoption is a follow-up sweep.
Fixes
- Silence
sqlx::postgres::noticeboot spam — "relation already exists, skipping" messages were appearing at INFO despitesqlx=warn. Explicitsqlx::postgres::notice=warnoverride now keeps the boot log clean.
Upgrade notes
Drop-in. New dependency: tracing-error. Custom RUST_LOG users may want to append sqlx::postgres::notice=warn to match the new default.
v1.5.3 — Observability foundation: request_id span, JSON logs, banner
Released 2026-05-12
Closes the logging epic (#599, #600, partial #601, plus #604 follow-ups). Motivated by the 2026-05-12 JWT incident, which exposed how blind we were: log lines had no per-request correlation and every internal failure mode collapsed to one outer error code with no debug surface.
Features
- Per-request
request_idpropagated into ahttp_requesttracing span. Every request inheritsrequest_id,method,path,status,latency_msfields automatically. InboundX-Request-IDis echoed; missing ones get a server-generated UUIDv4. - JSON logs in production, pretty ANSI in dev. Toggled by env:
LOG_FORMAT=jsonforces JSON anywhere,RAILWAY_ENVIRONMENTdefaults to JSON. Span fields nest inside each log line — parseable by any aggregator or LLM. - Boot banner with build version. ASCII-shadow figlet "FORJA" +
CARGO_PKG_VERSIONprinted before tracing init. Always visible. - Structured-fields sweep on workers — 30 printf-style log calls in 8 worker files (
audit_cleanup,trash_cleanup, etc.) converted to structured fields withworker=,site_id,error,phase,kindetc. #[tracing::instrument]on worker tick functions — every worker tick gets a named span; per-iteration workers (webhook retry/flush) carry per-item IDs.
Fixes
- Downgrade
jsonwebtoken10 → 9 — v10'sHeader.extras: HashMap<String, String>rejects Clerk v2 tokens carrying integer header claims (oiat), silently failing every JWT auth. - No more silent
.ok()?in JWT validation — five gates invalidate_tokennow emit awarn!with the specific failure cause before returningNone. The 2026-05-12 incident took hours to diagnose because the WARN-level log only fired on the outer decode failure; inner causes weredebug!-only. - Boot banner to stdout (not stderr) so Railway tags it
[inf]instead of[err].
Upgrade notes
Drop-in. New optional env var: LOG_FORMAT=json to force JSON output regardless of RAILWAY_ENVIRONMENT.
v1.5.2 — Production hotfix: jsonwebtoken v9 downgrade
Released 2026-05-12
Emergency hotfix for a production-wide Clerk JWT validation failure. Every authenticated dashboard request returned 401 AUTH_TOKEN_INVALID, locking operators out and cascading into IP-based rate-limit bans.
Fixes
- Pin
jsonwebtokenat v9. v10.x'sHeader.extras: HashMap<String, String>collects unknown JWT header claims but rejects integer values, which Clerk's v2 session tokens carry asoiat. Result: every Clerk JWT failsdecode_header(). v9 doesn't have the buggyextrasfield — unknown claims are ignored per RFC 7515. Bug confirmed in v10.3.0 and v10.4.0.
Upgrade notes
Drop-in. The only side effect is the crypto backend swap (aws_lc_rs → ring); Forja uses neither directly.
v1.5.0 — Forms Module + localization + AI translate
Released 2026-05-11
Closes the Forms Module epic (#579) end-to-end: structured data collection with custom fields, server-side validation, copy-on-create templates, GDPR self-service for visitors + hourly retention worker, full per-locale translation of every visitor-facing string, and AI-assisted "Translate from default" in the admin Translations tab.
See the Forms admin guide and Forms API reference for full documentation.
Features
- Forms module — end-to-end structured submission collection. New admin pages at
/forms,/forms/:id(Settings + Fields + Translations tabs),/forms/templates,/forms/:id/submissions. Visitor pages at/forms/[slug]and/forms/lookupin the Astro template. - Per-locale translation of every visitor-facing string. Public endpoint accepts
?locale=and substitutes localized text into the response. Fieldlabelstays canonical (JSONB key) so storage shape is locale-invariant. All 11 admin locales carry the new strings. - AI-assist "Translate from default" in the Translations tab. Bulk header button + per-input ✦ icon, mirroring the blog-detail pattern. Gated behind both the
modules.aisite flag and an actual AI provider config. - Crypto-grade reference codes (
XXXX-XXXX-XXXX, ~57 bits, ambiguous chars excluded) generated from the kernel CSPRNG. - GDPR retention worker — hourly tick, soft-deletes submissions older than each form's configured
retention_days.NULL/0opts out. - Webhook integration with PII-safe payloads —
form.submission.createdevent excludes field data; webhook receivers fetch via admin API if they need it. - Idempotent self-service delete returns 410 Gone — distinguishes "already deleted" from "wrong code" atomically.
- TS client library — new
FormsResource+ client-sidevalidateSubmissionhelper. 100% test coverage on all four metrics. - Conditional field logic spec for phase 2 (forward-compatible data model, no migration needed).
Fixes
- Clerk JWT site Owners can now create/edit/delete forms. Form admin endpoints used
WriteKeywhich always rejects Clerk JWT users (they carryApiKeyPermission::Readregardless of site role). Swapped toReadKey+ the existing role-basedPermissionService::require. - Try-demo spinner no longer hangs. A recent security commit randomized the demo guest API key; the admin still sniffed the old
dk_guest_*prefix to detect guest sessions, soisGueststayed false andRequireAuthkept rendering Welcome. Replaced the sniff with an explicitapi_key_is_guestsessionStorage marker. - Form admin pages no longer double-pad — Layout shell already provides outer chrome; the four Forms pages had stray inline padding.
- libs/client
tsc --noEmitpasses on the Angular DI test parent type (targeted cast onInjector.NULL → EnvironmentInjector).
Refactor
- ModulesTab dirty flag derived from data rather than tracked as parallel state. Same pattern as FormDetail.
Tests
- libs/client at 100% coverage on all four metrics; threshold pinned at 100 so future regressions fail CI immediately. 162 → 194 tests.
Upgrade notes
Drop-in for sites that don't enable Forms. The module ships disabled by default; enable per-site under Site Settings → Modules. Two new migrations add tables only — no existing tables are modified.
v1.4.6 — Site Settings restored for new owners (effect-ordering race fix)
Released 2026-05-09
Follow-up to v1.4.5: a brand-new Clerk user who created their first site through the wizard could see the site selected and was correctly listed as Owner on the Site Detail page, but the Site Settings sidebar link was missing and the dashboard rendered the read-only banner. Hard refresh and re-login did not clear it. Backend /auth/me was already returning the correct membership; the bug was isolated to a frontend effect-ordering race.
Fixes
- fix(admin): resolve effect-ordering race that hid Site Settings for new owners.
AuthProviderregistered its_selectedSiteIdlistener inside auseEffect;SiteProvider(a child) callsnotifySelectedSiteChangedfrom its ownuseEffect. React runs effects child-first, so on cold mount the initial site-id update fires before the listener exists —currentSiteRoleresolved tonull,isOwner/isAdminwere false, sidebar link was hidden and the read-only banner shown. The fix replaces the hand-rolled module-store + listener with React'suseSyncExternalStore, which reads the current snapshot on every render and is race-free by design. Tracer-bullet test replays the exact production/auth/meJSON. (#574, #575)
v1.4.5 — New-user site creation unblocked + curated timezone list
Released 2026-05-08
Three issues surfaced by a brand-new (non-system-admin) Clerk user trying to create their first site through the wizard.
Fixes
- fix(sites): let site Owners apply settings during initial site creation. The Create Site wizard issues
POST /sitesfollowed byPUT /sites/{id}/settings. The settings handler usedAdminKey, which gates on API-key class (Master/Admin tier) — Clerk JWT users default toReadtier, so freshly-minted site Owners were rejected with "Admin permission or system admin required". The extractor is nowAuthenticatedKey; the existing per-sitePermissionService::require("settings","update")already enforces site-level Admin/Owner correctly. (#557) - fix(admin): timezone dropdown shows distinct, region-grouped IANA zones. The Create Site wizard previously rendered duplicate-looking rows like multiple "Atlantic Standard Time (GMT-4)" entries because labels were derived from
Intl.DateTimeFormat({ timeZoneName: 'long' }). Labels are nowEurope/Vienna (UTC+02:00)style (IANA identifier + live offset), grouped by region. (#557)
Features
- feat(admin): slug field is read-only and auto-derived from site name. Users no longer choose a slug.
Site::createnow retries inserts with-2,-3, … suffixes on slug unique-violations so duplicate site names produce usable slugs (my-site,my-site-2). (#557)
v1.4.4 — Security hardening patch
Released 2026-05-08
Targeted security patch stacking six hardening fixes plus three admin UX improvements and an i18n backfill.
Security
- fix(security): encrypt webhook HMAC secrets at rest with AES-256-GCM. Webhook
secretfields were previously stored in plaintext. They are now encrypted withENCRYPTION_KEYviaforja_crypto::encrypt_value/decrypt_value. (#542) - fix(security): pin DNS in AI provider requests to prevent rebinding. AI HTTP clients now pin the resolved IP address on the
reqwest::Client, matching the SSRF pattern already applied to webhook delivery. (#543) - fix(security): validate JWKS URL with SSRF check before fetch. Clerk JWKS fetching now runs the same SSRF guard as webhook and AI outbound requests. (#544)
- fix(security): add Origin/Referer CSRF check to admin session endpoints. All admin-session mutation endpoints now require an
OriginorRefererheader matching the configuredPUBLIC_URL. (#545) - fix(security): replace hardcoded demo guest token with random key. The demo guest API key is now generated at startup with
forja_crypto::generate_api_key. (#546) - feat(security): add auth brute-force rate limiting. Failed authentication attempts are now rate-limited per IP and per endpoint, configurable via
AUTH_RATE_LIMIT_*env vars. (#547)
Fixes
- fix(devops): change CORS wildcard default to deny-all in
docker-compose.prod.yml. Operators must set their actual origin list. (#548) - fix(i18n): backfill missing translation keys across all 11 locales. (#549)
- fix(admin): show Create Site button for all authenticated users, not just admins. (#553)
- fix(demo): require opt-in to join demo site, prevent auto-rejoin after leave. (#554)
Features
- feat(admin): auto-generate slug from site name + localized timezone dropdown. (#555)
v1.4.3 — Hotfix: storage content-type fallback + dashboard CSP allow blob:
Released 2026-05-07
Two prod bugs surfaced after v1.4.2 deployed — the new error toasts in the favicon page made them visible. Both blocked the favicon-replacement flow that v1.4.2 was supposed to fix.
Storage layer lied about Content-Type for some S3 objects
Picking an existing image from the media library produced "Selected file is not a valid image" even though the bytes were a perfectly valid PNG. The two storage backends were asymmetric on fetch: LocalStorage::fetch already sniffed the bytes with infer::get(&data), but S3Storage::fetch trusted whatever S3 returned and fell back to application/octet-stream if absent. Some objects in the prod bucket carry a missing or generic Content-Type (older uploads from before the upload path correctly forwarded the inferred MIME, plus S3-compat-quirks where Hetzner Object Storage doesn't preserve PutObject's Content-Type metadata). When the file proxy served those objects, the browser stamped application/octet-stream onto blob.type, and v1.4.2's new blob.type.startsWith('image/') guard correctly rejected them.
S3Storage::fetch now sniffs the bytes when the stored content-type is missing or generic, mirroring LocalStorage::fetch exactly. No migration needed — works against the existing prod bucket as-is.
Dashboard CSP missing blob: in img-src
The Register Media upload dialog showed a broken-image glyph for the selected file's preview thumbnail. File metadata (10.7 KB · image/png) rendered correctly; only the visual preview was broken. The dashboard CSP template's img-src directive read 'self' data: https: — but URL.createObjectURL(file) produces blob: URLs, which is how every modern SPA renders local-file image previews before upload. Without blob: in img-src, browsers silently dropped the image. Only surfaced in prod because Vite's dev server doesn't enforce a CSP. The actual upload was unaffected (the file is sent as multipart FormData; the blob URL is only used for the on-screen preview).
The fix is one keyword: img-src 'self' data: blob: https:. The neighbouring worker-src 'self' blob: was already in the policy — img-src was a one-off omission.
v1.4.2 — Hotfix: favicon-replacement reliability + post-migration doc drift
Released 2026-05-07
Hotfix release stacking three independent fixes that all surfaced after v1.4.1 shipped.
Favicon replacement now actually replaces
Replacing an existing site favicon used to leave the old icon stuck on screen. Two compounding bugs were responsible.
Silent upload failures. The branding page swallowed errors at three points — img.onload had no onerror partner, the media-picker fetch never checked response.ok, and uploadMutation.onError discarded the server's RFC 7807 detail. The user got no toast and no console output when something failed; the upload simply vanished. Each gate now emits a specific snackbar, and server errors append the actual reason to the failure toast.
Cache-Control: immutable on path-addressable URLs. The file proxy ships every response with Cache-Control: public, max-age=31536000, immutable. The immutable directive forbids browser revalidation entirely — once a URL is cached, it is never re-requested, not even on hard reload. Favicon variants live at deterministic paths per site (site_favicons/SITE_ID/favicon-32x32.png), so overwriting the S3 key was invisible to browsers. Fix: stamp every favicon URL with ?v=UNIX_TS so each generation is its own URL. upload_favicon uses Utc::now().timestamp() at write time and persists the busted v32 URL into Site.favicon_url so SSR templates also pick up the new icon. get_favicon, get_webmanifest, and get_browserconfig use site.updated_at.timestamp(). The existing immutable header stays — it's correct once the URLs behave content-addressable.
Post-Axum-migration doc drift
References missed by the v1.4.1 docs hotfix: README.md and backend/README.md still listed Rocket 0.5 as the framework, with the pre-migration source-tree diagram. SECURITY.md tagged the backend as Rust/Rocket, the frontend as Next.js, and pinned 1.0.x as the supported version. All updated to Axum 0.8, Astro frontend templates, supported = 1.4.x.
Swagger UI title fix
The admin OpenAPI doc rendered as "Forja API (Axum)" — leftover migration debug noise — while the page's HTML <title> already said "Forja Admin API". Renamed info.title to "Forja Admin API" and rewrote the description to describe what the admin doc actually is now (admin surface, requires Master/Admin/Write key or Clerk JWT, with a pointer to the Consumer API doc). Server description dropped the "(Axum)" qualifier. Consumer doc title ("Forja Consumer API") is unchanged.
Upgrade notes
Drop-in. No DB migrations, no env-var changes, no API contract changes. Variant URLs returned by the favicon and webmanifest endpoints now carry a ?v=<timestamp> query string — clients that consume them as opaque strings (the admin, the Astro template) require no changes.
v1.4.1 — Hotfix: media-route multi-segment + Axum docs refresh
Released 2026-05-07
Hotfix for v1.4.0. Two changes, one tag.
Production fix — file proxy multi-segment regression
The Rocket → Axum cutover ported the public file-proxy route as /files/{path}. In Axum 0.8, single-segment captures use {name} and the greedy multi-segment catch-all (Rocket's <path..> equivalent) is {*name}. Any deployment with a STORAGE_S3_PREFIX set — or any local-storage layout with subdirectories — was generating multi-segment public URLs (/files/media/2026/05/UUID/foo.png) that fell straight through to the not-found middleware. Effect on production: every image, blog hero, page asset, and media-library thumbnail returned RFC 7807 "No route matched" to the browser. The route pattern was changed to {*path} and the one hardcoded path-template assertion in the OpenAPI test was updated to match.
The diagnostic crumb that pinned this without ssh: single-segment paths returned an empty 404 body (handler ran, storage.fetch missed) while multi-segment paths returned the router fallback's RFC 7807 JSON. Different layers, different shapes.
Documentation — Docusaurus catch-up to the Axum cutover
The docs hadn't moved with v1.4.0. Refreshed every present-tense reference to Rocket across 16 files (changelog.md left intact for historical accuracy):
- Architecture pages — Rocket fairings → tower layers; Rocket request guards → Axum extractors (
FromRequestParts); Rocketon_liftoffworkers →axum_app::workers::spawn_all(state); RocketFileServer→tower_http::services::ServeDir;Responder→IntoResponse. Diagrams updated. - Backend directory tree — rewritten to match the actual
src/axum_app/layout (handlers,middleware,extractors,openapi_split,workers). - Developer guide — handler example rewritten to use
State/Path/Queryextractors,OpenApiRouter, and theutoipa_axum::routes!collector instead of the Rocket#[get(...)]+routes![]pattern.
Documentation — drift caught while auditing
Three things the docs claimed and the code disagreed with:
CLERK_EXPECTED_ISSUERauto-derivation — the v1.3.0 docs (and.env.example, and the boot-guard error message itself) advertised that the issuer would be auto-derived fromCLERK_PUBLISHABLE_KEY/CLERK_FAPI_DOMAINwhen unset. The v1.4.0 boot path read only the explicit env var with no fallback — which caused a brief production incident. Docs now tell the truth: the issuer must be set explicitly in production. The fallback restore is a separate follow-up.APP__CORS_ORIGINS— the actual override key isCORS_ALLOWED_ORIGINS. Fixed in.env.exampleand four deployment docs.RATE_LIMIT_FAIL_MODEdefault — the example comment saiddefault: open. The real default has beenclosedsince v1.3.0. Comment fixed.
Newly documented env vars: APP__LOG_LEVEL, APP__ENABLE_TRACING, PUBLIC_URL, ENCRYPTION_KEY (with AI_ENCRYPTION_KEY alias), and DEMO_MODE. All ROCKET_ADDRESS / ROCKET_PORT / ROCKET_LOG_LEVEL references removed from deployment docs — those env vars no longer do anything in v1.4.0+ (bind address comes from APP__HOST / APP__PORT, log level from APP__LOG_LEVEL).
v1.4.0 — Rocket → Axum cutover
Released 2026-05-07
A single, decisive web-framework migration release. Forja's backend now runs on Axum (with axum-server for TLS and utoipa-axum for OpenAPI), replacing Rocket 0.5.1. The motivation: Rocket is unreleased on master, has stalled CVE remediation (rand@0.8.6, rustls-webpki@0.101.7 were upstream-blocked LOW alerts in v1.3.0), and its ecosystem footprint was holding back the rest of the dep graph. Cutover was done as one long-lived branch (the project is pre-launch, so no compatibility shim was needed).
Net change: 105 files, +13,097 / −15,052 LOC. Smaller surface, simpler boot, no more Rocket-only dormant deps.
Highlights
- Single Axum server on
:8000with TLS viaaxum-server0.8. Rocket removed fromCargo.toml;axum_app/is now the only HTTP layer. - All ~165 endpoints ported across the bundle taxonomy — auth, blogs, pages, legal, media, sites, navigation, webhooks, AI, trash, taxonomy, locales, sitemaps, files, robots, system, and more. Phase rollout (1 → 5) replaced Rocket fairings + guards with Axum extractors and
towermiddleware while keeping route paths byte-identical so the admin SPA and consumer template clients didn't notice. - Auth & authorization model preserved:
MasterKey/AdminKey/WriteKey/ReadKeywrappers +AuthenticatedKeyextractor +ModuleGuard+CurrentSiteall reimplemented as Axum extractors with the same trait shape.ClerkJwksStateplumbed throughAppState. - OpenAPI split kept: consumer Swagger at
/api-docs/consumer/, admin at/api-docs/admin/. The post-split admin doc is now injected into the handler viaExtension, and the admin trailing-slash route was restored. CSP relaxed for the inline Swagger bootstrap script. - Middleware re-pointed at
tower: dynamic CORS, public rate-limit, rate-limit headers, security headers, request-ID, usage tracking, not-found handling. Same behavior, smaller code. - Integration smoke tests reinstated at the HTTP level against the Axum router.
- CVE patches:
rustls-webpkiand Clerk SDK bumps applied as part of the cutover.
Versioning
All workspaces aligned to 1.4.0 — backend/Cargo.toml, every package.json in admin/, docs/, e2e/, libs/analytics, libs/client, libs/sections, libs/sections-react, templates/astro-blog/, plus the @forjacms/sections peer-dep range in sections-react. scripts/bump-version.sh is the single source of truth.
Breaking changes
None at the HTTP contract level — every previously-documented endpoint (and the Swagger paths) is preserved. The breaking surface is internal: Rocket attribute macros, Rocket<Build> test fixtures, and RawData / Form / &State patterns are gone. Anyone forking this repo at v1.3.x will need to port their custom routes to Axum extractors before merging v1.4.0.
Residual cleanup
Old upstream-blocked Dependabot alerts (rand@0.8.6, rustls-webpki@0.101.7) pinned by Rocket are resolved by this release — Rocket is no longer in the tree. The five MODERATE @astrojs/check → yaml-language-server → yaml dev-tooling alerts in templates/astro-blog/ from v1.3.0 are unchanged here; that decision is still pending.
v1.3.3 — Toast container positioning (production build)
Released 2026-04-21
Follow-up to v1.3.2 — the bottom-left toast overlap kept occurring on Railway deployments even though the override was correct. Root cause was narrower than suspected.
Admin — notistack container pinned without relying on goober
Notistack v3's container uses goober (not emotion) for its own positioning classes — styles$1.top, styles$1.right, styles$1.root. In a Vite production bundle the goober stylesheet can end up initialized after the first class names are requested, so those class names are set on the container element but the matching CSS rules never land in the document. Under that failure mode, anchorOrigin: top-right is still respected at the React level — the right classes are applied — but the CSS behind them is a no-op, so the container loses position: fixed and renders inline with the React tree, at the bottom-left of the sidebar area.
HMR on localhost was masking this: goober re-injects its sheet after every hot reload, so the container always rendered correctly there. Only in a cold production load did the positioning drop.
Rather than fight the goober initialization order, the fix targets notistack's stable semantic class (.notistack-SnackbarContainer) directly — always applied by the library, never hashed by goober — and pins the full anchor in one CSS rule: position: fixed, top: 72px, right: 16px, left: auto, bottom: auto, z-index: 1600, plus the display: flex; flex-direction: column notistack needs to stack multiple toasts. The forja-snackbar-top-right class is kept alongside so both paths converge on the same positioning. Inner pointer-events: auto restores click-through on the individual snackbar items.
v1.3.2 — Toast positioning hardening + docs build fix
Released 2026-04-21
Targeted patch release addressing two regressions surfaced after v1.3.1.
Admin — toast positioning
The v1.3.1 top-right toast anchor relied on notistack's internal anchor class for right, z-index, and the left / bottom: auto resets — the forja-snackbar-top-right override only declared top. When the internal class landed after ours in cascade order (as happens on certain deployment pipelines and stale caches), the container fell back to the default bottom-left and slid under the sidebar footer.
The override now carries the full anchor in one place: top: 72px, right: 16px, left: auto, bottom: auto, z-index: 1600. Toasts are pinned to the top-right regardless of class ordering and always float above the Drawer (z-index 1200) and Dialog (1300) surfaces.
Docs
docs/docs/changelog.md contained **libs/{analytics,client,sections}** as a bold-highlighted path. MDX tried to evaluate the curly braces as a JS expression, throwing ReferenceError: analytics is not defined when statically rendering /docs/changelog. Wrapping the path in backticks so MDX treats it as inline code restores the Deploy Documentation workflow (triggered on v* tags).
v1.3.1 — M3 Expressive admin redesign + dashboard stats
Released 2026-04-21
UX-focused release. The admin dashboard was ported end-to-end onto the M3 Expressive token system, a handful of list pages gained clickable count badges backed by new backend aggregate endpoints, the audit and notifications surfaces got the polish they needed, and a cluster of small usability gaps around navigation and destructive actions were closed.
Admin — M3 Expressive redesign
Every admin surface now reads from the same token palette:
- Top bar unified. AppBar drops the primary (purple) fill for
--surfacewith an--outline-variantstroke; the three top-right triggers (notifications, help, account) adoptM3IconButton size=40so they read as a single icon strip. A sharedm3MenuPaperSxdrives the notification panel, help menu, account menu, and bell popover so every drop-down anchored on the AppBar has identical chrome (radius, stroke, hover, selected state). - Site switcher, launcher, site card, page editor toolbar, blog editor toolbar, cookie consent page, media filter chips, sidebar site badge — all rebuilt on tokenised surfaces with primary-tinted accents, status pills keyed to
--tertiary-container/--warn-container/--err, and Roboto Flex with opsz/wght variable-font axes. - Forja brand mark extracted to a shared
ForjaBrandMarkdesign-system component rendered as live SVG text at weight 700 with a Roboto Flex fallback chain, so the sidebar tile and the site launcher mark stay pixel-identical. - Sidebar site badge decluttered. Role tag moves out of the site badge into the user footer (where it belongs — a role describes what you can do, not what the site is). Site badge now shows name + domain + site avatar; user footer carries avatar + name + role + logout.
- API docs page gets tokenised tabs, a download-spec button in the header, and a same-origin style injector that hides the Swagger topbar and re-tints the embedded swagger-ui against the parent palette. The injected style re-runs via MutationObserver whenever the parent's
--primarychanges, so theme/accent swaps propagate into the iframe.
Admin — filter-pill counts with backend support
Four list pages gained clickable count badges on their filter chips, each powered by a new aggregate endpoint so counts render without additional round-trips:
- Blogs / Pages —
GET /sites/{id}/blogs/status-countsandGET /sites/{id}/pages/status-countsreturn{draft, in_review, scheduled, published, archived}in a singleFILTER-clause aggregate. Status pills are translated viacommon.status.*so "Draft" / "In review" / "Published" stop rendering as raw English on German sites.InReviewandScheduledpills auto-reveal when their bucket is non-zero even witheditorial_workflow_enabled=false, so legacy rows stay reachable. - Media —
GET /sites/{id}/media/category-countsreturns{image, video, audio, document, other}; Bilder / Videos / Audio pills show live counts with mutation-driven invalidation on upload/delete. - Notifications —
GET /sites/{id}/notifications/status-countspowers All/Unread/Read pills. Newis_readquery param on the list endpoint narrows server-side (pagination now correctly stays scoped to the active pill instead of client-filtering the current page).
Admin — activity log redesign
Audit log rebuilt around the listPageV2 shell with per-row entity resolution:
- Action column is a
TokenPillkeyed to success / info / warn / err / neutral tones withactivity.actions.*translations. - Entity column (1fr, fills the viewport) renders a human reference (
blog.slug,page.route,legal_document.title, etc.) resolved server-side via the newAuditLog::resolve_entity_displayshelper — one batched IN query per distinct entity type, with localization JOINs for the four types that carry their title in a*_localizationstable. Links to the detail route when known and the action isn't a Delete; falls back to an 8-char UUID with full UUID in the tooltip only when the server couldn't resolve a display. - Server-side filters for action, entity type, and timeframe (24h / 7d / 30d / All time) via new query params on
GET /sites/{id}/audit. Action values correctly round-trip through sqlx'ssubmit_review/request_changessnake_case for the multi-word variants. - AI usage chip next to the filters.
ai_generationaudit rows are hidden from the feed via a newAUDIT_LIST_HIDDEN_ENTITY_TYPESconst (they used to dominate the log every time an AI assist fired alongside a blog Update), with aGET /sites/{id}/audit/ai-usageendpoint exposing{total, last_30_days}from the same rows as a stat.
Admin — notifications redesign
Notifications page + bell popover both land on the new design vocabulary:
- Per-notification delete via row
M3IconButton; delete-all-read header CTA with a count-awareConfirmDialog; mark-all-read header CTA. Every destructive endpoint is scoped byrecipient_clerk_idat the SQL level so a user can't drop another's rows even with valid IDs. - Four new endpoints —
DELETE /notifications/{id},POST /sites/{id}/notifications/bulk-delete,DELETE /sites/{id}/notifications/read,GET /sites/{id}/notifications/status-counts. Notification routes grow 4 → 8.
Admin — site detail redesign
The per-site detail page (/site-detail, newly reachable to non-sysadmins via the sidebar site name) is now a dashboard:
- Click-through stat cards — Active members →
/site-settings/members, Owner →/site-settings/members, Storage used →/site-settings/content(with aLinearProgressthat flips to--errabove 85% quota). - Sysadmin-only panel (visible when
isMaster) — enable/disable the site, edit the storage quota in GB. Delete moves to the Danger Zone where every site admin already manages it; the header ships a singleOpen site settingsshortcut instead. - Languages manager rebuilt on
DataTableV2with tokenised pills; locale removal now routes through a namedConfirmDialoginstead of firing on one click. - Entry point — sidebar site badge is now a
ButtonBasethat navigates to/site-detailon click; the footer avatar is a separateButtonBasethat navigates to/profile.
Admin — navigation cleanup
Three redundant entries pruned from the account overflow menu:
- Websites verwalten removed — the sidebar switch-site button already does this.
- API-Dokumentation moved from the account menu into the help (
?) drop-down — docs belong with help. - Profil replaced by a click on the sidebar footer avatar.
- Sprachen removed — locales live under System Admin for users that can reach them.
What remains in the account menu: Einstellungen / Leave site / System / Abmelden.
Backend — OpenAPI + audit + site stats
- OpenAPI version now reads
env!("CARGO_PKG_VERSION")in both the consumer and admininfo()blocks, so futurescripts/bump-version.shruns automatically surface in/api-docs/*/openapi.jsonwithout a code edit. (#release) - Audit list filters —
action,entity_type,from_date,to_datequery params (strict RFC-3339 parsing). Extended filter struct lives inmodels::audit::AuditListFiltersso bothfind_for_site_filtered_extandcount_for_site_filtered_extshare the composition. - Audit entity display resolver — batched IN queries per known entity type (
blogs.contents.slugvia JOIN,legal_document_localizations.title,navigation_item_localizations.title,cv_entry_localizations.position, etc.) withtracing::warn!on SQL errors so future schema drifts are visible in logs instead of silently returning UUIDs. - Seven new aggregate endpoints across four domains:
/sites/{id}/blogs/status-counts→BlogStatusCounts/sites/{id}/pages/status-counts→PageStatusCounts/sites/{id}/media/category-counts→MediaCategoryCounts/sites/{id}/notifications/status-counts→NotificationStatusCounts/sites/{id}/audit/ai-usage→AiUsageCount- (plus the three notification delete endpoints above)
Fixes
- Accent hue no longer rotates to purple on dark flavors. Every
color-mixthat combined an accent colour with a--surface*neutral switched fromin oklchtoin srgb, because OKLCH interpolation of red × blue-tinted neutral passes through purple mid-mix. (#mix-fix) - Light-theme contrast fixed on status pills, tag chips, filter chips, and the count badge.
resolveAccentpicks theon-primarycontrast value by WCAG luminance of the accent itself, not by flavor-levelis_light, so amber / peach / yellow accents always land readable contrast text. - German AI-usage chip read as "KI genutzt 38 ×" — the
×glyph looked like a close-affordance next to pill chrome. Swapped forMal("times") so users stop trying to dismiss the chip. (#chip-close) - Swagger iframes re-paint on theme change via a MutationObserver that watches the parent's
<head>and<html>attributes. Caching the last-seen--primarykeeps the handler from thrashing on unrelated head mutations.
Dev / infra
ForjaBrandMarkexported from@/components/design-systemso future surfaces that need the F tile pull from one source.- Shared
m3MenuPaperSxexported from@/components/layout/m3MenuSx. scripts/bump-version.shextends unchanged; backend OpenAPI and all nine package manifests plus their lockfiles now track together.
v1.3.0 — Security defaults hardening + monorepo dependency refresh
Released 2026-04-18
Large security + upkeep release. Three layers of work shipped together:
- Runtime security hardening — SSRF protection, stored-XSS sanitization, JWT pinning, and closing a
/healthinfo leak and several rate-limit bypasses. - Monorepo-wide dependency refresh — 15 Dependabot CVE patches across 6 package manifests, plus coordinated major upgrades across every ecosystem.
- MUI v7 → v9 migration across the admin SPA.
Breaking changes
Three defaults flipped to the safer option. Existing deployments may need to add env vars; see the Environment Variables reference for the full setup.
RATE_LIMIT_FAIL_MODEdefault is nowclosed(wasopen). A Redis outage no longer silently bypasses rate limits. InAPP__ENVIRONMENT=productionthe service refuses to start when Redis is unreachable and the mode isclosed. SetRATE_LIMIT_FAIL_MODE=opento restore the old fail-open behavior. (#484)CORS_ALLOWED_ORIGINSdefault is now empty (was*). The wildcard no longer reflects every origin out of the box. Production refuses to start with*. Set the env var to your actual admin-SPA and consumer-site origins. Dev and staging still accept*. (#485)- Clerk JWT
audandissmust be pinned in production. WhenCLERK_SECRET_KEYis set andAPP__ENVIRONMENT=productionthe service refuses to start unless bothCLERK_EXPECTED_AUDIENCEand a resolvableCLERK_EXPECTED_ISSUERare configured (issuer can be auto-derived fromCLERK_PUBLISHABLE_KEYorCLERK_FAPI_DOMAIN). Non-production logs warnings but still boots. (#486)
Security hardening
- fix(security):
/healthresponse sanitized — version, per-service error strings, and storage internals no longer leak to anonymous callers. New admin-only/health/detailedendpoint (AdminKey-gated) preserves the full diagnostic view. IP-based rate limiting added to previously unbounded public endpoints (/health,/files/*,/sitemap.xml,/robots.txt,/.well-known/*,/api/v1/config,/nodeinfo/*). (#477) - fix(security): SSRF DNS-rebinding TOCTOU closed in webhook delivery.
webhook_servicenow pins the resolved public IP on the reqwest client so a low-TTL rebinding record can't flip the target to127.0.0.1or a cloud metadata endpoint after validation. (#473) - fix(security): Stored XSS prevented on public blog sites. The astro-blog template sanitizes rendered HTML with
sanitize-html, and Tiptap'sMarkdown.configure({ html: false })blocks raw HTML from round-tripping through the editor. (#472) - fix(security): Clerk JWT audience and issuer claims validated when configured. Two new optional env vars (
CLERK_EXPECTED_AUDIENCEandCLERK_EXPECTED_ISSUER) enable the checks. Empty strings are treated as "not configured" for backward compatibility, but production now requires pinning (see Breaking changes). (#474) - fix(security): Rustls-webpki upgraded to 0.103.12 to patch RUSTSEC-2026-0098 / 0099 name-constraint bypasses in outbound HTTPS (webhook delivery, Clerk API, S3). The legacy
rustls-webpki 0.101.7remains in the tree via Rocket 0.5.1 and is dormant in production. (#476) - fix(security): Admin npm prod deps audit-cleaned — 0 vulnerabilities remaining. Bumps cover
@clerk/shared,axios, andfollow-redirectsCVEs within existing caret ranges. (#475)
Dependency refresh — Dependabot CVE patches (#488)
15 Dependabot alerts patched across 6 manifests via npm audit fix — lockfile-only updates, no package.json ranges widened:
- e2e:
@clerk/shared≥ 4.8.1 fixes a critical route-protection bypass (GHSA-vqx2-fgx2-5wq9). - docs:
follow-redirects≥ 1.16.0 fixes a cross-domain header leak. libs/{analytics,client,sections}and astro-blog:vite≥ 8.0.5 (or ≥ 7.3.2 for astro-blog) fixes three dev-server vulnerabilities (arbitrary file read via WebSocket, path traversal,fs.denybypass);defu≥ 6.1.5 fixes prototype pollution.
Dependency refresh — coordinated majors (#488)
Wave 1: npm update across every package within existing semver ranges picked up latest patch/minor releases including @tiptap/* 3.22.4, @tanstack/react-query 5.99, react 19.2.5, vitest 4.1.4, eslint-plugin-react-hooks 7.1.1, astro 6.1.8, playwright 1.59.1.
Wave 2 — deliberate major bumps, one commit per package:
| Package | Location | Jump | Impact |
|---|---|---|---|
marked | astro-blog | 17 → 18 | No code changes |
dotenv + @cucumber/cucumber | e2e | 16→17, 11→12 | No code changes |
@stencil/react-output-target | libs/sections | 0.7 → 1.5 (pre-1.0 → stable) | No config changes |
sha2 + hmac | backend | 0.10→0.11, 0.12→0.13 | RustCrypto moved new_from_slice from Mac to KeyInit; fully-qualified paths at 2 call sites |
reqwest | backend | 0.12 → 0.13 | rustls-tls feature renamed to rustls; query/charset/http2 now explicit |
zip | backend | 2 → 8 (6 majors skipped) | No code changes |
@angular/core + zone.js | libs/client | 19→21, 0.15→0.16 | No code changes |
Docusaurus 3.9 → 3.10 was attempted and reverted — the new transitive webpack graph fails at build time with a Progress Plugin options-schema mismatch. The 3.10 bump is deferred to a dedicated PR with docs-build gating.
MUI v7 → v9 across admin (#490)
Single-major jump since MUI skipped @mui/material v8 entirely (7.3.10 → 9.0.0, no v8.x released). Coordinated migration across @mui/material, @mui/icons-material, and @mui/x-date-pickers (8 → 9) using three automated passes plus targeted manual fixes.
The admin was already on Grid v2 syntax, so the biggest v7 → v8 migration concern (Grid item → Grid size) was a no-op. The main mechanical work was:
- Icon renames —
XxxOutline→XxxOutlinedacross 11 files (v9 removed the deprecated aliases). @mui/codemod deprecations/all— 52 files. TextField'sInputProps/inputProps/InputLabelProps/SelectProps→ nestedslotProps, AutocompleterenderTags, list-item-textprimaryTypographyProps, and ~58 more codemods.- Custom system-props-to-sx migration — 101 files. v9 removed
fontWeight,fontFamily,fontStyle,alignItems,justifyContent,flexWrap,display, andtextAlignas direct component props; the script moves them into the nearestsx={{ ... }}, merging with any existingsx.
Test infrastructure tuning to keep the suite stable under MUI v9's heavier bundle: vitest.config.ts switched from pool: 'forks' to pool: 'threads' with maxThreads: 4 so MUI transforms are cached across workers instead of re-run per file, plus widened timeouts. Testing-library's asyncUtilTimeout raised to 5 s.
Result: 0 TypeScript errors (down from 293), 454/454 tests green, react-doctor score 100.
Known upstream-blocked residuals
- Two LOW Dependabot alerts on
rand@0.8.6andrustls-webpki@0.101.7remain, both transitive through Rocket 0.5.1. Crates.io's latest Rocket is also 0.5.1; waiting on Rocket 0.6. Dismiss these aswontfix: upstream-blocked. - Five MODERATE alerts in
templates/astro-blog's@astrojs/check→ yaml-language-server → yaml dev-tooling chain. Dev-only; the remediation is a@astrojs/checkdowngrade that regresses tooling, so left for a focused decision.
v1.2.9 — AI Vision Hotfix
Released 2026-04-01
Fixes two issues discovered immediately after v1.2.8 when testing vision models.
- fix(ai): Allow empty
contentfield for vision actions (auto_tag,alt_text) — these useimage_urlas primary input, not text content. Text actions still require non-empty content. - fix(ai-settings): Model selector dropdown now shows all discovered models when clicked — previously the
freeSoloAutocomplete filtered options by the current value, making it impossible to browse available models.
v1.2.8 — AI Settings Rework + Site-Settings Overhaul
Released 2026-04-01
Major rework of AI settings with per-task model configuration and vision model support, plus comprehensive bug fixes and enhancements across all site-settings pages.
AI Settings (#464)
- feat(ai): Per-task model configuration — assign different models, temperature, and max tokens to each AI task (SEO, excerpt, translate, draft outline, draft post, auto-tag, alt-text)
- feat(ai): Vision model support — new
auto_tagandalt_textactions with multimodal message support for OpenAI-compatible and Anthropic providers - feat(ai): AI Vision actions on media items — "Generate Alt Text" and "Auto Tag" buttons in the media detail dialog for image files
- feat(ai): Model selector auto-loads available models on page mount (removed manual reload button)
- feat(ai): Admin/Owner can change model selection without knowing the raw API key (backend falls back to stored encrypted key)
- feat(ai): New
AiTaskConfigsaccordion UI for per-task overrides with "Vision" chip indicators - feat(ai): Migration:
task_configsJSONB column onsite_ai_configs
Site-Settings Bug Fixes (#465)
- fix(settings): Maintenance mode now saves correctly from Content tab (was missing from PUT payload)
- fix(settings): Preview templates no longer duplicate on every save (3-layer dedup: backend read, backend write, frontend filter)
- fix(settings): Built-in preview templates rendered as read-only with
is_builtinflag - fix(favicon): Client-side file size validation using
max_media_file_sizesetting (no hardcoded limit) - fix(favicon): Favicon package query gated behind
site.favicon_url— eliminates 404 warnings when no favicon uploaded
Site-Settings Enhancements (#465)
- feat(overview): Compact site info bar showing slug, ID (with copy), and creation date
- feat(content): "Advanced Settings" split into 4 distinct sections (Upload Limits, Feature Toggles, Document Security, Preview Templates)
- feat(content): Editorial Workflow toggle hidden when site has fewer than 2 members
- feat(modules): Cards redesigned with colored icon avatars and richer descriptions
- feat(seo): Template variable autocomplete — typing
{{triggers dropdown with 7 variables - feat(seo): Robots.txt predefined template chips (Block AI Crawlers, Block Common Scrapers, Privacy-Focused Default)
- feat(favicon): "Choose from Media Library" button alongside drag-and-drop upload
- feat(favicon): Image cropping for non-square images via
react-easy-crop - feat(code-injection): Syntax-highlighted editor using lowlight + highlight.js dark theme
- feat(global): Maintenance mode warning banner on every dashboard page with "Turn Off" button
- feat(global): Max-character indicators on all text input fields across settings
System Admin
- feat(admin): New
GET /admin/sites/overviewendpoint returning combined per-site metrics (maintenance mode, member count, storage) - feat(admin): System Sites page rewritten with paginated, filterable table
Client Library
- feat(client): Added
getCodeInjection()toSiteResourceandrenderCodeInjection()helper for template embedding - feat(client): Exported
CodeInjectiontype
i18n
- fix(i18n): Fixed de-AT.json: 42 obsolete settings keys replaced with current structure
- feat(i18n): 30+ new keys added across all 11 locales for AI tasks, vision actions, and settings enhancements
New Dependencies
react-easy-crop— interactive image cropping for favicon uploads
v1.2.5 — npm Publishing and Package Rename
Released 2026-03-31
Renamed all library packages from @forja/* to @forjacms/* and added automated npm publishing on version tag push.
Package Rename
- chore(libs): Renamed
@forja/analytics,@forja/client,@forja/sections,@forja/sections-reactto@forjacms/*
npm Publishing
- feat(ci): Unified
npm-publish.ymlworkflow publishes all 4 packages to npmjs.com onv*tag push - chore(libs): Added
repository,homepage,bugsmetadata to all library package.json files - chore(libs): Created
@forjacms/sections-reactREADME - chore(scripts): Bump script auto-updates
@forjacms/sectionspeer dependency in sections-react
Fixes
- fix(docs): Synced
docs/package-lock.json(typescript version mismatch broke deploy) - feat(admin): Demo instance warning banner when
DEMO_MODEis active (non-guest users only) - refactor(admin): Extracted
appConfigto own module to prevent bootstrap side effects in tests - fix(admin): Silenced 77 TanStack Query "undefined data" warnings in test output
v1.2.4 — Welcome Page Cleanup and Dependency Updates
Released 2026-03-31
Welcome page polish and a comprehensive dependency update sweep across the monorepo.
Welcome Page
- chore(admin): Removed all Fediverse/Federation references from welcome page and documentation
- chore(admin): Changed CTA from "Get Started Free" to "Sign up now" across all 11 locales
- chore(admin): Removed "Created by" attribution from footer, added visual separator
- chore(admin): Updated mock browser URL to
cms.dorfstetter.at - chore(admin): Widened hero section with earlier responsive breakpoint (1200px)
Dependencies
- chore(deps): ESLint 9→10 with
@eslint/compatbridge; react-hooks 5→7, globals 16→17 - chore(deps): i18next 25→26, react-i18next 16→17, react-markdown 9→10
- chore(deps): Tiptap 3.21, TanStack Query 5.96, React 19.2.4, recharts 3.8, Vite 8.0.3, Vitest 4.1.2
- chore(deps): Astro 6.1.2, Docusaurus 3.9.2, Playwright 1.58.2
- chore(deps): 22 Cargo crate updates (hyper 1.9, uuid 1.23, mio 1.2)
Test Stability
- fix(admin): Resolved 5 flaky test timeouts by moving dynamic imports to
beforeAll - fix(admin): Increased timeouts for lazy-loaded editor and chart components
Documentation
- chore(docs): Removed federation references from READMEs, architecture, admin guide, testing, API docs
v1.2.2 — Landing Page Redesign, Guest Demo Mode, and Leave Site
Released 2026-03-30
Redesigned the unauthenticated welcome page with modern 2025 patterns, added read-only guest demo access, and enabled users to leave sites.
Welcome Page Redesign (#434)
- feat(admin): Asymmetric hero layout with gradient "built in Rust" headline and 3D-perspective dashboard wireframe mock
- feat(admin): "Try the Demo" promoted to primary CTA button with distinct violet styling
- feat(admin): Bento grid differentiators — Privacy by Design, Rust Performance, Multi-Tenant, Developer-First with side-slide animations
- feat(admin): Bento grid feature tiles — 10 features with scale-up reveal, "Lightning Fast" and "AI Content Assist" highlighted
- feat(admin): New "Ship faster with official libraries" section showcasing
@forjacms/client,@forjacms/analytics,@forjacms/sections,@forjacms/sections-reactwith code snippet - feat(admin): 3D wireframe puzzle piece background element rotating with scroll (multi-layered SVG, screen blend mode)
- feat(admin): Social proof badges (Rust, React 19, PostgreSQL, TypeScript, GDPR) integrated inline in hero
- feat(admin): Section backgrounds with alternating subtle tints replace gradient dividers
- feat(admin): AGPL license badge in hero
- feat(admin): Privacy-first positioning — "Self-host for free, forever" across all 11 locales
- refactor(admin): Extracted
WelcomeHero,DashboardMock,WelcomeLibraries,WelcomePuzzlecomponents for single-responsibility
Guest Demo Mode (#449, #450)
- feat(backend):
DEMO_MODEenvironment variable — auto-seeds a demo site and API key on startup - feat(backend):
GET /auth/guest-tokenendpoint returns a read-only API key for unauthenticated visitors - feat(admin): Guest mode renders the full dashboard in read-only mode without Clerk authentication
- feat(admin): "Try the Demo" button on the welcome page (visible only when
DEMO_MODEis enabled) - fix(admin): Prevent duplicate React root and ClerkProvider on HMR in guest mode
Leave Site (#448)
- feat(admin): "Leave Site" option in user account menu
- feat(backend): Users can remove themselves from a site membership
v1.2.1 — Web Components, SDK Expansion, and Locale Filtering
Released 2026-03-27
Major release introducing framework-agnostic Web Components for page sections, expanding the client SDK to cover all public read endpoints, and fixing server-side locale filtering across the full stack.
@forjacms/sections — Web Components Library (NEW)
- feat(sections): Rewrote
@forjacms/sectionsfrom Astro-only components to framework-agnostic Stencil Web Components (#414–#420) - 24 components: Hero, Features, CTA, Gallery, Testimonials, Pricing, FAQ, Contact, Stats, Team, Timeline, LogoCloud, Newsletter, Video, Divider, Text, Portfolio, TagCloud, Projects, Blog, Legal, Nav, Footer, SectionRenderer
- Light DOM rendering (
shadow: false) with BEM class hooks for styling - Auto-generated
@forjacms/sections-reactwrapper package @forjacms/sections/definebarrel import for Vite/Webpack bundler compatibility- 107 component tests via
@stencil/vitest
@forjacms/client — SDK Expansion
- feat(client): Added
ProjectsResource—listPublished(),get(),getBySlug() - feat(client): Added
RedirectsResource—lookup()for SSR redirect middleware - feat(client): Added
site.listLocales()for language switcher data - feat(client): Added
media.list()with search, MIME category, and folder filters - feat(client): Added
legal.getDetail()andlegal.listVersions()for full document rendering - feat(client): Added
taxonomy.getTag(),getTagBySlug(),getCategory(),getCategoryChildren() - feat(client): Added
LocaleFilterParamstype —localeIdparam onblogs.listPublished()andblogs.listByCategory() - 145 tests, 94.85% coverage
Backend
- feat(api): Added
locale_idquery param toGET /sites/{id}/blogs/publishedandGET /sites/{id}/blogs/published/category/{slug}— filters to blogs with content in the specified locale, with accurate pagination counts
Astro Blog Template
- feat(template): Replaced Styled*.astro section components with
@forjacms/sectionsWeb Components - feat(template): Server-rendered Nav and Footer using BEM classes (same CSS as web components)
- feat(template): Legal pages now use
<forja-legal>component with metadata display - fix(template): Blog pagination locale inconsistency — page 1 and page 2+ now use same locale source
- fix(template): Category pages now filter by locale and support
?page=Npagination - fix(template): Similar blogs now filtered by active locale
- fix(template): BlogCard date format respects active locale instead of hardcoded
en-US - fix(template): Dark mode now works in sections.css via
@reference "./global.css" - fix(template): Social links fetch errors logged instead of silently swallowed
- fix(template): Dockerfile includes
@forjacms/sectionsbuild step
v1.2.0 — Portfolio, Webhooks v2, and Module Modernization
Released 2026-03-25
Major release modernizing four core admin modules, introducing Projects as a new content type, and upgrading the webhook system with debounce, templates, and analytics.
Portfolio (formerly CV)
- feat(portfolio): rename CV module to Portfolio — sidebar, routes (
/cv→/portfolio), site setting (module_cv_enabled→module_portfolio_enabled), and all i18n keys updated. Old/cvroute redirects automatically - feat(portfolio): add Projects as a new content type — first-class portfolio items with slug, featured flag, date range, publishing workflow (Draft → InReview → Scheduled → Published → Archived), and display ordering
- feat(portfolio): project localizations — per-locale title, short description, and full description
- feat(portfolio): project links — typed links (source, demo, documentation, website, other) with inline editor
- feat(portfolio): project media — image gallery with display order and cover flag via media picker
- feat(portfolio): project relations — link projects to skills and CV entries for a connected portfolio graph
- feat(portfolio): 3-step project wizard — Basics (title, slug, dates, featured), Content (descriptions, links), Relations (media, skills, CV entries)
- feat(portfolio): 4-step CV entry wizard — Company (name, logo, location, URL), Timeline (dates, is_current), Content (position, description, achievements per locale), Skills (linked skill IDs)
- feat(portfolio): project actions menu — publish, unpublish, archive, restore with status transitions
- feat(portfolio): backend API for projects — full CRUD, paginated listing with search/status/featured filters, bulk reorder, media/link/relation sub-endpoints, webhook triggers (
project.created,project.updated,project.deleted,project.published)
Webhooks v2
- feat(webhooks): 27 webhook events — expanded from 10 to 27 events covering all content types. New events:
blog.published,page.published,legal.published,legal.created,legal.updated,legal.deleted,cv.created,cv.updated,cv.deleted,cv.published,project.created,project.updated,project.deleted,project.published,navigation.created,navigation.updated,navigation.deleted - feat(webhooks): debounce support — configurable
debounce_seconds(0–300) per webhook. Events within the debounce window are accumulated into a JSONB buffer and delivered as a singlebatchevent withevent_countandbatch_window_secondsmetadata - feat(webhooks): flush worker — background Rocket fairing polling every 5 seconds for ready buffers (batch size 10). Automatic cleanup of flushed buffers older than 24 hours
- feat(webhooks): hosting platform templates — pre-configured templates for Vercel, Netlify, and Cloudflare deploy hooks with URL validation patterns, default events, and 30-second debounce. Plus a Custom template for arbitrary endpoints
- feat(webhooks): template picker UI —
WebhookTemplatePickercomponent auto-detects hosting provider from URL and pre-fills webhook configuration - feat(webhooks): analytics dashboard —
GET /webhooks/{id}/stats?window=<1h|24h|7d|30d>endpoint returning total deliveries, success rate, pending retries, and per-event breakdown. Admin UI with color-coded summary cards and event table - feat(webhooks): i18n for all webhook template, debounce, and analytics keys across all 11 locales
Legal Module Modernization
- feat(legal): single tabbed page — Legal Documents and Cookie Consent tabs with icons, replacing the previous flat listing
- feat(legal): creation wizard — single-step guided wizard for new legal documents with auto-generated slug
- feat(legal): version history panel — view previous versions of legal documents with create-new-version action
- feat(legal): cookie consent management — full CRUD page for cookie consent groups and items with drag-and-drop reordering
- feat(legal): status filter — filter legal documents table by status (Draft, Published, etc.) with empty-table state instead of vanishing content
- feat(legal): bulk actions — context-sensitive row actions and pagination in documents table. Publish/unpublish added to cookie consent
- feat(legal): publish gate validation for legal documents
- feat(legal): detail, version, and localization API endpoints
- fix(legal): deep-linkable tabs and context-aware breadcrumbs
- fix(legal): breadcrumbs and back link during loading/error states
- fix(legal): group action buttons moved out of AccordionSummary
- fix(legal): cookie group reordering — assign sequential positions on move
- fix(legal): arrow up/down for group reorder, simpler delete confirm, smart bulk actions
Navigation Modernization
- feat(admin): navigation wizard — module-aware creation wizard with Internal/External link toggle
- feat(admin): page picker — resolve page UUIDs to routes in the navigation table
- feat(admin): blog picker — select blog posts as navigation targets
- feat(admin): legal picker —
LegalPickerreplaces hardcoded legal document selector - feat(admin): modernized navigation management — wizard, tree view, and inline editing
- feat(admin):
data-testidattributes, i18n locale display, and row info tests for navigation - fix(admin,backend): inline external icon, fix locale count in navigation rows
- fix(backend): allow relative paths in navigation
external_url
Editor
- feat(editor): Zen Mode — distraction-free fullscreen editing overlay (720px max-width, centered). Toggle via toolbar button, slash command (
editor:toggle-zen), or Esc to exit. Fade animation with fixed positioning
Trash Expansion
- feat(admin): extend trash page for legal documents, social links, and navigation items
- feat(backend): soft-delete columns and models for legal documents, social links, navigation menus, and navigation items
- feat(backend): trash cleanup extended to purge all soft-deleted entity types
Security and Infrastructure
- feat(security): per-site CORS allowed origins in site settings
- fix(security): nonce-based CSP for dashboard styles — upgrade Vite to v8
- fix(security): remove
unsafe-inlinefrom dashboardscript-srcCSP - fix(security): move API key from localStorage to sessionStorage
- fix(security): pass CSP nonce to MUI Emotion caches
- chore(deps): migrate to Zod 4 and TypeScript 6
- chore(deps): migrate libs to tsdown, upgrade Node to 24
- chore(deps): fix 3 of 4 Dependabot security alerts
- chore: lighthouse optimizations — a11y, caching, SEO, security headers
- chore: resolve react-doctor warnings
Fixes
- fix(admin): label overlap on template pre-fill + Enter to confirm delete
- fix(admin):
isOwnerrace condition and inconsistent Add Member role filter - fix(admin): resolve infinite re-render in page sections tab
- fix(admin): use array instead of fragments in
UserAccountMenuand legalMenuchildren - fix(template): resolve media URLs to backend origin in preview mode
- fix(template): read
PREVIEW_TOKEN_SECRETat runtime viaprocess.env - fix(template): lazy-init
ForjaClientto fix preview service crash - fix(template): resolve all
astro checkwarnings and TypeScript errors - fix(i18n): add CORS allowed origins translations to all locales
- chore(admin): remove dead
NavigationFormDialog
v1.1.6 — Sections Library, AI Improvements, and Welcome Redesign
Released 2026-03-23
Sections Library
- feat(sections): add
@forjacms/sectionslibrary with headless, accessible Astro components for rendering Forja page sections — zero CSS, bring your own styles - feat(sections): add 8 new page section types: Stats, Team, Timeline, LogoCloud, Newsletter, Video, Divider, Text
AI Content Assist
- fix(ai): generate content in site default language regardless of input language (#349)
- feat(ai): add insert-at-position and regenerate outline buttons to blog wizard (#349)
Admin Dashboard
- feat(admin): show user role prominently in sidebar header (#350)
- feat(admin): redesign welcome page with clear value proposition and static feature grid (#358)
- feat(admin): add "Why Forja" comparison and use case sections to welcome page (#359)
Documentation
- docs: documentation completeness audit — fix stale schema, fill developer guide gaps, improve navigation structure (#364)
v1.1.5 — Trash, Focal Point, and Reliability
Released 2026-03-22
Trash and Soft-Delete
- feat(trash): trash view with restore and batch operations — soft-deleted blogs and pages appear in a dedicated Trash page with restore, permanent delete, select-all, and "Empty Trash" actions. Includes auto-purge countdown badges (#226, #340)
- fix(trash): deleted media files and documents now appear in trash — extended the trash handler with a UNION query across contents, media_files, and documents tables. Documents converted from hard-delete to soft-delete with new
is_deleted/deleted_atcolumns. Media storage cleanup deferred to permanent delete to enable restore (#341, #343)
Media Focal Point
- feat(media): image focal point selection for responsive crops —
focal_x/focal_ycolumns (REAL, default 0.5) with a clickable overlay picker in the media detail dialog. Thumbnail variants now crop to 1:1 centered on the focal point. Grid card thumbnails respect focal point via CSSobject-position. Includes 8 crop math unit tests and keyboard accessibility (#235, #342)
Storage Quota
- feat(storage): per-site storage quota with usage visibility — storage usage bar in site settings, quota enforcement on upload, and a sysadmin quota selector (100MB–50GB) (#233, #339)
API Key Improvements
- feat(api-keys): replace burst-based rate limits with quota-based model — monthly quota per key with daily usage aggregation, usage summary endpoint, redesigned quota dashboard widget (#333, #338)
- feat(api-keys): anomaly detection with auto-blocking — hourly/daily spike detection and error rate monitoring. Compromised keys are automatically blocked with audit logging
- feat(api-keys): expiry warnings on dashboard and notifications — proactive alerts for keys approaching expiration
Content SDK
- feat(sdk):
@forjacms/client— typed TypeScript SDK for the consumer content API with pagination helpers, error mapping, and framework-agnostic design (#230) - feat(sdk):
@forjacms/client/angular— Angular adapter with DI integration (provideForja,injectForja) and signal-basedforjaResource()helper (#328)
Reliability Fixes
- fix(webhooks): persist webhook dispatch to queue before delivery — replaced fire-and-forget
tokio::spawnwith queue-based dispatch. Events are written towebhook_retry_queuebefore the handler returns, surviving server restarts (#344, #347) - fix(notifications): persist notifications synchronously — all four
notify_*functions converted from fire-and-forget to synchronous DB writes. Reviewers and authors no longer miss notifications on server crash (#345) - fix(federation): retry transient key errors instead of marking dead — encryption key resolution and RSA decryption failures now schedule retry with exponential backoff instead of permanently killing the delivery job (#346)
Other
- feat(audit): audit log retention policy — daily background cleanup of old audit logs, configurable per site (default: 365 days) (#234)
- feat(command-palette): content search, missing nav commands, and broken action fixes (#224)
- fix(docker): add
@forjacms/clientlib to astro-blog Docker build; fix analytics lib build order (#332, #323, #324) - fix(security): resolve RUSTSEC-2026-0049 rustls-webpki vulnerability (#326)
v1.1.4 — Draft Preview
Released 2026-03-20
Draft Preview System
- Preview token endpoint — new
GET /sites/:id/preview-tokengenerates short-lived JWTs (5 min, HMAC-SHA256) for authenticated draft content access. Requires ReadKey auth (#229) - Preview token as API auth —
X-Preview-Tokenheader accepted as a third authentication strategy (alongside Clerk JWT and API keys). Creates a read-only identity scoped to the token's site — all existing ReadKey endpoints work automatically - Admin preview integration —
usePreviewUrlhook fetches a preview token before opening the preview URL. Falls back to direct URL if token generation fails. Preview URLs use/preview/blog/{slug}?token=...format - Astro preview route — new
/preview/blog/[slug]page validates the JWT, fetches content dynamically using the token's site_id, and renders with a "Preview Mode" banner, status badge, favicon, andnoindexheaders
Built-in Preview Service
- Dockerfile — multi-stage Node 24 build with non-root
forjauser for the Astro blog template - docker-compose — preview service added to
docker-compose.dev.yamlon port 4321 - Template registry — new
APP__PREVIEW__BUILT_IN_TEMPLATESconfig (name|urlformat). Built-in templates are auto-injected into every site's preview dropdown — no user configuration needed - Multi-site support — preview service uses dynamic API client (
preview-api.ts) that authenticates viaX-Preview-Tokenand derives site_id from the token. One deployment serves previews for all sites - Zero-dependency JWT validation — Astro template validates preview tokens using Node's built-in
crypto.createHmac()(no npm JWT library)
Documentation
- Updated authentication architecture docs for the permission model (resource:action:scope format, role-to-permission table, ownership enforcement)
- Updated blog API endpoint docs with granular permissions and slug auto-generation notes
v1.1.3 — Security Hardening
Released 2026-03-20
Comprehensive security audit and hardening pass. All findings from two independent security reviews addressed.
Access Control Fixes
- Audit log authentication — three audit endpoints (
GET /sites/<id>/audit,GET /audit/entity/<type>/<id>,GET /audit/history/<type>/<id>) were accessible without authentication. AddedReadKeyguards andPermissionServicesite-level authorization (#313) - IDOR in taxonomy handlers —
GET /taxonomies/<id>,GET /categories/<id>lacked site ownership checks. AddedPermissionService::require()(#298) - IDOR in CV/skill handlers —
GET /skills/<id>,GET /cv-entries/<id>lacked site ownership checks (#299) - IDOR in legal document handlers —
GET /legal/<id>lacked site authorization (#300) - IDOR in media read endpoints —
GET /media/<id>missing site ownership check (#284)
Injection & XSS Prevention
- SQL injection hardening — centralized
ORDER BYconstruction to prevent column injection viasort_byparameters. All 11 model files migrated to sharedorder_clause()utility (#301) - AI prompt injection defense — sandwich defense pattern for AI content generation: system prompt boundaries, input sanitization, output validation (#303)
- SVG stored XSS mitigation — SVG files served with
Content-Disposition: attachmentto prevent browser rendering of embedded scripts (#290) - CSP nonce-based scripts — replaced
unsafe-inlineinscript-srcwith nonce-based CSP for dashboard routes. Clerk SDK scripts use dynamic nonce injection (#304)
SSRF Protection
- Webhook delivery SSRF — added private IP validation (RFC 1918, loopback, link-local, RFC 6598) to outbound webhook URLs. Blocks
localhost,127.0.0.1,::1,10.*,172.16-31.*,192.168.*, and169.254.*(#285) - AI service SSRF — same private IP validation applied to AI provider
base_urlconfiguration (#286) - Federation worker SSRF — added target URL validation to ActivityPub delivery worker (#288)
Cryptographic Improvements
- Argon2id API key hashing — upgraded from SHA-256 to Argon2id with transparent auto-upgrade on successful validation. Legacy keys migrated on next use (#302)
- Constant-time SHA-256 comparison — legacy SHA-256 verification path now uses
subtle::ConstantTimeEqto prevent timing oracle attacks (#314)
Infrastructure Hardening
- Security headers — added
Strict-Transport-Security(HSTS),Permissions-Policy(camera, microphone, geolocation, payment disabled) to all responses (#289) - ActivityPub signature verification — inbox endpoint now requires valid HTTP signatures, rejecting forged activities (#287)
- LocalStorage path canonicalization — added defense-in-depth path traversal protection with component validation and
canonicalize()+starts_with()boundary checks (#315)
Dependencies
- aws-lc-sys — updated to 0.39.0 for CVE fixes (#307)
Documentation
- Added quality gates, accessibility guidelines, error codes, and security documentation pages
- Updated coding standards, testing patterns, CI/CD docs, contributing guide, sidebar, and READMEs
v1.1.2 — SEO, RBAC, and User Moderation
Released 2026-03-19
SEO Foundation
- Site URL configuration — new
base_urlfield on sites for canonical URL identity (sitemaps, SEO tags, preview links). Configurable in Site Settings > Overview with URL validation - XML sitemap endpoint — public
GET /sites/<slug>/sitemap.xmlwith multi-locale alternate links and 1-hour cache - robots.txt configuration — structured robots.txt rules as a site setting with a public
GET /sites/<slug>/robots.txtendpoint. Admin UI in the SEO tab with add/remove user-agent blocks, Allow/Disallow directives, live preview, and auto-appended Sitemap directive - Site-level SEO defaults — fallback
meta_title(template with{{title}}/{{site_name}}),meta_description, andog_image_url(cascade: cover image > default OG > site logo) applied at response time in blog and page detail endpoints. Admin UI with live title preview, character-counted description, and media picker for OG image - Custom code injection —
code_injection_headandcode_injection_footersite settings for injecting analytics, verification tags, chat widgets, or tracking scripts. Admin Code Injection tab with monospace editors, 10,000-char limit, and security warning banner - Favicon package generation — upload a source image to generate
favicon.ico(multi-size 16/32/48), individual PNGs, Apple Touch Icon (180x180), and Android Chrome icons (192/512). PublicGET /sites/<slug>/site.webmanifestandGET /sites/<slug>/browserconfig.xmlendpoints. Admin Site Icon tab with drag-drop upload, variant preview grid, theme/background color pickers, and copy-to-clipboard HTML<head>snippet. Downloadable zip with all variants plus manifest files
Fine-Grained Access Control (RBAC)
- Permission model — new
PermissionServicewith{resource}:{action}[:{scope}]format (80+ permissions). All 30 handler files migrated from role-basedauthorize_site_action()to permission-basedPermissionService::require(). Ownership enforcement (:own/:any) and published content protection (:published) - Frontend permission model —
/auth/meexposes resolved permission strings per membership. NewusePermissions()hook withcan(),canAny(),canAll()replaces role-rank comparisons - Permission caching — Redis-backed cache for resolved permission sets (5-min TTL, configurable via
PERMISSION_CACHE_TTL_SECS). Cache invalidated immediately on membership create/update/delete. Graceful degradation when Redis is unavailable
Content Authoring Quality
- Slug auto-generation —
slugfield optional in blog/page creation. Auto-generated from title (blogs) or route (pages) with Unicode transliteration and-2/-3suffix deduplication - Publish-gate validation — blocks publishing incomplete content (no title, no body for blogs, no sections for pages). Non-blocking SEO warnings for missing meta_title
- Excerpt fallback — content without an excerpt now auto-generates one from the body by stripping HTML/Markdown, collapsing whitespace, and truncating at a word boundary (160 chars). Applied in all localization responses and RSS feed descriptions
- Auto-unpublish cron — publish scheduler now archives content when
publish_endhas passed (Published → Archived). Fires*.archivedwebhook events and creates audit log entries
Media & Webhooks
- Media usage tracking —
GET /media/{id}/usagereturns all references across blogs, pages, and sites.DELETE /media/{id}returns 409 Conflict when media is in use (?force=trueto override) - Webhook automatic retry — failed deliveries retried with exponential backoff (0s, 5m, 30m, 2h, 12h, 48h — 6 attempts). Database-backed retry queue with background worker. Manual retry via
POST /webhooks/deliveries/{id}/retryfor dead deliveries
User Moderation
- Moderation data model — new
user_moderationtable withactive/suspended/bannedstatus per Clerk user. Supports suspension with duration + reason, permanent bans, and expiry-based auto-unsuspend queries - Moderation API — new endpoints:
POST /admin/users/{id}/suspend(with reason + duration),POST /admin/users/{id}/ban,POST /admin/users/{id}/unsuspend,DELETE /admin/users/{id}(permanent deletion of banned users). All require system admin. Audit logged - Auth-level moderation blocking — suspended/banned users receive 403 with
ACCOUNT_SUSPENDED/ACCOUNT_BANNEDerror codes on any API call. Expired suspensions auto-lift on next request - Moderation UI — system users list shows real moderation status (Active/Suspended/Banned) with color badges and reason text. Context-aware action menu: active users can be suspended or banned, suspended users can be unsuspended or banned, banned users can only be deleted. Delete requires safeword confirmation via the standard
ConfirmDialog - User detail page — new
/system/users/:idpage with profile card (avatar, name, email, status badge, moderation reason) and paginated activity timeline from audit logs
Other Improvements
- Audit log enhancements — new action types (SettingsUpdate, PermissionDenied, OwnershipTransfer, Export).
RequestContextcaptures IP address and User-Agent. Site settings changes logged with changed keys - Legal document cloning — new
POST /legal/{id}/cloneendpoint clones a legal document with all groups, items, and localizations as a new Draft - GDPR export completeness —
GET /auth/exportnow includes preferences, notifications, onboarding state, help state, and authored content summary
Fixed
- Permission 403 on all updates —
has_permission("blog:update")now checks scoped variants (:own,:any) - Cross-site access leak —
resolve_permissions()returns empty set when user has no membership on a site - Missing
site:updatein Admin tier permissions - Route collision between
POST /webhooks/retry/<id>andPOST /webhooks/<id>/test - Hardcoded English text in user moderation UI — all strings use i18n with proper translations for all 11 locales
v1.1.1
Fixed
- Site Launcher inaccessible for single-site users — auto-redirect now only fires on first visit, not when explicitly navigating via "Switch site"
- Dashboard pie chart not rendering — chart container given explicit height so recharts
ResponsiveContainercan size correctly
v1.1.0 — Navigation Redesign
Added
- Site Launcher — full-page site selection replaces the top-bar dropdown; card layout with role badges, auto-redirect for single-site users
- Sidebar restructure — Workspace and Administration zones; Documents/Legal visible in Content section
- Preferences Drawer — theme, language, autosave, page size in a slide-out drawer from the avatar menu
- Site Settings sub-pages —
/site-settings/*routes with tab navigation (Overview, Content, Modules, AI) - System Administration —
/system/*area for sysadmins: health dashboard, all-sites CRUD, user management, languages, federation - World languages seed — ~90 languages with regional variants (de-AT, en-US, fr-CA, es-MX, ar-SA) and fictional languages (Klingon, Esperanto, Latin)
- Language management UI — active languages as cards + searchable catalog table; safety dialog for deactivation
- Arabic, Ukrainian, Wienerisch — three new admin interface languages (11 total)
- RTL layout support — full right-to-left UI via
stylis-plugin-rtlfor Arabic - Localized dates — all date/time formatting respects the active UI language
Changed
- Monolithic Settings page dissolved — all tabs redistributed to dedicated routes
/settings→/site-settings,/clerk-users→/system/users(redirects)- AI Settings and Federation Settings use full layout width
- 75 missing i18n keys added across all languages
Fixed
- Mixed-language relative dates ("Aktualisiert vor about 2 hours" → "vor 6 Tagen")
- German dative grammar in relative time
- US date format in non-English locales
- Wienerisch auto-detection from browser locale
- PreferencesDrawer z-index overlap
v1.0.14
Added
- Copy public URL -- media and document cards now have a copy-to-clipboard button for the public shareable URL (including password-protected documents)
- Bulk selection -- checkbox overlay on media and document cards with floating action bar for bulk delete
- Document password policy -- new site settings for minimum password length and regex pattern, enforced on backend (via
fancy-regexfor lookahead support) and validated in real-time on frontend - Password auto-generation -- secure password generator in document upload dialog and set-privacy dialog, respects site policy (length + regex), with regenerate and show/hide toggle
Changed
- Document upload dialog -- drag-and-drop file zone (matching media UX), auto-derived document type from filename, removed display_order field, folder prefilled when browsing
- Card design -- redesigned media and document cards with better visual hierarchy, larger image previews, always-visible actions
- Delete confirmation -- simplified to a single confirm button (removed "type DELETE" requirement) for media and documents
- Document password page -- Forja-branded standalone page with logo, Outfit font, dark mode support, AES-256 badge, file info card; extracted to HTML template (
resources/templates/document_password.html)
Fixed
- CSP blocking password page --
default-src 'self'on API paths was blocking inline styles/scripts; added targeted CSP exception for document download pages
v1.0.13
Changed
- Native arm64 Docker builds -- replaced QEMU-emulated cross-compilation with parallel native builds using GitHub's
ubuntu-24.04-armrunners. Build time drops from ~3.5 hours to ~30 minutes. Each platform pushes a digest independently, then a merge job creates the multi-platform manifest.
v1.0.12
Fixed
- Docker publish -- fixed
reqwestversion constraint (0.13.2→0.12); reqwest 0.13 removed therustls-tlsfeature, causingcargo-chef cookto fail in Docker builds - Docs deployment -- added
actions/configure-pagesstep and allowedv*tag deployments in the GitHub Pages environment policy
v1.0.11
Added
- Password-protected documents -- documents can now be encrypted with AES-256-GCM using a user-provided password. Encrypted files are stored at rest and require a password to download. Includes time-limited HMAC-signed access tokens (1 hour), admin recovery via optional server-side key wrapping, and key rotation support. New endpoints:
POST /documents/{id}/privacy,DELETE /documents/{id}/privacy,POST /documents/{id}/verify-access - Federation health check --
/healthendpoint now reports federation status (up,down, ordisabled) alongside existing PostgreSQL and Redis checks - Dashboard status badges -- new AI Enabled (purple) and Federated (blue) badges in the dashboard header indicate active modules at a glance
- Single instance block dialog -- manually add a blocked instance via a dialog with DNS-compliant domain validation and an optional reason field
- Blocklist CSV template & export -- download a pre-formatted Mastodon-compatible CSV template, or export your entire blocklist as CSV
- Federation domain info card -- new card in Federation Settings showing the actor URI and WebFinger address
Fixed
- CSV parser handles quoted fields -- blocklist import now correctly parses RFC 4180 quoted fields containing commas, quotes, or newlines
- Discover models uses stored API key -- AI model discovery falls back to the stored API key when the form field is empty
- Analytics toggle moved to Modules tab -- previously under Site Settings, now correctly grouped with other module toggles
- Settings layout -- site settings use a flattened layout with full-width embedded pages
- S3 media proxy -- media files are now proxied through the backend to avoid ACL issues on direct S3 URLs
Changed
- Security scanning -- replaced CodeQL with Trivy (filesystem scan), cargo-audit (Rust CVEs), and npm audit (Node.js CVEs). Runs on push, PRs, and weekly schedule
- React Doctor score 100/100 -- lazy-loaded recharts via
React.lazy, split FederationSettings into focused sub-components, stabilized array keys, removed unused exports - Federation settings layout -- 2-column layout with signature algorithm and key rotation side-by-side; blocklist full-width below
- Dependency updates: react-i18next, @types/node, @vitest/coverage-v8, @tiptap/extension-image, fake, dorny/paths-filter
v1.0.10
Added
- End-to-end testing framework -- Cucumber (Gherkin BDD) + Playwright e2e test suite in
e2e/. 49 active scenarios covering all 7 roles (Viewer, Reviewer, Author, Editor, Admin, Owner, System Admin) with real Clerk authentication via@clerk/testing. Includes Docker Compose for isolated test DB, seed scripts, and documentation screenshot capture data-testidattributes across admin UI -- added stable test selectors to 28 components (pages, shared components, dialogs) for reliable e2e element targeting- Clerk test user provisioning script --
e2e/scripts/create-test-users.shcreates 7 role-based test accounts via Clerk's Backend API - Configurable rate limit fail mode -- new
RATE_LIMIT_FAIL_MODEenv var (openorclosed) controls whether requests are allowed or rejected when Redis is unavailable - Proxy-aware IP extraction -- new
TRUST_PROXY_HEADERSenv var enables reading client IPs fromX-Forwarded-ForandX-Real-IPheaders for correct rate limiting behind reverse proxies
Fixed
- Federation domain using per-site domains -- federation now uses the app public URL (
PUBLIC_URL) for the federation domain instead of per-site custom domains, ensuring consistent actor identities - Logging severity levels -- adjusted log levels across the backend for clearer operational diagnostics; raised Clerk rate limits and added a 429 status catcher
- Missing RBAC guards on mutation handlers -- all mutation API handlers now enforce
WriteKey/AdminKeyauthorization - Missing RBAC guards on Webhooks page -- admin UI Webhooks page now respects role-based access control
- Missing RBAC guards on Redirects page -- admin UI Redirects page now respects role-based access control
Changed
- Updated rate-limiting, authentication, and configuration documentation to cover new options
- Updated developer testing guide with full e2e setup instructions
- Updated README with e2e project structure and commands
- Comprehensive documentation gap fix and URL migration
v1.0.9
Fixed
- S3 uploaded files returning 403 Unauthorized --
put_object()calls now explicitly setpublic-readACL so media files are publicly accessible via their URLs
v1.0.8
Fixed
- Federation handle showing
@user@localhost-- centralized domain resolution intoSite::resolve_domain()with smart fallback chain (primary production → any production → any active domain). Returns an error instead of silently defaulting tolocalhost - Blocked instances not displaying after import -- backend
list_blocked_instancesnow returns paginated response matching frontend format - Unblock instance silently failing -- frontend now sends domain string instead of UUID
- Mastodon CSV blocklist import -- handles multi-column CSV export format (skips headers, extracts first column)
- Docusaurus baseUrl -- changed from
/forja/to/
Added
- Edit block reason --
PUT /sites/:id/federation/blocks/instances/:domainendpoint and edit button in admin UI - Clear entire blocklist --
DELETE /sites/:id/federation/blocks/instancesendpoint (Owner only) with confirmation dialog - Full i18n support for new blocklist features across all 8 languages
Changed
- Docker build optimization --
cargo-chefdependency caching pattern reduces rebuild time from ~2 hours to ~20 minutes - CI Docker build uses Buildx with GitHub Actions layer cache
- Consolidated 7 duplicate domain resolution queries into shared
Site::resolve_domain()
v1.0.7
Added
- Welcome page -- branded landing page for unauthenticated visitors with Forja logo, tagline, feature carousel, and Sign In / Register buttons
- Auto-scrolling horizontal carousel showcasing 8 platform features
- Full i18n support across all 8 languages (EN, DE, FR, ES, IT, PT, NL, PL)
- Language selector for switching locale before login
- Creator credit, EU badge, version number, GitHub and Docs links in footer
- Uses Clerk hosted redirect (CSP-safe) instead of embedded sign-in components
Fixed
- CSP blocking Clerk on custom FAPI domains -- Content-Security-Policy for
/dashboardnow dynamically includes the Clerk Frontend API domain extracted fromCLERK_PUBLISHABLE_KEY. Custom Clerk domains (e.g.clerk.dorfstetter.at) no longer cause blank pages. OptionalCLERK_FAPI_DOMAINenv var for explicit override. - Docker tag immutability conflict -- removed
major.minorDocker tag from CI to avoid push conflicts on patch releases
Changed
- Root route (
GET /) now redirects to/dashboardinstead of returning API version string ENCRYPTION_KEYandCLERK_FAPI_DOMAINdocumented in.env.example- App version injected at build time via Vite
definefrompackage.json
v1.0.6
Added
- ActivityPub Federation -- full Fediverse integration for blog syndication (#45)
- WebFinger discovery, Actor profile, Inbox/Outbox protocol endpoints
- HTTP Signature creation and verification (RSA-SHA256 + Ed25519)
- 6-layer security pipeline on inbound activities (rate limiting, block checks, signature verification, payload validation, content sanitization)
- SSRF protection on all outbound HTTP requests
- Background delivery worker with PostgreSQL queue (Redis-ready with circuit breaker failover)
- Blog posts syndicated as ActivityPub Article objects with auto-hashtags from tags
- Direct posting (Notes) with scheduling support
- Outbound Create, Update, Delete activity flows
- Publish scheduler (60s interval) auto-federates scheduled content and blog posts
- Follow/Unfollow handling with auto-accept
- Inbound likes, boosts, and comments with configurable moderation (queue all / auto-approve / followers only)
- Federation events pushed to notification system
- Engagement counters (likes/boosts) on blog detail page
- Social feed dashboard with Twitter/X-style timeline showing rich post content
- Quick Post composer with inline edit/delete and scheduling
- Profile editing (bio, avatar via media picker)
- Featured/pinned posts (ActivityPub featured collection, max 3)
- Mastodon-style post preview on blog detail page
- Followers, comments, activity log management pages
- Actor blocklist (site-wide) and instance blocklist (sysadmin-only) with CSV import
- Instance health dashboard with delivery stats per remote server
- Federation module toggle in Settings > Modules
- Custom RBAC (federation publish permission separate from content editing)
- Full i18n support (EN, DE, ES, FR, IT, NL, PL, PT)
- Encrypted keypairs at rest (AES-256-GCM)
- Complete documentation: admin guide, API endpoint reference (26 endpoints), OpenAPI spec
Changed
ENCRYPTION_KEYenv var now accepted alongside legacyAI_ENCRYPTION_KEY(backward-compatible)- Site context API now includes
federationmodule flag - Site settings DTO includes
module_federation_enabled
v1.1.0
Backend
- Similar blogs endpoint -- new
GET /sites/{site_id}/blogs/{id}/similar?limitendpoint that returns related posts ranked by taxonomy overlap (shared tags, categories, primary category match, and same author). - Similar pages --
find_similar_pages()model method for page similarity scoring. - Unique slug generation --
ContentService::generate_unique_slug()for creating conflict-free slugs when cloning content.
Admin Dashboard
- Blog creation wizard -- replaced the single-dialog blog form with a step-by-step wizard (slug, metadata, settings).
- Content template wizard -- new guided wizard for creating content templates.
- Dashboard widgets -- attention panel (items needing review), content status chart, and recent activity feed on the home page.
- Editorial workflow improvements -- approve and restore actions, review comment dialog enhancements.
- Command palette enhancements -- additional quick actions in Cmd+K.
- New shared components -- ApproveDialog, CopyableId, PageTypeChip, RestoreDialog.
- PWA support -- web app manifest and app icons for installable dashboard experience.
- Extended i18n -- new translation keys across all 8 supported locales (de, en, es, fr, it, nl, pl, pt).
Astro Blog Template
- Dark mode -- system preference detection, manual toggle in the nav bar, and localStorage persistence. All colors adapt via CSS custom properties.
- Similar blogs section -- "Continue Reading" section on blog detail pages showing up to 3 related posts from the similarity API.
- Footer redesign -- 3-column layout with brand, navigation links, and social icons.
- UI refinements -- sticky nav with backdrop blur, card hover animations, responsive excerpt clamping, improved typography and spacing.
- Category archive pages -- blog listing filtered by category slug at
/blog/category/{slug}.
v1.0.0 -- Initial Release
The first public release of Forja, a complete multi-site CMS built with Rust and React.
Backend
- Multi-site CMS -- manage multiple independent websites from a single installation.
- Internationalization (i18n) -- localized content fields and navigation titles with full locale management.
- Role-Based Access Control (RBAC) -- four permission levels (Master > Admin > Write > Read) with site-level membership roles.
- Dual authentication -- supports both API key (
X-API-Keyheader) and Clerk JWT (Authorization: Bearer) authentication. - Rate limiting -- Redis-backed request rate limiting to protect against abuse.
- OpenAPI documentation -- auto-generated Swagger UI at
/api-docsvia utoipa, covering all API endpoints. - Audit logging -- tracks who changed what and when, with queryable audit log endpoints.
- Content scheduling -- publish and unpublish blog posts and pages on a schedule.
- Webhooks -- event-driven webhook delivery system with retry logic and delivery logs.
- Notifications -- in-app notification system for admin users.
- RSS feeds -- auto-generated RSS 2.0 feeds for site blog content.
- URL redirects -- 301/302 redirect management per site with active/inactive toggle.
- Media management -- upload, serve, and organize media files with folder support and image processing.
- Image processing -- server-side image resizing and optimization.
- TLS support -- native HTTPS via Rocket's rustls integration (
TLS_CERT_PATH/TLS_KEY_PATH). - Health check --
/healthendpoint reporting PostgreSQL and Redis connection status. - SQLx migrations -- automatic database schema management on application startup.
- Content types -- blog posts, static pages, CV entries, legal documents, documents, and content templates.
- Navigation system -- hierarchical navigation menus with drag-and-drop ordering and localized titles.
- Taxonomy -- tags and categories with i18n support.
- Social links -- per-site social media link management.
- S3 storage -- optional S3-compatible storage (AWS S3, MinIO, Cloudflare R2, DigitalOcean Spaces).
Admin Dashboard
- Full Material UI interface -- responsive admin dashboard built with React, Vite, and MUI.
- Clerk authentication -- sign in with Clerk, with role-based UI visibility.
- Drag-and-drop navigation -- visual navigation tree editor with reordering.
- Markdown editor -- rich text editing for blog posts and page content.
- Media library -- upload, browse, and manage media files with folder organization.
- Webhook management -- create, test, and monitor webhook subscriptions with delivery logs.
- API key management -- create and manage API keys with different permission levels.
- Audit log viewer -- browse and filter the audit trail.
- Command palette -- keyboard shortcut (Cmd/Ctrl+K) for quick navigation.
- Internationalization -- admin UI language selection.
- Theme support -- light and dark mode.
- Setup checklist -- guided first-time setup wizard for new installations.
- Site management -- create and configure multiple sites.
- Content editors -- dedicated editors for blogs, pages, documents, CV entries, and legal pages.
- Taxonomy management -- create and assign tags and categories.
- Redirect management -- create and manage URL redirects per site.
- Notification center -- view and manage in-app notifications.
- Member management -- invite and manage site members with role assignment.
- Settings pages -- per-site settings configuration including locale and preview URLs.
Infrastructure
- Docker -- multi-stage Dockerfile producing a minimal production image.
- Docker Compose --
docker-compose.dev.yamlfor local development with PostgreSQL, Redis, and pgAdmin. - GitHub Actions CI -- automated pipeline with formatting, linting, unit tests, and integration tests for both backend and admin.
- Railway deployment guide -- step-by-step deployment instructions for Railway.
- Developer scripts -- helper scripts for starting, stopping, testing, building, seeding, and cleaning the development environment.
Templates
- Astro blog template -- server-rendered blog and portfolio site built with Astro 5, including pages for blog posts, CV, legal documents, and RSS feeds.