Skip to main content
Version: 1.11.0

Release notes

The current release is v1.11.0.

Nucleus is on the stable v1.x line (v1.0.0 tagged 2026-07-10): stable surfaces are frozen by contract tests, and every v1.x upgrade is designed to be drop-in for code that uses them — see Support & compatibility and the upgrade guide. Commit-level detail for every release, including the pre-1.0 history, lives on GitHub Releases.

v1.10.0 (2026-08-21)

A minor release: the vertical-slice module arc. A mounted module can now carry everything its feature needs, and a generator emits that shape.

  • Modules declare their own policy rows and CSRF exemptions. Module.Policies contributes RBAC rows (same shape as a rbac_policy.csv row, objects relative to the module Prefix) to the default-deny enforcer, and Module.CSRFExempt rides the same pre-startup window as the automatic webhook-prefix exemption. Rows join the live in-memory ruleset only — the host's policy file is never written, and a deny row in the host's CSV overrides any module allow. Malformed declarations fail boot with ErrInvalidModulePolicy naming the module and entry.
  • Embedded module migrations are applicable. One deliberate call — rt.ApplyModuleMigrations(), typically in OnStart — applies Module.Migrations through the real pipeline: the module-scoped ledger (<module>/<id>) with checksum tracking, idempotent across restarts. Application boot still never mutates the schema on its own; the boot warning about declared-but-unapplied migrations now names the call and goes quiet once the module uses it. Under the hood: db.NewModuleFSMigrator, an fs.FS-backed migrator usable directly.
  • Embedded module templates. Module.Templates registers a module's .html files under the <module-name>/ namespace; app.WithTemplatesFS(prefix, fsys) is the general accumulating extension point behind it. On a name collision the host's templates_dir parses last and wins.
  • nucleus generate module <name>. One self-contained package under internal/<name>/ — model + storage for the configured dialect, controller, and a module carrying its policy rows, CSRF exemption, embedded migrations and page template. Mounting it is the whole integration: no rbac_policy.csv or nucleus.yml edits, no manual migrate step, pinned by an executable-scaffold test that asserts the policy file stays byte-for-byte untouched.

v1.9.2 (2026-08-19)

A patch release that corrects documentation which promised more than the code delivered. No runtime behavior changes.

  • EnsureBucket's documented scope now matches what the code can do. The godoc suggested it could provision "the" bucket after construction, but NewS3Store verifies the configured bucket(s) up front and refuses to construct when one is missing — so a store whose own bucket is absent can never exist to call EnsureBucket on. The contract now states the real scope: provisioning buckets other than the store's own (exports, per-tenant spaces, scratch areas); self-provisioning the configured bucket is exactly what storage.s3.create_bucket_if_missing is for, at construction time. The storage guide says the same, and a live MinIO contract test (TestS3Live_EnsureBucketProvisionsAnotherBucket) pins it.

v1.9.1 (2026-08-18)

A patch release from the SSR arc's re-verification.

  • The outbox dispatcher starts after extensions attach. The dispatcher's first pass is immediate, and Extension.Attach — which ran later — is the supported way to register bridges: a message already durable in the table could be leased with an empty route registry and fail with "no bridge route matched", consuming a retry and dirtying attempts/last_error. The dispatcher now starts only after every extension has attached; a pre-existing pending message delivers on attempt 1. Durability semantics are unchanged.
  • The template extension point is reachable from the documented builder. nucleus.New() gains WithTemplateFuncs, WithTemplates and WithOpenAuthz (plus package-level re-exports) — v1.9.0's template options only existed on app.New, which the builder wraps. A parity test now enforces that every public application option has a builder counterpart, so this class of gap fails the suite instead of the next release.
  • API additions must regenerate the frozen baseline in the same change. The baseline only failed on removals, so v1.9.0's new symbols shipped unlisted and external coverage denominators undercounted the public surface. Regenerated, and a new check fails on unlisted additions.

v1.9.0 (2026-08-17)

