|
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 / (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.
|