Umbral
Changelog

What's shipped, what's next

Umbral is greenfield and moving fast. Here's where it stands today and the milestones on the way to v0.1 — managed from the admin, like everything else on this site.

Version Status Date Highlights
toward v0.1
Roadmap
On the road to v0.1
  • Email sending (SMTP / API backends) and a hardened background task queue.
  • WebSockets / SSE for realtime push — user- and room-targeted.
  • REST nested writable serializers; CSV / Excel import-export.
  • A testing & factory library for models and pages.
  • Caching, rate limiting, structured logging, and metrics.
v0.0.1
Current
Released Jun 10, 2026
First public release

0.0.1 - First public release

2026-06-25

The first public release of umbral - a batteries-included web framework in Rust. Declare your data and get migrations, an admin, CRUD, and an optional REST API almost for free, with Rust's compile-time guarantees. The whole framework is thin core, plugin-heavy: auth, sessions, admin, tasks, and REST are all plugins, structurally identical to a third-party one. All 27 crates published to crates.io.

The core

  • umbral-core - the ORM (QuerySet builder, typed column constants, relations: FK / O2O / M2M / reverse-FK, select_related/prefetch, aggregates, transactions), the managed-migration engine (model-state snapshots, autodetection, tracking table, migrate), routing/middleware over axum, SQLite + Postgres backends with a boot-time field/backend system check, and the Plugin trait - the dynamic seam every plugin implements.
  • umbral-macros - #[derive(Model)], #[derive(Choices)], #[task], and the other proc-macros that turn a plain struct into a full ORM model.
  • umbral - the facade crate: the single stable surface (umbral::prelude::*) that user code and plugin authors import.
  • umbral-cli - the umbral command-line tool: serve, migrate, makemigrations, inspectdb, checkmigrations, dumpdata/loaddata, startproject/startapp, and more.
  • umbral-casing - shared snake/pascal-case helpers.

The declare → migrate → change → migrate loop

The everyday loop works from day one: declare or change a model → an autodetected migration is generated → migrate applies it. Autodetection ships create/alter/drop table and add/remove/alter column; inspectdb introspects an existing database into models so an existing schema drops straight into the same managed-migration loop.

Built-in plugins

  • umbral-auth - users, permissions, password hashing (argon2), login/logout.
  • umbral-sessions - session store + middleware (tower-sessions).
  • umbral-admin - auto-generated CRUD admin UI.
  • umbral-tasks - a DB-backed background task queue with a worker.
  • umbral-rest - optional REST layer: serializers, viewsets, routers, filters.
  • umbral-openapi - optional Swagger UI / OpenAPI schema generation.
  • umbral-security - CSRF + security-hardening headers.
  • umbral-permissions - permission checks and gated routers.
  • umbral-rls - Postgres row-level security policies.
  • umbral-storage - unified static + media storage (local filesystem, pluggable backends).
  • umbral-cache - a cache abstraction with page caching.
  • umbral-signals - model lifecycle signals (post_save, post_delete, m2m_changed).
  • umbral-realtime - SSE/WebSocket realtime, presence, live model subscriptions.
  • umbral-email - SMTP + API-based email sending.
  • umbral-oauth - OAuth login/connect flows.
  • umbral-tenants - multi-tenancy (schema-per-tenant and database-per-tenant).
  • umbral-health - liveness/readiness endpoints.
  • umbral-logs - request logging.
  • umbral-analytics - pageview analytics.
  • umbral-livereload - dev-mode live reload.
  • umbral-playground - an interactive API playground.
  • umbral-testing - test helpers.

Secure by default

CSRF protection, clickjacking/HSTS-capable headers, template autoescaping, and always-parameterized SQL are baked in. Postgres-first, SQLite for tests.

v0.0.2
Released Jun 26, 2026
Packaging & polish

0.0.2 - Packaging & polish

2026-06-26

A packaging & polish release. umbral becomes properly consumable from crates.io — new projects build without your local checkout, every crate is documented, and the documentation site goes live.

Added

  • umbral startproject now scaffolds a project that depends on published crates.io versions instead of git paths, so a freshly generated project builds anywhere.

Changed

  • The documentation site is live on GitHub Pages.
  • Every crate now ships a README on crates.io, linked back to the docs site — including new pages for the health and oauth plugins.
  • Removed the prior-art (Django) framing throughout the codebase and crate metadata; umbral reads as its own framework.
v0.0.3
Released Jun 29, 2026
The auth release

0.0.3 - The auth release

2026-06-29

