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.
-
Read the changelog. The changelog is kept per release; every entry that needs an operator action says so. Releases are also on GitHub.
-
Move to the new version. Change the pinned image tag, or pull the new git tag and rebuild:
Terminal window git fetch --tagsgit checkout v<version>bun installbun run buildThe 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.
-
Re-run the provisioning step against the same database:
Terminal window bun run db:setup # MongoDBbun run db:setup:pg # PostgreSQL -
Apply any schema migrations the release declares:
Terminal window bun run db:migrateOne command for either datastore. It reports
currentand exits when there is nothing to do, which is how most upgrades go.--planlists 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.
-
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.
Why step 3 is not optional
Section titled “Why step 3 is not optional”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:setupis what applies it: a new reserved client, a new field on the administrator bucket, a group the ownership model now expects. The reservedadmin-mcpagent client arrived exactly this way. The script carries an explicit$addToSetfor it, because$setOnInsertwould 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.
Why step 4 is a separate step
Section titled “Why step 4 is a separate step”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/jwksserves 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 intobucketKeyswithout 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.
One upgrade signs your end users in again
Section titled “One upgrade signs your end users in again”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_uriat the instance’s/jwks. Now itsjwks_uriis 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/jwksby 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 --plannames it; take a backup beforedb: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:
-
Take a backup. There is no way back from either migration except a restore.
-
Run
db:setup(ordb: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 — anddb:migrateexpects them to exist. -
Run
db:migrate --plan, thendb: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:
rolesis gone from the admin API and from MCP. A script or agent that setroleson 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_grantandadmin_super_withdraw, and end users are grouped with thebucket_group_*tools. Relying parties that read roles from a claim read thegroupsclaim instead.- Refresh tokens are served only while
offline_accessis a supported scope. The token endpoint now accepts exactly the grant types discovery advertises, so withoffline_accessabsent from thescopessetting, a refresh, including one using a token issued before the upgrade, is refused withunsupported_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 ofamras “used a second factor” now gets that wrong. Look formfaorotpin the array instead. jwks_deleteis 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 thekid.
Then 0.9.0
Section titled “Then 0.9.0”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.
Then 0.10.0
Section titled “Then 0.10.0”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.
GETandPATCH /admin/api/admins/settingsand the MCP toolsadmin_settings_readandadmin_settings_updateare removed. Read and change the administrators’ bucket withbucket_getandbucket_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 malformedrichAuthorizationRequests.typesstops startup too, even while RAR is off, because the consent screen reads it.
Then 0.11.0
Section titled “Then 0.11.0”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/usersandbucket_user_listanswer{ 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: trueand the sameexpect. A script that did any of these in one request must now send the second. - Deleting a bucket schedules it.
DELETE /admin/api/buckets/:idswitches the bucket off and destroys it 30 days later;POST /admin/api/buckets/:id/purgedestroys a scheduled bucket at once, andbucket_restorebrings 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_namefrom a user’s claims document while the profile held another now receives the profile’s.
After the restart
Section titled “After the restart”curl -sf https://auth.example.com/readycurl -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.