A big batch landed since the last update. Here are the highlights.

Secrets at Rest

Tenant secrets (SSO config, webhook signing keys, Slack URLs) are now encrypted with per-tenant envelope encryption: a per-tenant data key (DEK) sealed under a key-encryption key (KEK), with an atlas9 keygen subcommand to generate the KEK. Session secrets are hashed at rest, and the session token is split into a key and a secret so the key can be logged for request correlation without exposing anything usable.

Entitlements and Quotas

Tenants have per-tenant entitlements enforced by SQLite triggers on insert. It's fail-closed — a tenant with no entitlements row can't create anything — so seeding defaults and backfilling existing tenants is part of the migration.

Grant Planes

The IAM model got a real overhaul. Platform-operator access moved out of the tenant grant tables into a separate system_grants plane, which closes an owner → cross-tenant escalation. Grants split per principal type (user_grants / group_grants / bot_grants) with real foreign keys, and root resources now scope to their owner (a tenant or a user) rather than borrowing the operator plane. The old god-mode access.Admin() is gone — bootstrap and offline paths use an explicit AllowAll guard chosen at store construction instead of being smuggled through context.

Guarded Egress

Every external effect now goes through an app-owned layer that enforces a capability before delegating to a dumb transport, so the check is a structural chokepoint rather than a convention. Mail was first (CapMailSend), then webhooks, Slack, and SMS all followed the same shape.

Notifications

Tenant-scoped webhooks with signed (HMAC), at-least-once delivery, drained by the standard task worker so retry and backoff come for free. Slack channels parallel them. An events.Dispatcher fans a single domain event out to the audit log (the floor) and to each opted-in channel, routing by asserting events against small interfaces so it doesn't import the feature packages.

MFA

SMS-based second factor, mirroring the mail stack end to end (transport, mock receiver in the dev UI, a guarded egress layer, and a per-tenant policy). When a tenant requires MFA, login withholds the session until the texted challenge is redeemed via Identity_VerifyMFA.

Scheduler and Tasks

The task system consolidated onto a generic Producer[T] with task_id as the primary key. A DB-authoritative scheduler drives a system_schedules table — a conditional-UPDATE claim is the fleet coordination point — and now runs prune_sessions / prune_tokens jobs. The long-dead outbox tables, never wired to anything, were finally dropped.

CLI

A generated CLI client (atlas9api) with a command per endpoint and bearer auth. The --token flag is gone — the token resolves from ATLAS9_TOKEN or a 0600 credentials file only, so it never leaks into shell history or process listings.

Dashboard and Site

The demo dashboard is now an app shell that lands users in the product (todos) with a workspace switcher, admin pages grouped under Settings, self-service bot keys, a per-tenant audit log page for owners, and members/invitations merged into one table. A shared kit.js removes the copy-paste drift between the dashboard and the (amber-branded) admin console. There's also a new static site generator (atlas9 gensite) behind the demo's marketing site, and the API reference is now generated as a full HTML page rather than a bare method list.

Plumbing

Bumped to Go 1.26 and moved API response encoding to encoding/json/v2 (nil slices/maps render as []/{} instead of null). The dev runner got uniform trace-style logs across every phase, a fresh log file each run, and reliable live rebuild that keeps serving through a failed build.


Check out the demo app code at https://atlas9.dev/src/apps/demo/