The auth release. A complete authentication surface lands - email verification, password reset, and both form-based and JSON endpoints - plus read-only REST scoping and a log-out-everywhere primitive.

Added

Authentication

  • Email verification. A verification code is issued on registration and confirmed through a dedicated endpoint. Opt into require_verified_email to auto-send on register and block login until a user verifies. Adds the email_verified_at field and an AuthChallenge model.
  • Password forgot / reset. Request a reset token, then set a new password - existing sessions are revoked on reset.
  • Two auth surfaces. Alongside the JSON API, with_form_routes mounts HTML form-action endpoints (form in → 303 redirect out) for server-rendered apps. A shared umbral_auth::logout backs both.
  • JSON endpoints for verify / resend / forgot / reset under the REST base path, each with a matching OpenAPI path item.
  • Pluggable mailer. An AuthMailer seam (with a ConsoleMailer default) receives the email kind and its params, so you can customize each email type. Plus reusable challenge-generation, hashing, and lifecycle helpers.

REST & sessions

  • Read-only resources everywhere. views([List, Retrieve]) is now enforced end to end: writes return 405 (with an Allow header), OPTIONS advertises only the served verbs, and the OpenAPI spec omits the write operations.
  • revoke_user_sessions - log a user out of every device at once.
  • umbral::web::api_base - the REST base path is published at build time so any plugin can discover it.

Fixed & Hardened

  • auth - email actions are throttled; email verification and the AuthChallenge attempt counter are now atomic (server-side increment), closing brute-force and timing-variant gaps. Reset-revocation failures are logged instead of swallowed.
  • rest - custom @action endpoints appear in the OpenAPI spec even when they declare no schema.
  • playground - per-request headers and query params now persist and are sent; a loaded draft no longer clobbers params you're mid-way through typing, and an in-progress header/param row is committed when it loses focus.

Internal / Docs

  • Corrected auth status codes and revocation/timing documentation; expanded JSON password-reset and verified-resend test coverage; documented the reset_url_base host-trust requirement.
v0.0.4
Released Jun 30, 2026
Hotfix: Postgres migration ordering

0.0.4 - Hotfix: Postgres migration ordering

2026-06-30

A targeted hotfix release for Postgres migration ordering.

Fixed & Hardened

  • migrate - migrate the implicit app plugin last, so a user model's foreign key to a plugin-owned table (e.g. auth_user) is created after its target table exists. On Postgres, where foreign keys are enforced at CREATE TABLE time, the previous ordering could fail a fresh migrate; SQLite was unaffected.
v0.0.5
Released Jul 5, 2026
Security hardening & custom admin views

0.0.5 - Security hardening & custom admin views

2026-07-05

The security-hardening release - the result of a deep production-readiness audit. umbral becomes secure by default across the board, and picks up three big usability wins along the way: custom admin views, object-level row scoping on REST, and recursive N-level nested writes.

Highlights

  • Custom admin pages. Register your own admin views with the AdminView builder; they mount under /custom-views/, appear in the sidebar nav, and are permission-gated on both render and their data endpoints.
  • Object-level row scoping. Built-in REST CRUD can now scope reads and writes to the rows a caller owns, so users only ever see and touch their own records.
  • Recursive nested writes. REST accepts writable nested children to arbitrary depth in one transaction (previously one level, with deeper trees silently dropped).
  • Secure by default. Mass assignment is default-deny, row-level security is actually enforced, hardening headers and request limits ship out of the box, and a long list of XSS / enumeration / error-leak holes are closed (below).

Secure by default

  • Default-deny mass assignment. Mark sensitive fields #[umbral(privileged)]; the JSON write paths refuse to set them unless the caller explicitly authorizes it - no more POST {is_staff: true} escalation.
  • Row-level security, enforced. RLS now emits FORCE ROW LEVEL SECURITY (so the app's own role is subject to its policies) and sets the per-request app.user_id GUC through a connection-pool hook that resets on release - no cross-request leakage. On SQLite (which can't enforce RLS) it fails closed instead of silently allowing everything.
  • Hardening headers by default. Every response ships X-Content-Type-Options, X-Frame-Options, and a referrer policy out of the box (opt out if you must); SecurityPlugin adds CSP/HSTS.
  • Request limits by default. A default request body-size limit and request timeout are installed, and multipart uploads are capped mid-stream.
  • Production defaults. Release builds default environment to Prod; a too-short or empty secret_key hard-fails Prod boot rather than running with a weak/absent key. Boot also warns on SQLite-in-Prod and wildcard allowed_hosts.
  • Trusted-proxy client IP. Client-IP resolution honours a configured trusted-proxy chain instead of blindly trusting forwarding headers.
  • Ungated-route audit. Boot audits your routes and reports mutating endpoints that have no permission/login gate; new permission-recording gated builders make "gated by construction" easier.
  • Sensitive data stays out of the wrong places. #[umbral(signal_skip)] strips fields (e.g. password hashes) from signal payloads; Masked<T> is sealed on the dynamic write path; the OpenAPI spec + Swagger UI are not mounted in Prod by default.
  • Destructive migrations are opt-in. migrate refuses to apply a migration that drops a table or column unless you pass --allow-destructive, guarding against a missing model registration quietly dropping a production table.