A feature minor: the server-side render layer works the way the documentation prescribes. Found by the external coverage demo's first real MVC application (session login, CSRF forms, full CRUD).

  • Modules with a Prefix receive the template engine and the session manager. The prefix sub-router was built from scratch and never inherited either — so the documented way of declaring a module answered every render with "template engine is not configured" and every session helper with "session manager is not configured". The three composition paths (Group, With, Route) now share a single derivation function, so a future Mux-level dependency cannot be forgotten by one of them. Auditing this also surfaced that nothing ever handed the session manager to the router tree at all: app.New now does, and the Context session helpers (SessionPutString & co.) work everywhere.
  • Template functions and prebuilt bases. app.WithTemplateFuncs registers a template.FuncMap available to every template the startup loader parses, and app.WithTemplates injects a prebuilt *template.Template as the parse base — presentation logic (date formats, percentages, pagination URLs) belongs in templates, not precomputed in Go. Order: registered functions → recursive parse of templates_dir → the engine is wired into the router. See the routing guide.
  • csrf_insecure_cookie (development-only, default false) disables the Secure attribute on the CSRF cookies, mirroring session_cookie_secure: false — without it the double-submit flow was unreachable for plain-HTTP non-browser clients such as Go's cookie jar against http://127.0.0.1.
  • An SSR conformance suite now runs in CI: a module WITH a prefix, served over real HTTP, must render a loaded template by name, keep session state (write → read → destroy), enforce CSRF (419 without the token, 200 with it), serve module-mounted statics, and apply registered template functions — the five things any server-rendered application needs on day one, none of which had a test before.

v1.8.2 (2026-08-17)

A patch release: three findings from the external coverage demo's first server-rendered MVC application.

  • The scaffold renders with the framework's own engine. app.New used to load templates with a flat glob while nucleus startapp scaffolds its template into a subdirectory — on a fresh project no template loaded and every render answered "template engine is not configured", with no startup warning. The loader now walks templates_dir recursively; each template registers under its path relative to that directory (fieldservice/index.html), root files keep their flat name (base.html), and {{define}} blocks keep their declared names — flat layouts keep resolving unchanged. Startup logs templates loaded with the count, a present-but-empty directory logs a WARN, and the render-without-engine error now says what to check. A new executable-scaffold test boots a scaffolded project in CI and demands the generated page renders over HTTP — the class of generator/runtime disagreement that produced this finding (and the SQLite-DDL one before it) now fails the suite instead of the first user.
  • Outbox: per-instance lease owner and a routing-policy knob. Every process used to write lease rows as the same literal nucleus-app, so a co-tenant process could lease — and fail — messages another instance was able to deliver, untraceably. The default owner is now derived per instance (nucleus-<hostname>-<pid>); outbox.lease_owner sets a stable identity and outbox.missing_route_policy (error, the previous behavior, or ignore) controls what happens when a leased topic has no registered bridge. Startup logs both.
  • SessionCache.Flush no longer panics. Flushing sliced every session key to a fixed length, panicking on unrelated keys of 8–13 bytes — ordinary session data. It now uses prefix checks and touches only cache-prefixed entries.

v1.8.1 (2026-08-17)

A patch release closing the loop on S3 bucket provisioning.

  • storage.s3.create_bucket_if_missing is reachable from nucleus.yml. The missing-bucket startup error recommends that exact key, but the strict configuration validator rejected it: the key existed in pkg/storage and not in the application config schema — a circular repro with no operator exit (the environment form did not work either). The key now loads from the file and from NUCLEUS_STORAGE__S3__CREATE_BUCKET_IF_MISSING, and reaches the storage constructor. Opt-in with default false: a missing bucket without the flag still fails startup loudly, exactly as before.
  • A parity test keeps the two config surfaces in sync. The application schema mirrors pkg/storage's own config structs; every storage key must now be mirrored or carry an explicit, reasoned exclusion — so a future divergence fails the suite instead of becoming another advertised-but-rejected key.

v1.8.0 (2026-08-16)

