跳转到内容

Upgrading

此内容尚不支持你的语言。

An upgrade is five steps. Skip either of the two database steps and you get a deployment that looks fine and is subtly broken.

  1. Read the changelog. The changelog is kept per release; every entry that needs an operator action says so. Releases are also on GitHub.

  2. Move to the new version. Change the pinned image tag, or pull the new git tag and rebuild:

    Terminal window
    git fetch --tags
    git checkout v<version>
    bun install
    bun run build

    The published tags are listed on the GitHub releases page and on the container package page. Images are signed and carry an SBOM, so this is the step at which to verify the version you are adopting — you are reading the changelog anyway.

  3. Re-run the provisioning step against the same database:

    Terminal window
    bun run db:setup # MongoDB
    bun run db:setup:pg # PostgreSQL
  4. Apply any schema migrations the release declares:

    Terminal window
    bun run db:migrate

    One command for either datastore. It reports current and exits when there is nothing to do, which is how most upgrades go. --plan lists what would run without running it and exits non-zero when work is outstanding, so a pipeline can gate on it before the deploy instead of during it.

    A migration that cannot be undone is called out by name before it runs. When you see one, take a backup first.

  5. Restart the server. It refuses to start against a database whose schema is not the one the release expects: behind, ahead, or holding a record for a migration whose declaration has since changed. A missed step 3 or 4 therefore stops the deploy instead of corrupting data quietly.

db:setup is the only thing that applies a release’s structural changes. It is also idempotent: it upserts, so running it against an already-current database changes nothing. What a release can add:

  • New collections (tables, on PostgreSQL), for a capability that did not exist before.
  • New indexes, TTL indexes included. A collection that MongoDB auto-creates on first write has no TTL index, so documents that were meant to expire simply do not, and nothing complains. The script also drops stale expiry indexes for a field a release has stopped writing. On PostgreSQL there are no TTL indexes to create; the running server sweeps expired rows itself.
  • Seed changes. When a release changes the admin seed, db:setup is what applies it: a new reserved client, a new field on the administrator bucket, a group the ownership model now expects. The reserved admin-mcp agent client arrived exactly this way. The script carries an explicit $addToSet for it, because $setOnInsert would not touch a project that already existed.

db:setup exits non-zero when a constraint it declares is not in force, so make it a gate instead of a fire-and-forget. The shipped fly.toml runs it as release_command and Fly aborts the deploy on failure; the Compose files run it as a setup service the server depends_on with service_completed_successfully; on Kubernetes it is a Job you wait for.

db:setup only ever adds things: a collection, an index, a seeded document. That is the only kind of change safe to repeat against a database in any state. Reshaping data that already exists is a different job. It has to run once, in a declared order, and be recorded so the next deploy knows it happened. That is what db:migrate does, on either datastore, and it is why the two are not one command.

Three things follow from the record it keeps, and each of them stops a deploy instead of letting it proceed on a guess:

  • A database behind this release has outstanding migrations. Run them.
  • A database ahead of it holds a record for a migration this release does not declare, which means the binary is older than the database — a rollback past a migration, usually.
  • A database is diverged when a migration it already applied no longer matches the one declared under that name. A released migration edited after the fact is the cause; the fix is a new migration, and the released one stays as it was.

Only one process migrates at a time. A run takes a short renewable lease, so two replicas rolling at once do not both apply the same step, and a crashed run leaves a lease that expires rather than a database nobody may touch.

What a restart carries, and what it does not

Section titled “What a restart carries, and what it does not”
  • Server settings are already in force. A change saved in the console takes effect in the instance that saved it, so an upgrade neither applies nor re-applies it: the stored settings are simply read again at startup. An upgrade is still a good moment to re-read the settings reference for anything the release added. A setting that genuinely cannot be applied to a running server says so on its card and in the console, and there are none at present.
  • Signing keys persist in the database — every issuer’s, the instance’s included, as records in bucketKeys. An upgrade neither rotates nor invalidates them: tokens issued before it still verify, and every /jwks serves the same key set it did. Keys are not environment configuration and there is nothing to carry across by hand. The one exception is the release that gave buckets keys of their own, below; 0.8.0’s migration moved the instance’s keys into bucketKeys without changing them.
  • SMTP settings are read live from their own store on every send, so they are unaffected by a restart in either direction.
  • Tokens, sessions and grants survive, since they live in the database. Users are not signed out by an upgrade, with one release-specific exception below.

Upgrading to the release that made user buckets addressable, and only that one, ends every end-user sign-in once.

A sign-in is held in a browser cookie, and that cookie is now named after the bucket it belongs to — _session_default, _session_acme — so that somebody can be signed in to two buckets at once without either sign-in disturbing the other. Cookies written before the upgrade carry the old, bucket-less name. They are not read, and the server expires them on the first request that presents one, so a browser stops sending a cookie that no longer means anything.