Auth, sessions & permissions

  • Auth - argon2 hashing concurrency is bounded so a login/signup flood can't OOM the server; the login-timing enumeration oracle, error leaks, and a mailer secret-print are all closed.
  • Sessions - configurable cookie SameSite policy; an absolute session-age cap alongside sliding expiry; revocation works on every store (SessionStore::destroy_user); out-of-request set_data routes through the installed store; and an empty CookieStore secret hard-fails Prod boot.
  • Permissions - the permission layer is PK-agnostic (works with UUID/String user keys, not just i64), bounds its permission/group fetch, and denies deactivated accounts instead of trusting a live session.
  • OAuth & multi-tenancy - an unknown OAuth provider key returns 404 (not 500); tenant routing fails closed (an un-onboarded tenant never falls through to the default database); a verified-email allowlist prevents account-takeover via auto-linking; and a resolved tenant is bound to the caller via TenantMembership.
  • Routing - auth JSON routes resolve at both the bare and trailing-slash form, so /api/auth/login/ no longer 404s under an append-slash policy.

Admin

  • Custom views - a first-class AdminView builder registers, routes, and renders your own pages under /custom-views/, listed in the sidebar; view paths are validated at build so a bad path can't panic the router.
  • Per-widget permissions - dashboard widgets are permission-gated on both render and their data endpoints, the "add widget" catalog is filtered to what you can access, and the checks are batched. New has_codename / require_codename helpers back this.
  • Delete actually deletes - CSRF tokens are enforced on bulk and JS-driven deletes, and the single-delete URL bug is fixed.
  • XSS closed - inline event-handler output is JS-escaped (escapejs), and every model list/detail view is gated by a per-model View permission.
  • Visual refresh - a neutral pure-white / near-black palette, a unified card recipe with elevation, a responsive changelist and create/edit sheet (no button overflow on mobile), pagination that survives HTMX swaps, and a single Tailwind theme source.

ORM, migrations & data integrity

  • Concurrency - a FOR UPDATE SKIP LOCKED claim primitive (used by the task queue) lets multiple workers pull disjoint rows safely; alias-aware begin_for / transaction_on open a transaction on a specific database.
  • Safer migrations - the autodetector now picks up unique_together / composite-index changes; tightening a column to NOT NULL backfills existing NULLs; the SQLite table-recreation dance preserves indexes and unique_together and works on a table with inbound foreign keys; combined alter-plus-add/drop applies cleanly; an ambiguous column-shape rename fails closed instead of guessing; FK targets resolve to the right PK and raw DDL identifiers are escaped.
  • Concurrent migrators - a Postgres advisory lock serializes two replicas deploying at once, so they can't race the same DDL.
  • Correct writes & signals - dynamic-pool writes are atomic and return the real affected-row count; update_or_create fires a per-row post_save on both the create and update branches (so realtime/signal consumers see upserts).
  • Backup/restore - restore is transactional and FK-ordered, and resets Postgres sequences afterward.
  • Connection handling - settings.databases pools open lazily at build; UMBRAL_DB_* overrides are honoured on the default pool, with a warning for dead entries.

Realtime, storage, cache & observability

  • Realtime - an inbound-message can_send authorization hook; a default connection cap and per-connection message-rate cap; the Redis URL is redacted before logging and credentials are masked in Debug output.
  • Storage - an access-control hook for serving media; bounded media-processing concurrency; and an active-content guard (S3), upload cap, and symlink guard on the media path.
  • Cache - the shared page cache is bypassed for responses that carry Authorization or Cache-Control: private/no-cache, so one user's response is never served to another.
  • Analytics - sensitive path prefixes are excluded from pageview capture, and outbound sends are bounded.
  • Logs - user attribution is unforgeable (read from the request extension, never a client header) and the request-capture handle list is bounded to avoid unbounded growth.
  • Health - /ready no longer leaks a raw database error into its unauthenticated body.
  • OpenAPI - the Swagger UI asset source is version-pinned and configurable (self-hostable), and neither the spec nor the UI mount in Prod by default.