The developer-experience minor: the scaffolding, testing and configuration gaps measured by the 2026-08-16 developer-experience audit, on top of the fixes already landed in earlier patches.

  • generate resource (and startapp) emit a mountable module wired to a real repository. The scaffold now produces a nucleus.Context controller implementing the REST Resource sub-interfaces, a repository running real SQL against the framework-managed *sql.DB (statements rendered for the configured dialect — RETURNING on PostgreSQL, OUTPUT INSERTED on SQL Server, OUT bind on Oracle, LastInsertId elsewhere), and internal/modules/<name>_module.go: nucleus.New().Mount(modules.<Name>Module()) is the whole integration. The *router.Mux handler that forced a hand-written adapter and the in-memory map repository are gone; startapp shares the same artifacts and its module also serves the scaffolded page.

  • In-process test kit — pkg/nucleustest (experimental). nucleus.RunContext(ctx, app) gives the run loop a caller-owned lifetime (cancel = graceful shutdown), and the kit wraps it: nucleustest.Start(t, builder) boots the app on a free loopback port, waits for /healthz, stops via t.Cleanup, and MintToken issues bearer tokens against the app's own jwt_secret. E2E suites no longer need go build + exec.Command + hand-rolled polling.

  • profile: dev boots a realistic config with zero backing services. A production nucleus.yml naming PostgreSQL (+ replica), two Redis endpoints, S3 and SMTP boots unchanged with profile: dev (or NUCLEUS_PROFILE=dev): in-memory sessions and jobs, local filesystem storage, the no-op mailer, and SQLite (an already-SQLite URL is kept; extra database aliases are dropped). Unknown profile values fail config load. Identical on app.LoadConfig and the fluent loader.

v1.7.0 (2026-08-16)

