Umbral
Back to the blog
Release Aug 3, 2026 7 min read

From GraphQL to Private Media - What Shipped in Umbral Since v0.0.6 - v0.0.11

Five releases in three weeks. A full GraphQL API derived from your models, a generated TypeScript client, admin dashboards, a security-hardening sweep, and media that's finally private by default. A tour of what landed after July 9.

When I introduced Umbral in mid-July, the framework was at v0.0.6: declare a model once and get migrations, a typed ORM, an admin, REST, and OpenAPI from that single declaration. That was the promise, and it held.

What I didn't say out loud was how fast the next stretch would move. Between July 9 and the start of August, five releases shipped - v0.0.7 through v0.0.11 - and each one added a whole surface or closed a class of problem. This is the roundup: what's new, why it exists, and what it means if you're building on Umbral (or thinking about it).

The through-line for all of it is the same idea that started the project: your model is the single source of truth, and every surface is derived from it. GraphQL is the clearest example, so let's start there.

GraphQL, derived from the model registry (v0.0.8)

Umbral now speaks GraphQL. Not a hand-written schema you maintain alongside your models - a complete API generated from the same ModelMeta the ORM and REST already use. Queries, mutations, subscriptions, and cursor pagination (Relay connections), with no resolver boilerplate.

The point isn't "Umbral has GraphQL now" as a checkbox. It's that the GraphQL layer is a plugin that reads the same model registry as everything else, so it can't drift from your schema. Add a field to a model and it appears in GraphQL, in REST, in the admin, and in the migration - from one edit.

Three things I'm particularly happy with:

  • Mutations ship with mandatory CSRF. A state-changing GraphQL endpoint can't be opened by accident - the defence is built in, not opt-in.
  • The caller's identity is plumbed into the schema context, so row-level authorization and owned_by-style scoping work exactly the same way they do in REST. One authorization model, every surface.
  • Subscriptions run over WebSocket and SSE, and GraphiQL points at the subscription socket out of the box.

Alongside GraphQL came a new ORM concept it needed: field-visibility tiers. #[umbral(private)] and #[umbral(secret)] mark a field's read policy once, on the model, and every surface honours it - with allow_private_if(...) to unlock a private field for an authorized caller. A field can be readable in one context and hidden in another, honestly, in one schema.

A typed TypeScript client, generated (v0.0.7)

If GraphQL is about flexible reads, v0.0.7's headline is about typed writes from the frontend. umbral gen-client reads your REST spec and emits a fully-typed TypeScript client: one JS runtime plus a .d.ts, with typed create/update DTOs, per-model id types, typed realtime subscriptions, and a session client (login / logout / me) discovered straight from the spec.

The client matches your API - pagination is configurable, auth is scheme-driven - instead of being a lowest-common-denominator fetch wrapper you hand-correct. Change a model, regenerate, and your frontend types move with it. It's the same anti-drift principle pushed all the way to the browser.

The data-modeling bench got deep (v0.0.7)

v0.0.7 was a big release beyond the client. A pile of ORM primitives landed that turn "I have models" into "I have a data layer":

  • Database views, regular and materialized - declare a view as a model and query it through the ORM like any table.
  • Request validation two ways: Valid<T> + #[derive(Validate)] validate a body at the extractor boundary, and #[derive(Dto)] declares custom response types that flow into the generated client.
  • A model audit trail - #[umbral(audited)] records who changed a row and when, and auto_user_add / auto_user stamp the acting user without a handler writing it.
  • Cascading soft-delete, so deleting a row takes its soft-delete children with it instead of orphaning them.
  • Parent-scoped sub-resources - ResourceConfig::under(...) mounts /projects/{id}/tasks with the scope enforced automatically.
  • Zero-downtime rollouts - the app drains readiness on shutdown and gates /readyz on pending migrations, so traffic never hits a half-migrated instance.

And the startproject scaffold got a real redesign: compiled Tailwind, the Umbral palette, working mobile navigation, and a landing page that shows how the framework works rather than talking about the generator.

Admin dashboards, and tests that can't drift (v0.0.9)

Two very different wins in v0.0.9.

The admin grew real dashboards: a saved layout you can drag to rearrange, per-widget filters, and one-click CSV export for any widget with rows behind it - no per-widget code.

The quieter win matters more for the framework's health: tests now derive their schema from the models. umbral-testing builds the test database from the model registry and boots in one line, and 205 existing suites were converted. Hand-written CREATE TABLE in a test is a place where a schema drift can hide; deriving it from the models means the tests exercise the real schema, and the conversion itself surfaced (and fixed) real bugs the old fixtures had been papering over. This release also closed the last gaps in non-i64 primary keys - UUID-keyed M2M relations, and SQLite uuid columns declared as the BLOB they actually are.

A security-hardening sweep (v0.0.10)

v0.0.10 was a deliberate pause to harden. A framework-wide review closed the confirmed critical and high findings, with fail-closed IN filters, a capped page cache, a bounded realtime broker, and coalesced writes across auth, cache, and realtime.

The multi-tenant surfaces got specific attention, because that's where a framework earns or loses trust:

  • Row-level GraphQL mutations - owned_by scopes a mutation to the rows the caller owns, so a write can't reach across tenants.
  • 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.
  • Reverse-FK lists windowed per parent - a parent's child list paginates per-parent instead of over the global table.
  • An analytics path scrubber - identifying segments (ids, slugs, tokens) are stripped from auto-pageview paths before they're logged.

Plus umbral startcommand for scaffolding app-owned management commands. Security work is rarely a headline, but shipping a framework means the boring, careful passes are the product.

Plugin ergonomics, and media that's private by default (v0.0.11)

The most recent release, v0.0.11, made wiring an app quieter and gave uploaded media real access control.

On the ergonomics side: models and task handlers discover themselves now (discovered_models!() and #[umbral::task] register via inventory - declare them anywhere, no manual list to maintain). One App::builder().authentication(...) sets an ambient auth backend that REST, GraphQL, realtime, and the media gate all inherit. AuthPlugin::new() dropped the turbofish. Small things, but they compound into an app that reads like intent instead of plumbing.

On media: uploaded files got three composable access gates.

  • .media_access_owner() serves a file only to the user recorded as its owner - a media_file.owner column and a one-line set_media_owner at your upload site, no hand-written join.
  • .media_signed_urls() mints HMAC-signed, time-bounded links that need no session - the signature binds the key and the expiry to your secret, so a leaked link can't be edited or extended.
  • .media_access_identity(...) hands you the resolved caller for a custom rule.

And crucially, a gated proxy streams private files through S3 and custom backends too - so gating isn't a filesystem-only feature. Private media on Umbral is now genuinely private, not a documented aspiration.

Where this leaves things

Step back and the shape is clear. In three weeks Umbral went from "declare a model, get REST + an admin" to "declare a model, get REST, GraphQL, a typed frontend client, an admin with dashboards, a background-task system, private media, and a security posture that's been swept for the obvious holes" - all from the same single declaration, all as plugins that compose through one contract.

The usual honest caveats still apply. Umbral is pre-1.0 and published on crates.io, so APIs still move between releases - treat any snippet as the current shape, not a frozen contract. And the framework is young; the value proposition is the integration and the conventions, not a claim that every edge is polished.

But the declare-once-derive-everything idea is holding up under real weight. Every surface added since v0.0.6 read the model registry and got wired for free, which is exactly the test that matters: if a new surface can't be expressed as a plugin over the model metadata, the contract is wrong. So far, it hasn't been.

If you want to try it: cargo install umbral-cli, then umbral startproject. The docs walk through every surface, and the changelog has the release-by-release detail behind everything above.