What your end users see: the sign-in screen, once, the next time they visit. Nothing is lost — accounts, grants, consent and refresh tokens are all untouched, and a refresh token keeps working without anybody signing in at all. There is nothing to do on your side and no order to do it in; it is listed here so the support question is expected rather than alarming.

The console is unaffected in one respect worth naming: it stays at /admin. An earlier plan moved it to free that word for a bucket address, and that turned out not to be necessary — the administrators bucket is served at the root and never puts its name in a URL.

One upgrade gives each bucket keys of its own

Section titled “One upgrade gives each bucket keys of its own”

Upgrading to the release that gave addressable buckets their own signing keys and resource namespaces changes three things an integration can see. The instance’s own issuer, its metadata and its /jwks are unchanged; everything below concerns buckets with a path or a hostname of their own.

  • A bucket signs with its own key from its first use. Before, every bucket signed with the instance keys and its discovery pointed jwks_uri at the instance’s /jwks. Now its jwks_uri is beneath its own issuer — https://auth.example.com/acme/jwks, or its hostname’s /jwks — and holds only its own keys. A resource server that re-reads the bucket’s metadata picks this up by itself; one configured with the instance’s /jwks by hand has to be pointed at the bucket’s.
  • Tokens a bucket issued before the upgrade stop verifying at its resource servers. They were signed with the instance keys, which the bucket no longer publishes. The switch is immediate, with no overlap, because an overlap would keep one tenant’s tokens verifiable at another’s. Access and ID tokens are short-lived and refresh tokens are unaffected, so clients recover on their next refresh; plan the upgrade for a quiet moment if long-lived access tokens are in use.
  • The release carries its first schema migration, and it cannot be undone. It re-keys declared protected resources so that an identifier is unique per issuer rather than across the instance. db:migrate --plan names it; take a backup before db:migrate, which step 4 above runs. From then on, declaring a resource in a bucket of one’s own needs nothing from the resource, and at the instance’s root issuer only a super administrator declares.

One upgrade turns roles into groups and moves the instance’s keys

Section titled “One upgrade turns roles into groups and moves the instance’s keys”

Upgrading to 0.8.0 carries two schema migrations, and neither can be undone. Do the steps in this order:

  1. Take a backup. There is no way back from either migration except a restore.

  2. Run db:setup (or db:setup:pg). The release adds storage areas the migrations and the new features write into — bucket groups and their memberships, provisioning connections, the deprovisioning tally — and db:migrate expects them to exist.

  3. Run db:migrate --plan, then db:migrate. The plan names both migrations below. The server refuses to start against a database that has not run them, so a skipped step stops the deploy rather than serving from a half-converted database.

2026-09-30-root-keys-lifecycle moves the instance’s signing keys into the store a bucket’s keys already use, so that the instance’s keys rotate without a restart too. For each algorithm, the key that signs afterwards is the key the old server was signing with; on PostgreSQL, which keeps no insertion order, that is the key with the lowest kid. The migration prints which key it chose for each algorithm, so check that output against what your relying parties expect. Every other key becomes published only: it is still served at /jwks and still verifies, but it does not sign. A migrated key can be promoted once the 60-second publication window has passed. See Rotate signing keys.

2026-10-08-roles-to-groups removes roles. Every role a bucket declares, or an end user holds, becomes a bucket group with exactly the same holders as members. Role names that differ only in letter case are merged into one group, blank names are skipped, and a name some user held but the bucket never declared is kept as a group. The migration reports each of these as a count. On the administrator side, every holder of super_admin, active or deactivated, becomes a member of the Super administrators group. project_admin disappears without replacement, because it never granted anything.

What an integration sees after 0.8.0:

  • roles is gone from the admin API and from MCP. A script or agent that set roles on an administrator, a bucket or an end user is now refused, because the field is unknown. Super administrator standing is granted and withdrawn by operations of its own, admin_super_grant and admin_super_withdraw, and end users are grouped with the bucket_group_* tools. Relying parties that read roles from a claim read the groups claim instead.
  • Refresh tokens are served only while offline_access is a supported scope. The token endpoint now accepts exactly the grant types discovery advertises, so with offline_access absent from the scopes setting, a refresh, including one using a token issued before the upgrade, is refused with unsupported_grant_type. refreshToken.enabled, which changed nothing anyone could observe, is removed.
  • ID tokens from a password sign-in now carry "amr": ["pwd"]. Every ID token from a sign-in states how the user authenticated: ["pwd"], or ["pwd", "otp", "mfa"] with a one-time code; a federated sign-in carries none. A test or relying party that treated the mere presence of amr as “used a second factor” now gets that wrong. Look for mfa or otp in the array instead.
  • jwks_delete is gone, and so is deleting a key. Keys are retired instead, and a retire, of an instance key or a bucket key, is refused unless its body names the key: { "confirm": "<kid>" }. The console makes the operator type the kid.