Web, CLI & operations

  • Graceful shutdown - the server drains its connection pool and shuts down cleanly on SIGTERM/SIGINT.
  • Robust request handling - the request authority falls back to the HTTP/2 :authority when the Host header is absent; the rate-limiter's key map is bounded by a periodic sweep; core error pages and forms no longer reflect XSS or leak internals; secrets are redacted in Debug; and misspelled UMBRAL_ env keys are flagged at startup.
  • CLI - umbral <cmd> transparently forwards to cargo run -- <cmd>; generated project defaults are hardened; maskkeygen warns before printing a private key to stdout; and a Form's foreign-key field parses into the target's PK type rather than assuming i64.

Internal / Docs

  • Dropped the end-of-life rustls/hyper stack by upgrading to rust-s3 0.37, and patched the remaining dependency-audit findings.
  • Added a composite claim index on the task table; corrected signals rustdoc; documented the filter_sql injection contract and Masked<T> at-rest behaviour.
v0.0.6
Released Jul 8, 2026
Data ergonomics & authorization

0.0.6 - Data ergonomics & authorization

2026-07-08

The data-ergonomics & authorization release. umbral learns to handle text the way developers expect - case-insensitive accounts, login by username or email, and declarative trim / lowercase / case_insensitive columns - and finishes the production-readiness audit with a real object (row-level) permission primitive and gated-by-construction routes. Ships with an llms.txt so any LLM can learn the framework in one file.

Highlights

  • Object-level (row) permissions. #[umbral(...)] model perms were all-or-nothing - a model-level grant let a holder touch any row (IDOR by design). New has_object_perm(user, perm, object_pk) scopes authorization to the row a request actually targets, with grant_object_permission / revoke_object_permission and objects_with_perm for list filtering.
  • Case-insensitive data, three ways. #[umbral(trim)] / #[umbral(lowercase)] canonicalize a string column on write; #[umbral(case_insensitive)] makes the column case-insensitive at the database level (Postgres citext, SQLite COLLATE NOCASE) while preserving the original casing - the Django CIText experience.
  • Log in with username or email. authenticate matches every column in UserModel::login_columns() (AuthUser: username + email), case-insensitively. Accounts are stored + matched case-insensitively, so Dalmasonto and dalmasonto are one account and a case-only duplicate signup is rejected.
  • Gated by construction. App::builder().deny_ungated_mutations() turns the boot-time ungated-route audit into a hard build error, so a mutating route with no recorded permission fails the build instead of shipping open.
  • One file for LLMs. A new llms.txt (served at /llms.txt) teaches umbral end to end and links every doc page - point your AI assistant at it.

ORM & data

  • Normalized fields. #[umbral(trim)] strips surrounding whitespace and #[umbral(lowercase)] lowercases a string column on the dynamic write path (REST create/update, admin form-submit) - combine them for canonical usernames/emails. Pair with #[umbral(unique)] to get case-insensitive uniqueness for free. String-only (a compile error on any other field type).
  • Case-insensitive columns. #[umbral(case_insensitive)] renders citext on Postgres (the migration auto-creates the extension) and COLLATE NOCASE on SQLite; =, UNIQUE, and ORDER BY fold case while storage keeps the original casing. A boot check warns that SQLite's NOCASE folds ASCII only.

Auth & OAuth

  • Case-insensitive accounts. Usernames and emails are stored and matched trimmed + lowercased across every path - signup, login, email verification, password reset, resend, and social login - closing the "register twice, differing only by case" hole. New normalize_username / normalize_email helpers.
  • Login by username or email, via the new UserModel::login_columns() (defaults to username-only for custom user models, so nothing breaks).
  • Social accounts get a real password hash. random_password_hash() gives OAuth-created accounts a valid argon2 hash of a random password instead of an empty/sentinel value, so password login fails cleanly and the account can still adopt a known password via reset.
  • Atomic social signup. Creating the user and its social account now happens in one transaction - a failed social-account insert rolls the user back, so a half-created account can never orphan and occupy a verified email.

Authorization & permissions

  • Object (row-level) permissions. A new ObjectPermission grant table + has_object_perm / has_object_perm_for_superuser / objects_with_perm, and grant_object_permission / revoke_object_permission / revoke_object_permissions_for. The check does not fall back to the model-level grant - a handler scopes authorization to the row it's acting on.
  • Deny ungated mutations. AppBuilder::deny_ungated_mutations() promotes the audit_2 H19 boot warning to a BuildError - opt into "every mutating route must be gated" and a forgotten permission fails the build.
  • RLS reconciles on reapply. Row-level-security policies are no longer append-only across boots: a policy removed from the builder is now dropped, so a stale policy can't keep granting.