A feature minor: the security composition the docs describe now works end to end, and object storage can provision itself.

  • The global default-deny authorization layer sees JWT claims. When JWT signing material is configured, a bearer is decoded ahead of the global enforcement and the request's subjects are tried in order — the token's user id, its role, then anonymous. Role-based CSV policies (p, admin, /api/admin/*, read, allow) finally work at the global layer without re-implementing RBAC per module. Strictly non-restrictive: requests that passed before still pass (the anonymous fallback preserves bootstrap grants for authenticated callers); requests that were wrongly denied now succeed.
  • S3 bucket bootstrap. storage.s3.create_bucket_if_missing: true provisions the configured bucket(s) at startup; S3Store.EnsureBucket is the programmatic, idempotent form. Behaviour change: without the opt-in, a missing bucket now fails the constructor loudly (with an actionable message) instead of booting green and failing on the first upload.
  • ServiceRegistration.Health is wired into /healthz as the check service:<name>; a failing service flips the endpoint to 503. New building blocks: health.FuncProbe and app.RegisterHealthProbe for application-owned checks.

v1.6.2 (2026-08-16)

A patch release: dumpdata/loaddata round-trip on schemas with foreign keys, plus documentation honesty fixes.

  • loaddata inserts in FK-dependency order. The load plan is now ordered topologically by the foreign-key graph introspected from the target database, so a fixture produced by dumpdata (which lists tables alphabetically) restores in a single invocation instead of failing on the first child table. The caller's order — the file's, or an explicit --tables — is the stable tie-break: a valid explicit order passes through unchanged (it used to be silently re-sorted alphabetically), an FK-invalid one is repaired. Self-references are skipped and FK cycles fall back to the given order rather than failing the load; the dry-run plan shows the real order.
  • Documentation: the health-probe comment no longer denies the mail probe the code performs; the storage guide's import path is current; the auth guide's policy examples use the CRUD action vocabulary the middleware actually enforces (read/create/update/delete), with the mapping spelled out; ServiceRegistration.Health now states loudly that it is accepted but not yet wired into /healthz.
  • The showcase example re-pins the current sibling tags (nucleus v1.6.1 at cut time, agent v0.5.7, server v0.9.2, quarkbridge v0.3.7, quarkdatasource v0.2.8, quark v1.4.1).

v1.6.1 (2026-08-15)

A patch release: the resource scaffolder targets your real database.

  • generate resource / startapp emit migration DDL for the configured database instead of unconditional SQLite. A project configured against PostgreSQL used to receive "id" INTEGER PRIMARY KEY AUTOINCREMENT and DATETIME columns, and nucleus migrate then failed with a syntax error on the very migration the CLI had produced. The scaffold now resolves the dialect from the project's config (the same config nucleus migrate reads, NUCLEUS_* overrides included), and both commands accept --dialect / --config / --database to override it. A fresh project with no config keeps the sqlite default.
  • New helpers for the same decision in your own tooling: db.SystemFromURL (URL → SQL system, no connection) and model.BuildMigrationScaffoldForSystem (dialect-dispatched scaffold).
  • The generated repository is an in-memory placeholder and now says so — a note in the generated file and in the command output replaces the previous silence about the migration's table going unused.
  • Security: Go 1.26.6 (stdlib CVEs), grpc v1.82.1, otel v1.44.0; the nucleus new scaffold inherits the same directives, and the showcase example re-pins quark v1.4.1.

v1.6.0 (2026-07-22)

Defense-in-depth hardening of the webhook surface, from the first directed security review of the continuous-audit regime. No behaviour changes for correctly-configured apps; the wire is unchanged.

Added

  • Webhook registration rejects non-canonical mounts. A module webhook registered with path == "/" (which would mount a catch-all subtree) or a module name containing ..// (which would shift the mount point) now fails boot instead of mounting something surprising. Canonical paths and names are unaffected.
  • Outbox payload-encoding header is informational by design. The bridge signs the body only — byte-for-byte the module-webhook scheme, so one verifier covers both surfaces — and X-Outbox-Payload-Encoding is now documented as unsigned/informational. New consumer helper outbox.CheckPayloadEncoding decodes by the encoding a consumer expects and rejects a mismatch (ErrPayloadEncodingMismatch), rather than trusting the request header. The signed wire is unchanged from v1.5.0.

Upgrade notes

Drop-in. If a module registered a webhook at / or with a slash in its name, that was already broken (unreachable or mis-mounted) and now fails loudly at boot — give it a real path. Outbox consumers should verify the body-only signature with the module-webhook verifier and check the payload encoding against their own config (see CheckPayloadEncoding), not the request header.

v1.5.0 (2026-07-22)

Signs and versions the outbox webhook contract, hardens module webhooks (canonical paths, opt-in anti-replay), and fixes Oracle pagination and S3/GCS not-found detection. Drop-in: the outbox wire is unchanged by default and the new webhook behaviour is opt-in.

Added

  • The outbox bridge webhook has a signed, versioned contract. With outbox.bridges.<n>.config.secret, every delivery carries an HMAC-SHA256 signature over the body in X-Nucleus-Signature (sha256=<hex>) — the same scheme module webhooks verify, so one verifier covers both. Every delivery also declares its payload shape in X-Outbox-Payload-Encoding: json|base64, so a consumer never guesses. The wire is byte-for-byte the v1.4.0 default (base64); payload_encoding: json opts into embedding the payload as JSON. A body-level contract test compares the emitted webhook byte for byte per variant — the gap the symbol-only freeze cannot see. Without a secret, deliveries are unsigned and a boot WARN says so. See Storage & background tasks.
  • Webhook anti-replay (opt-in). WebhookSpec.TimestampTolerance > 0 requires an X-Nucleus-Timestamp header inside the signed material (SignWebhookBodyWithTimestamp), rejecting stale or tampered timestamps. The default (tolerance 0) keeps the body-only scheme unchanged. The absence of anti-replay in the default scheme is now documented as a limit, with event-ID dedup as the recommended pattern.

Fixed

  • Oracle pagination emits valid SQL. FindAll and FindByID in pkg/model used LIMIT on Oracle (ORA-00933); they now use OFFSET … FETCH NEXT … ROWS ONLY / FETCH FIRST 1 ROWS ONLY, the twin of the earlier MSSQL fix. The admin-user CLI lookup is fixed the same way. Exercised against a real Oracle in CI.
  • Webhook paths must be canonical. A module webhook registered with a non-canonical path (.., ., doubled or trailing slash) now fails boot instead of mounting an unreachable route.
  • S3/GCS not-found by SDK type, not error text. Get/Exists of a missing key now map to storage.ErrNotFound against real endpoints (previously matched on the error string, which a real S3 endpoint does not produce). A real-MinIO CI lane covers it.
  • Security: golang.org/x/text bumped to v0.39.0 (GO-2026-5970).

Upgrade notes

Nothing to change. If a bridge was relying on the base64 payload wire, it is unchanged; opt into payload_encoding: json when your consumer is ready. Configure outbox.bridges.<n>.config.secret to start signing deliveries.

v1.4.0 (2026-07-20)

Module jobs and webhooks are now executed, not just declared: the Jobs and Webhooks closures a module registers run for real, backed by the existing task runtime and the application router. Also fixes a stop-path bug in the Asynq task provider and rejects, on request, primary keys assigned by HTTP clients. Drop-in upgrade; the new surfaces are opt-in.

Added

  • Module jobs run on a real scheduler. JobRegistry.Register(name, spec) schedules background work declared by a module: Every for fixed intervals or Cron for 5-field cron expressions and descriptors (@hourly, @every 90s), validated at boot and identical on every provider; per-run Timeout; and Singleton to skip a tick while the previous run is still executing. The jobs_provider key selects the runtime — memory (default, in-process) or asynq (Redis-backed, durable, with jobs_redis_url and jobs_concurrency). A broken registration (duplicate name, invalid cron, missing handler) fails boot instead of silently never running. See Module jobs and webhooks.
  • Module webhooks mount real routes. WebhookRegistry.Register(path, spec) mounts an inbound receiver at <webhooks_prefix>/<module><path> behind a method allow-list (405), a body cap (413, default 1 MiB) and — when Secret is set — constant-time HMAC-SHA256 verification of the X-Nucleus-Signature header, rejecting unsigned or mis-signed requests with 401 before your handler runs. nucleus.SignWebhookBody produces the signature for senders and tests. With csrf_enabled: true the webhook prefix is exempted automatically — webhooks authenticate by signature, not CSRF token. A webhook registered without a Secret is flagged at boot.
  • RejectClientPK. A per-model opt-in that rejects entities arriving through Create with a client-assigned primary key (model.ErrClientAssignedPK), for apps that bind request bodies straight into models. The check runs before hooks, so server-side key assignment in BeforeCreate keeps working.

Fixed

  • The Asynq task worker stops when you stop it. Manager.Run waited on OS signals internally, so cancelling its context (or calling Close) shut the server down but never unblocked Run — an embedded worker could not be stopped through the API. Run now returns promptly on context cancellation and on Close.
  • Boot no longer warns about declared jobs and webhooks. The "background execution is not yet wired" readiness warning is gone — both surfaces execute. The warning for embedded migrations stays: Nucleus is SQL-first and never auto-applies them.

Upgrade notes

Nothing to change in existing apps. If a module already declared Jobs or Webhooks closures (previously inert), they now execute on the next boot: review those closures before upgrading, set jobs_provider if you want durability over the in-process default, and note that invalid registrations that were silently ignored before now fail startup — which is the point.

v1.3.3 (2026-07-19)

A correctness patch: client-assigned primary keys work through Create, unsupported engines fail at startup instead of at runtime, and two more surfaces emit valid T-SQL. Drop-in for most apps — read the upgrade notes if you point the sql session store or the outbox at SQL Server or Oracle.

Fixed

  • A pre-assigned primary key now travels in the INSERT. Client-generated keys (UUIDs, natural keys) were silently dropped from the insert: SQLite stored a row with a NULL primary key without any error, and PostgreSQL/SQL Server failed with a NOT NULL violation. A non-zero key is now included in the statement, and the read-back / back-fill machinery is skipped, so the entity keeps exactly the key you set. A zero-value key keeps the previous behavior: the column stays out of the INSERT and the database generates the key. See Models & database — including the security note on accepting keys from HTTP clients.
  • The SQL session store and the outbox refuse unsupported engines at startup. Both subsystems speak SQLite, PostgreSQL and MySQL only, but an MSSQL or Oracle database URL used to be silently treated as SQLite — the failure surfaced later, mid-request, as invalid SQL. Construction now fails at startup with an error naming the supported engines.
  • not null is matched exactly in db: tags. db:"not null unique" (a space where a ; was intended) used to mark the field required and silently lose the unique; the malformed directive now falls through to the startup WARN introduced in v1.3.2 instead of half-applying.
  • By-id operations reject models without a primary key. FindByID, Update and Delete on a model that declares no primary key return an explicit "model has no primary key" error (check with errors.Is) instead of guessing a phantom id column, and the default list ordering falls back to a real column of the model.
  • nucleus createuser and nucleus changepassword emit valid T-SQL. Their admin-user lookups used a LIMIT clause SQL Server does not accept; on MSSQL they now use SELECT TOP 1.

Upgrade notes

If your configuration points the sql session store or the outbox at an MSSQL or Oracle database, the app now stops at startup with a clear error instead of failing later with invalid SQL. That configuration never worked — it silently ran SQLite-flavored SQL against the wrong engine — but a deployment that "started fine" before the upgrade will now refuse to boot until those subsystems point at a supported engine (SQLite, PostgreSQL, MySQL).

v1.3.2 (2026-07-19)

A correctness patch focused on the model layer's db: tags and on Create across database engines. Drop-in.

Fixed

  • Unknown db: tag directives now warn at startup. A directive the parser does not recognize was — and still is — applied as nothing; the difference is that the app now logs one startup WARN per affected field, naming the unrecognized tokens and the supported syntax, instead of leaving you trusting a constraint that never existed. db:"-" now excludes a field from persistence.
  • Create only reads back the generated key when it actually can. The RETURNING / OUTPUT INSERTED read-back is now emitted only for models that declare a real, integer primary-key field. Models with string/UUID keys or without a declared primary key previously got a read-back query that could fail (for example against tables with no id column); they now take the plain insert path, matching SQLite/MySQL behavior.
  • List pagination on SQL Server emits valid T-SQL. Paginated list queries used a LIMIT clause SQL Server does not accept; they now use the OFFSET … FETCH form. The whole CRUD surface is exercised against a real SQL Server (and Oracle) in release validation.
  • The version pinned by nucleus new can no longer go stale. The framework version written into generated go.mod files is maintained by the release tooling and cross-checked in CI on every build.

Upgrade notes

Nothing to change. If your startup logs show new WARN lines about db: tags, those tags were already being ignored — fix the tag syntax, don't silence the log. See the FAQ for the supported directives.

v1.3.1 (2026-07-15)

A one-fix patch. Upgrade if Create should hand you the generated primary key on PostgreSQL or SQL Server.

Fixed

  • Create backfills the generated primary key on PostgreSQL and SQL Server. Those drivers do not implement LastInsertId, so the entity's ID field silently stayed at zero after a successful insert. Create now uses RETURNING (PostgreSQL) / OUTPUT INSERTED (SQL Server) to populate it. Oracle remains a declared gap — see Support & compatibility.

v1.3.0 (2026-07-13)

A minor release that completes the v1.2.0 security hardening pass and rounds out observability.

New

  • Opt-in driver-level SQL instrumentation (sql_driver_instrumentation). Off by default (zero hot-path cost); when enabled, direct QueryContext/ExecContext statements that bypass the model layer — session stores, outbox dispatch, migrations, raw SQL — also reach the observability live SQL feed, without double-recording CRUD statements.
  • The observability package and its hooks are now stable, covered by the same compatibility promise as the rest of the framework.

Security

  • CSRF protection as a config switch. csrf_enabled: true mounts origin verification (Sec-Fetch-Site) with a double-submit token fallback; csrf_exempt_paths excludes Bearer-only subtrees. The mvc scaffold enables it by default.
  • metrics_public: false takes /metrics out of the anonymous allow-list and puts it behind the default-deny RBAC enforcer.

Upgrade notes

Both new switches default to the previous behavior (csrf_enabled: false, metrics_public: true); nothing changes until you opt in.

v1.2.0 (2026-07-12)

A security-hardening minor. Existing deployments can notice these changes at upgrade time — read the notes below.

Security

  • jwt_secret must be at least 32 bytes. Any non-empty value used to be accepted; a shorter secret is now a boot error. Generate a proper one (openssl rand -base64 32) or move to jwt_keys[].
  • Proxy headers are no longer trusted by default. X-Forwarded-For / X-Real-IP are ignored unless the immediate peer is listed in the new trusted_proxies key; otherwise the TCP peer address is the client IP for rate limiting and logs.
  • HSTS is emitted only over TLS or when explicitly forced (env: production) — plain-HTTP development runs are no longer pinned to HTTPS by a stray header.

Upgrade notes

  • Short jwt_secret values fail the boot — rotate the secret before upgrading.
  • If Nucleus runs behind a load balancer, set trusted_proxies to its address ranges or rate limiting will see every request as coming from the balancer.

v1.1.0 (2026-07-11)

New

  • SQL events report rows affected. The observability feed's SQL events carry the driver-reported RowsAffected. Additive; drop-in.

v1.0.0 (2026-07-10)

The first stable release. The compatibility promise starts here: stable surfaces are pinned by contract freeze tests and change only through the documented deprecation policy.

Breaking

  • Cross-origin requests are denied by default. The implicit allow-all CORS default is gone: an empty cors_origins now emits no CORS headers at all. Deployments that relied on allow-all must opt in explicitly — a real origin allow-list, or cors_origins: ["*"] to keep the old behavior.

Upgrade notes

If browsers suddenly report CORS errors after this upgrade, set cors_origins to the exact origins your frontend uses. Everything else in v1.0.0 is the certification of surfaces that already existed in v0.12.x.