0.9.0 declares no migration. It adds one storage area, UpstreamSession, which back-channel logout from upstream providers needs, so run db:setup (or db:setup:pg) and restart.

One fix in it concerns the upgrade you have just read about. The Compose files shipped before 0.9.0 ran only db:setup in their setup service, so pulling 0.7.0 or 0.8.0 over an existing database left a server that refused to start. The shipped docker-compose.yml and docker-compose.postgres.yml now run db:migrate after it. If you keep your own copy of an older file, add the step to its setup command. A Compose deployment that has refused to start since it moved to 0.7.0 or 0.8.0 is this: take the backup those releases asked for, then run db:migrate once.

0.10.0 carries one schema migration, and it cannot be undone. A personal group used to be able to gain members; from this release it has exactly one, its owner. The migration 2026-10-09-personal-groups-single-member removes everyone else from each personal group and writes one audit entry per group. Before you upgrade, list the personal groups with more than one member (group_list with kind: personal). Move what they hold into a regular group, or note who loses access.

Then take a backup and run db:setup (or db:setup:pg). It adds the activityMarks and activityFigures areas that count active users, and a new index on the audit trail. db:migrate --plan names the migration, and db:migrate runs it.

What else an operator or an integration sees after 0.10.0:

  • The administrators’ settings are bucket settings now. GET and PATCH /admin/api/admins/settings and the MCP tools admin_settings_read and admin_settings_update are removed. Read and change the administrators’ bucket with bucket_get and bucket_update, as any other bucket.
  • A setting stored with the wrong type stops startup. The settings API never writes one, so this only concerns a value edited into the database by hand, such as a flag stored as the string "false". The error names the key. A malformed richAuthorizationRequests.types stops startup too, even while RAR is off, because the consent screen reads it.

0.11.0 carries one schema migration, and it cannot be undone. A SCIM static token used to live until it was replaced or revoked; from this release it expires (RFC 7644 §7.4). The migration 2026-10-12-static-token-expiry gives every static token that has none an expiry a year after its issue, and never sooner than 90 days after the upgrade, so no directory stops syncing that day. Note the dates it sets and plan the replacements: a directory that still holds an expired token is refused.

Then take a backup and run db:setup (or db:setup:pg). It adds the provisioningRefusals area, which records the removals a held connection refused, and new indexes: prefix search over users’ emails and usernames, the audit trail’s bucket filters, and a connection’s previous static token. db:migrate --plan names the migration, and db:migrate runs it.

What else an operator or an integration sees after 0.11.0:

  • Administrators should enrol their authenticator again. Before this release the administrator list and the answers to creating and updating an administrator carried every enrolled administrator’s TOTP secret, readable by any super administrator or an agent acting for one.
  • The users list is paged by the server. GET /admin/api/buckets/:id/users and bucket_user_list answer { users, totalResults } in email order, a page at a time, instead of an array of everyone.
  • More changes ask to be confirmed. Setting or clearing a project’s bucket, tightening a bucket’s sign-in (passwords off, verification or an authenticator required, sign-up opened on a reserved bucket) and handing a group to a directory answer 409 with what they do. They apply only when the request is repeated with confirm: true and the same expect. A script that did any of these in one request must now send the second.
  • Deleting a bucket schedules it. DELETE /admin/api/buckets/:id switches the bucket off and destroys it 30 days later; POST /admin/api/buckets/:id/purge destroys a scheduled bucket at once, and bucket_restore brings it back. Its address stays taken until it is destroyed.
  • New buckets are closed to sign-up unless the request asks for it, and requiring a verified address is refused on every bucket until mail delivery is configured.
  • A user’s profile wins over a stored claim of the same name. An application that read a given_name from a user’s claims document while the profile held another now receives the profile’s.
Terminal window
curl -sf https://auth.example.com/ready
curl -s https://auth.example.com/.well-known/openid-configuration | jq -r .issuer

/ready rather than /health on purpose: /health answers for the process alone and would pass against a server that came up unable to reach its database. The second is the check worth automating. The shipped deploy workflow does exactly this, because the advertised issuer is the value a deploy can get wrong without anything else noticing.

Then open /admin and sign in. That single act exercises the seed, the console client, the administrator bucket and the authorization code flow at once, so if it works, the upgrade landed. Super administrators should also glance at Faults. Only genuine defects are recorded there, never routine client rejections, so anything new after an upgrade is probably worth reading.