Realtime

  • Authorized publish. MessageContext::publish(group, event, data) authorizes the sender via the group policy before broadcasting (safe-by-default over raw to_group().send()); MessageContext::can_send exposes the check.
  • Presence scales. The presence:sync roster is now sent only to the joining connection instead of re-broadcast to the whole group on every join - a join storm in a large room is O(N), not O(N²). Existing members track membership from the join/leave deltas they already receive.

Admin

  • CSRF-config warning. The admin warns at boot if the session cookie is configured SameSite=None (which weakens the default CSRF protection), pointing you at mounting a CSRF middleware.
  • Safer delete button. The delete-confirmation dialog carries its URL in a data- attribute read by JS rather than interpolated into an inline event-handler string, closing a JS-escaping edge and keeping the delete flow robust.

Docs & internal

  • llms.txt. A single self-contained, LLM-facing guide to umbral - mental model, model declaration with the full attribute vocabulary, the ORM API, plugins, and CLI - plus links to every hosted doc page. Served from the docs site and mirrored in the repo root.
  • Built-in AuthUser marks its privilege columns (is_staff, is_superuser) #[umbral(privileged)] and password_hash #[umbral(noform)], regression-guarded against mass-assignment.
  • #[derive(Choices)] fields decode from legacy VARCHAR columns on Postgres (no migration needed); a fresh-session set_data now emits its session cookie even when an unrelated (CSRF) cookie is already present.
  • Bumped notify 6 → 8 in the live-reload plugin, dropping stale transitive dependencies.
v0.0.7
Released Jul 13, 2026
Typed client & data modeling

0.0.7 - Typed client & data modeling

2026-07-13

The typed-client & data-modeling release. umbral learns to generate a fully-typed TypeScript query client from your API (umbral gen-client), grows a deep bench of ORM data-modeling primitives - database views, request validation, custom DTOs, parent-scoped sub-resources, a model audit trail, auto_user stamping, cascading soft-delete - and ships a redesigned startproject scaffold with a real compiled-Tailwind design. Plus zero-downtime rollouts: drain-on-shutdown and readiness gated on pending migrations.

Highlights

  • A generated TypeScript client. umbral gen-client reads your REST spec and emits one JS runtime + .d.ts: typed create/update DTOs, per-model id types, typed realtime subscriptions, and a session client (login / logout / me) discovered from the spec. Pagination is configurable and auth is scheme-driven, so the client matches your API instead of a lowest-common-denominator fetch wrapper.
  • Database views, regular and materialized. Declare a view as a model (features #73) and query it through the ORM like any table - the migration engine creates and refreshes it.
  • Request validation, two ways. Valid<T> + #[derive(Validate)] validate a request body at the extractor boundary, and #[derive(Dto)] declares custom response types that flow into the generated client (gaps3 #29.4/#29.5).
  • Parent-scoped sub-resources. ResourceConfig::under(...) mounts a resource beneath its parent (/projects/{id}/tasks) with the scope enforced automatically (gaps3 #29.2).
  • A model audit trail. #[umbral(audited)] records who changed a row and when; auto_user_add / auto_user stamp the creating/updating user without a handler writing it (gaps3 #54/#55).
  • A real scaffold design. umbral startproject now emits compiled Tailwind, the umbral palette, working mobile navigation, and doc links - a project that talks about itself, not about the generator.

ORM & data

  • Views as first-class models, regular and materialized (features #73).
  • register_cleaner - custom per-field clean/validate hooks on the write path (features #83).
  • #[umbral(audited)] model-level audit trail, designed before it was built (gaps3 #54).
  • auto_user_add / auto_user stamp the acting user onto a row (gaps3 #55).
  • Soft delete now cascades to related soft-delete rows instead of orphaning them (gaps3 #53).
  • AppBuilder::auto_models() - models register themselves via inventory, so a new model needs no manual wiring (gaps3 #46).
  • Order by an annotation, and get annotation rows back typed (gaps3 #29).
  • create() / delete() emit per-row signals (they were silent before), and #[umbral(trim, lowercase)] now applies on every write path (gaps3 #29).
  • filter_eq_string fails closed - a malformed id no longer degenerates into an unfiltered query that could delete the table (gaps3 #56).
  • Reject DST-ambiguous local times instead of silently shifting them.
  • Generate TypeScript types from the model registry (umbral typegen), the foundation the generated client builds on.

REST & the generated client

  • umbral gen-client emits a typed TS query client: create/update DTOs, per-model id typing, hidden-field exclusion from row types, typed realtime subscriptions, and a session client.
  • Membership scoping - scope_async + ScopeDecision::RestrictIn restrict a queryset to rows the caller belongs to.
  • Configurable pagination + scheme-driven auth in the generated client, with getting-started, paging, and error-handling docs.
  • Fixes: the generated client no longer 404s on get/update/delete; ids are typed per model, not as one global union.
  • Default JSON responses to Cache-Control: no-store so a shared cache can't serve one user's payload to another (gaps3 #36).

Auth, security & ops

  • Publish the session user id to the DB connection so Postgres RLS policies can read current_user (gaps3 RLS wiring).
  • RequireAuth extractor + an auth idioms page (gaps3 #37).
  • Mark personalised responses no-store, private at the security layer.
  • Zero-downtime rollouts - drain readiness on shutdown, and gate /readyz on pending migrations so traffic doesn't hit a half-migrated instance.
  • Fire on_ready when the app is up, not when it is built - hooks that seed or backfill now run against a live server (gaps3).
  • Order plugins by the foreign keys their models declare, so cross-plugin migrations apply in dependency order.

Storage & tasks

  • Built-in thumbnails behind the images feature - resized variants at derived keys, generated in an on_upload processor (gaps3 #50).
  • Upload content-type allow-list that sniffs the bytes, not the header (gaps3 #51).
  • Type-safe enqueue - #[task] generates a typed handle so a job's arguments are checked at the call site (gaps3 #48); an admin model for schedules (gaps3 #49).

Admin, scaffold & website

  • The admin version label is real and yours - no longer a hardcoded string (gaps3 #67).
  • Scaffold redesign - compiled Tailwind, the umbral palette, docs links, and a landing page that shows how umbral works (gaps3 #47/#64).
  • startproject emitted a project that would not compile - fixed the bad settings prefix and the static() HTML-escaping bug that turned / into &#x2f; (gaps3 #64/#66).
  • The website stops handing raw database errors to visitors and closes a scaffold-generated information leak (gaps3 #57/#58/#62).

Fixes

  • A UUID-keyed user was silently forbidden from everything by the permission check (gaps3 #59).
  • A bad env prefix made migrate succeed against nothing (gaps3 #59/#60/#61).
v0.0.8
Released Jul 13, 2026
The GraphQL release

0.0.8 - The GraphQL release

2026-07-13

The GraphQL release. A complete GraphQL API - queries, mutations, subscriptions, and cursor pagination - derived automatically from the model registry, with the same identity and field-visibility rules the REST layer already enforces. Ships alongside a new ORM field-visibility tier (private / secret) so a field can be readable in one surface and hidden in another, honestly, in one schema.

Highlights

  • A real GraphQL API from the model registry. umbral-graphql derives a typed schema from your models - no resolver boilerplate - and serves GraphiQL (features #9).
  • Mutations, with mandatory CSRF. GraphQL mutations ship with a CSRF defence they require, so a state-changing GraphQL endpoint can't be opened by accident.
  • Subscriptions over WebSocket and SSE. Live queries push over either transport; GraphiQL points at the subscription socket out of the box.
  • Cursor pagination (Relay connections) for stable paging over large result sets.
  • Field-visibility tiers enforced in the ORM. #[umbral(private)] and #[umbral(secret)] mark a field's read policy once, in the model, and every surface (REST, GraphQL) honours it - with allow_private_if(...) to unlock a private field for an authorized caller in one honest schema.

GraphQL

  • Schema derived from the model registry - queries and types generated from the same ModelMeta the ORM and REST already use (features #9).
  • The caller's identity is plumbed into the schema context, so per-row authorization and owned_by-style scoping work the same as REST.
  • hide() and a hard denylist moved into core, so a field can never be exposed through GraphQL by omission.
  • A default query depth / complexity budget guards against pathological nested queries (landed in the 0.0.10 hardening, tracked from here).

ORM

  • private / secret field tiers, enforced in the ORM - the read policy lives on the model, not in each surface's config.
  • private is a read policy - allow_private_if unlocks reads for an authorized caller without also gating writes (the write-blocking edge was corrected in 0.0.9).

Fixes & internal

  • Four dead documentation links, one of them in every scaffolded project.
  • clippy --fix across the plugins and the core + macros crates.
  • The website moved onto umbral 0.0.7.
v0.0.9
Released Jul 14, 2026
Testing ergonomics & admin dashboards

0.0.9 - Testing ergonomics & admin dashboards

2026-07-14

The testing-ergonomics & admin-dashboards release. Tests stop hand-writing CREATE TABLE and derive their schema straight from the models - across 205 suites - so a schema drift can't hide behind a stale fixture. The admin grows real dashboards: draggable saved layouts, per-widget filters, and one-click CSV export. Plus UUID-primary-key fixes that close the last gaps in non-i64 keys.

Highlights

  • Test schema derived from the models. umbral-testing builds the test database from the model registry and boots in one line; 205 test suites were converted, and the conversion surfaced (and fixed) real schema bugs the old hand-written DDL was papering over (gaps3 #78).
  • Draggable admin dashboards. A saved dashboard layout is now read back and rearrangeable - the admin remembers how you arranged your widgets.
  • CSV export for any widget with rows behind it - one click, no per-widget code.
  • Declarative widget filters for every widget kind, so a dashboard panel can scope its own data.
  • UUID primary keys, finished. An M2M relation with a Uuid-PK child can be written (gaps3 #79), and a SQLite uuid column is declared as what it actually stores - BLOB (gaps3 #80).

Testing

  • Derive the test schema from the model registry and boot the test app in one line (umbral-testing).
  • Converted 205 suites to the derived schema (gaps3 #78), plus 25 more in admin/core that the conversion exposed.

Admin

  • Saved dashboard layout is read and draggable.
  • CSV export for any row-backed widget.
  • Declarative widget filters for every widget kind, documented alongside CSV export and reordering.

ORM

  • M2M with a Uuid-PK child can now be written (gaps3 #79).
  • SQLite uuid columns declared as BLOB - matching their real storage (gaps3 #80).
  • private is a read policy, not a write block - it no longer refuses writes in REST and GraphQL.

Docs & website

  • Every plugin page says how to install it, with a verified cargo add line for all 22 plugins - install snippets that can't go stale.
  • Seed umbral-graphql into the site's plugin directory; make the site's plugins workspace members so they can be tested.
v0.0.10
Released Jul 15, 2026
Security-hardening sweep

0.0.10 - Security-hardening sweep

2026-07-15

The security-hardening sweep. A framework-wide review (review_3) closed the confirmed critical and high findings, and the gaps4 batch tightened the surfaces that a multi-tenant, real-traffic app leans on: row-level GraphQL mutation scope, per-request context that survives across SSE and WebSocket, reverse-FK child lists windowed per parent, and an analytics path scrubber that stops leaking identifiers into pageview logs. Plus umbral startcommand for app-owned management commands.

Highlights

  • Security sweep (review_3). Closed the confirmed critical/high findings from a framework-wide review, with fail-closed IN filters, a capped page cache, a bounded realtime broker, and coalesced writes across auth/cache/realtime.
  • Row-level GraphQL mutations. owned_by scopes a mutation to the rows the caller owns, so a GraphQL write can't reach across tenants (gaps4 #9).
  • Per-request context across transports. The request context now carries across SSE and WebSocket, so a subscription sees the same identity and scope a query did (gaps4 #12).
  • Reverse-FK lists windowed per parent. A parent's child list paginates per-parent instead of over the global table - no more one tenant's page bleeding into another's (gaps4 #13).
  • Analytics path scrubber. Auto-pageview paths have identifying segments (ids, slugs, tokens) scrubbed before they're logged (gaps4 #22).
  • umbral startcommand. Scaffold an app-owned management command; commands are collected once through the codegen seam.

Security & correctness

  • Fail-closed IN filters; declare OAuth's sessions dependency (gaps4).
  • A default GraphQL query depth/complexity budget.
  • Reset only umbral's own GUCs on a Postgres checkout, not RESET ALL (gaps4 #16).
  • Audit-log the shared/tenant split at boot (gaps4 #11).
  • Scoped Redis clear; a media-mount warning; guard the low-level _pg terminals against silent hydration skips.

ORM & REST

  • raw_with bound-parameter escape hatch for a query the builder can't express (gaps4 #25).
  • try_for_each honours a caller .limit() as a total cap (gaps4 #27).
  • insert_form returns the new PK in its true shape - i64 / Uuid / String - not a coerced i64 (gaps4 #26).
  • Drift-free route metadata via Plugin::routes_builder() - the route list can't diverge from what's actually mounted (gaps4 #31).
  • REST scaffold classes - permission / auth / pagination / throttle generators.

Admin & storage

  • Write M2M selections inside the parent save transaction, so a failed child write rolls the parent back (gaps4 #14).
  • Don't mount a local ServeDir for a custom storage backend - a non-FS backend serves its own URLs (gaps4 #18).

Internal

  • Finish the codegen migration; compile what the generators emit, in CI.
  • Close 10 codegen defects found by the pre-0.0.10 review.
v0.0.11
Released Aug 3, 2026
Plugin ergonomics & private media

0.0.11 - Plugin ergonomics & private media

2026-08-02

The plugin-ergonomics & private-media release. Wiring an app gets quieter - models and task handlers discover themselves, one App::builder().authentication(...) sets an ambient auth backend every surface inherits, and AuthPlugin::new() drops the turbofish. Uploaded media grows real access control: owner-only gating, HMAC-signed time-bounded share links, and a gated proxy so private files work on S3 and custom backends too - not just the local filesystem. Closes the entire gaps4 DX tracker (#32–#58).

Highlights

  • Autodiscovery. discovered_models!() and #[umbral::task] register models and task handlers via inventory - declare them anywhere and they wire themselves, no manual list to keep in sync (gaps4 #40).
  • App-wide authentication. App::builder().authentication(backend) sets one ambient auth backend that REST, GraphQL, realtime, and the media gate all inherit through default_authentication() (gaps4 #42).
  • Private media, done properly. Three composable gates - .media_access_owner() (serve a file only to its recorded owner), .media_signed_urls() (HMAC-signed, time-bounded links that need no session), and .media_access_identity(...) for custom rules - and a gated proxy that streams private files through S3 / custom backends so gating isn't filesystem-only (gaps4 #56/#57/#58).
  • Less ceremony. AuthPlugin::new() (no turbofish), SecurityPlugin::csrf_exempt(...) as a chainable shorthand (gaps4 #45/#41), and serve installs a default tracing subscriber + Dev autodetect so a fresh app logs and seeds sensibly out of the box (gaps4 #47/#48).
  • Async realtime room policy. GroupPolicy is async, so a room can be gated on live database state (membership, ownership) before a client joins (gaps4 #36).

Plugin & app ergonomics

  • Autodiscovery for plugin models and #[task] handlers (gaps4 #40).
  • Ambient authentication backend inherited by every surface (gaps4 #42).
  • AuthPlugin::new() without the turbofish; OAuthPlugin::from_settings + provider_opt construction (gaps4 #45/#46).
  • SecurityPlugin::csrf_exempt chainable shorthand (gaps4 #41).
  • serve installs a default tracing subscriber and autodetects Dev mode; seed_on_serve adopts the seed seams (gaps4 #47/#48).
  • Plugin sort ties honour registration order, so a deterministic mount order is preserved (gaps4 #44).
  • umbral startplugin absorbs the old startapp (the plugin contract is the only contract), and the generated plugin now compiles; all built-in plugin names are reserved so a scaffold can't collide.

Media & storage

  • Owner-only gating - a media_file.owner column + set_media_owner, and .media_access_owner() serves each file only to its owner (gaps4 #57).
  • Signed URLs - signed_media_url(mount, key, ttl) mints an HMAC-signed, time-bounded link; the signature binds the key and the expiry to secret_key (gaps4 #56).
  • Gated proxy for non-FS backends - with a gate configured, private files stream through S3 / custom backends instead of the framework mounting nothing there (gaps4 #58).

Auth, realtime & playground

  • Logout revokes the presented bearer token (gaps4 #32); 202 endpoints return a {"detail": ...} body (gaps4 #33); email format is validated at every entry point (gaps4 #35).
  • Async GroupPolicy for membership-gated realtime rooms; fan-out shares one Arc'd payload and a single render (gaps4 #36/#38).
  • Playground schema tab shows real per-field metadata (gaps4 #34); the default mount moved off /api to /playground (gaps4 #43).

ORM & correctness

  • choices columns filter by the enum variant (gaps4 #39).
  • varchar max_length changes emit Postgres alters (gaps4 #48 migration path).
  • Slash-redirect probes 404s from matched routes too, so a missing trailing slash redirects even under a matched prefix (gaps4 #50).
  • REST creates and bulk writes honour the object scope (gaps4 #37); update_json_in_tx reports the real matched-row count.

Security

  • ammonia 4.1.3 → 4.1.4 closes RUSTSEC-2026-0213 - an XSS hole in the HTML sanitiser's SVG handling, on the markdown-filter path.

Examples & docs

  • A new tracker example - the "One Model, Every Surface" tutorial app - dogfoods autodiscovery, health, and tasks.
  • Every doc page caught up to the gaps4 #41–#49 idioms; the media docs rewritten around the four gates and the proxy.

Track the work on GitHub, or follow releases via the newsletter.