📝(upgrade) complete the upgrade guide for yhub

The upgrade between version 5 and next version 6 is not simple. Lot of
breaking changes are made and must be documented to ease the upgrade for
the community.
This commit is contained in:
Manuel Raynaud
2026-08-17 17:04:13 +02:00
parent 2dfdae59b1
commit de5089c0b2
2 changed files with 215 additions and 17 deletions
+210 -16
View File
@@ -16,21 +16,160 @@ the following command inside your docker container:
## [Unreleased]
- The endpoint `/api/v1.0/documents/{document_id}/content/`, added in 5.0.0, is
removed, both its `GET` and its `PATCH`. The content of a document is now
saved and served by the collaboration server, the editor exchanging it over
the websocket, so nothing reads or writes it through the API anymore. If you
integrate with Docs, stop calling this endpoint: the `content_patch` and
`content_retrieve` abilities disappear from the document payload along with
it. `/api/v1.0/documents/{document_id}/formatted-content/` is not affected.
The `CONTENT_METADATA_CACHE_TIMEOUT` setting only tuned the cache of the
removed `GET` and is no longer read, you can drop it from your configuration.
- The JWKS of the resource server moved from `/api/{version}/jwks` to
`/external_api/{version}/jwks`, alongside the rest of the resource server
endpoints. `/api/{version}/jwks` now publishes the public key validating the
tokens Docs issues to call external services. If you enabled the resource
server (`OIDC_RESOURCE_SERVER_ENABLED`), update the JWKS URI declared to your
OIDC provider accordingly.
⚠️ This release replaces the collaboration server. The content of a document
does not live in the object storage anymore, it lives in that server, and the
`y-provider` that used to serve the websocket does not serve it. There is a new
service to deploy, a database to create for it, and the existing documents have
to be handed over to it: an instance that upgrades without doing so opens every
one of its documents **empty**. The entries below start with the steps of that
upgrade, in the order they are done, and end with the API changes.
- ⚠️ **A new service to deploy: the collaboration server**
(`lasuite/impress-yhub`, listening on `3002`), which replaces the
`y-provider` on everything under `/collaboration/`. It keeps the live state
of the documents in Redis/Valkey and persists them to a PostgreSQL database
of its own, so it needs both and there is nothing to default them to:
```yaml
REDIS: redis://{redis-host}:6379/0
POSTGRES: postgres://{user}:{password}@{postgres-host}:5432/yhub
```
It also needs `COLLABORATION_BACKEND_BASE_URL`, the backend it asks about
users and document access rights, and `COLLABORATION_SERVER_ORIGIN`, the
origins allowed to open a websocket — the two the `y-provider` already had.
Give it `Y_PROVIDER_API_KEY` as well, with the same value as the backend's:
it is the header that exempts the collaboration server from the API
throttling, and it calls the backend once per connection.
`REDIS_PREFIX` (default `yhub`) namespaces its keys when the Redis instance
is shared with something else, and `YHUB_ORG` (default `docs`) names the
organization the documents live under — the backend and the server must
agree on it, a room of any other organization is refused. In the helm chart
everything is under the `yhub` values key, enabled by default, and
`yhub.worker.enabled` splits it into a server deployment and a worker
deployment that scale on their own. `src/yhub-server/README.md` documents the
rest of what it reads.
- ⚠️ **Its schema is not created when it starts.** The server never runs DDL:
run the script it ships, `npm run init-db` (`node
node_modules/@y/hub/bin/init-db.js` in the image), once before starting it
and again after every upgrade that adds a table. It creates the database when
it is missing, and it is idempotent, so re-running it is always safe. The
helm chart runs it as a job (`yhub.initDb`, on by default), next to the
backend migrate job and retrying while the PostgreSQL server does not answer
— nothing in the chart creates that server. Until it has run, every document
read fails with a `relation "..." does not exist` error.
- ⚠️ **The existing documents have to be migrated into it.** Until now the
content of a document was a file in the media bucket, at key
`{document-id}/file`; the collaboration server starts out knowing none of
them, and the frontend no longer seeds content of its own. Two steps, in this
order:
1. Turn `SOFT_MIGRATION=true` on the collaboration server **before letting
anyone in**, and point it at the media bucket:
`LEGACY_S3_ENDPOINT_URL` (no path), `LEGACY_S3_ACCESS_KEY_ID`,
`LEGACY_S3_SECRET_ACCESS_KEY` (both with a `_FILE` variant),
`LEGACY_S3_BUCKET_NAME` (defaults to `impress-media-storage`, the
development name — production has to set it), and optionally
`LEGACY_S3_REGION_NAME` and `LEGACY_S3_SIGNATURE_VERSION` (`s3v4` by
default, `v4` for the providers wanting the other one; SigV2 is not
available). Read-only, bucket-scoped credentials are enough, and on AWS
they need `s3:ListBucket` beside `s3:GetObject` — without it a
brand-new document reads as `403` instead of `404` and fails to open. A
document is then seeded from its legacy snapshot the first time someone
opens it. These are **not** the backend's `AWS_S3_*` settings: nothing
here reads them, so a pod carrying both migrates out of the bucket named
here and no other.
2. Then backfill the corpus, which the lazy seeding never finishes on its own
— a document nobody opens stays in S3 forever. `python manage.py
migrate_documents` hands every document to the collaboration server, which
replays its **full** S3 version history rather than its last snapshot, so
`/documents/{id}/versions/` and the history the editor shows agree. It is
bounded (`--concurrency`, `--rate`, `--limit`, `--created-before`),
resumable and safe to re-run: what became of every document is recorded in
the new `impress_document_migration` table, a document the server refused
is left for a later `--retry-failed` run, and `--document-id` hands over a
single one. `--dry-run` counts what a run would do.
`SOFT_MIGRATION` may only be turned off once that backfill has covered the
corpus. Turning it off earlier loses nothing — what is migrated stays
migrated — but an unmigrated document then opens as an *empty* room over
content that is alive in S3.
Keep the media bucket, its objects and its versioning either way:
`/documents/{id}/versions/` still serves the version history from there, and
the full migration replays it.
- ⚠️ **The websocket url changed**, and so does what `/collaboration/` is
routed to. The room is appended by the client, and the last segment of the
base url is `YHUB_ORG`:
```yaml
COLLABORATION_WS_URL: wss://{yourdocsdomain.tld}/collaboration/ws/v1/docs
```
Both `/collaboration/` ingresses now point at the collaboration server. In
the chart, `ingressCollaborationApi.path` (a single path) becomes
`ingressCollaborationApi.paths` (a list, one ingress rule each), defaulting
to `/collaboration/ydoc/` and `/collaboration/jwks/`. What is not listed
stays in-cluster, which is how `create-ydoc`, `reset-connections`, `migrate`,
`restore-ydoc` and `reset-ydoc` are kept unreachable: they are
backend-internal, and publishing them would put document deletion and the
legacy migration one request away from the internet. If you route
`/collaboration/` by hand, publish the websocket, the browser-facing document
routes (`ydoc`, `rollback`, `prune`, `changeset`, `activity`) and `jwks`, and
keep those five in-cluster.
The `nginx.ingress.kubernetes.io/upstream-hash-by: $arg_room` annotation is
dropped from the websocket ingress, and should be dropped from yours: the
replicas exchange updates through Redis, so a room no longer needs a sticky
upstream, and the new urls carry no `room` query argument — hashing on it
would pin every connection to a single pod.
- ⚠️ **The backend has to reach the collaboration server.**
`YHUB_API_BASE_URL` is now required — creating a document, duplicating one,
`formatted-content`, the search indexation and the deletions all go through
it, and it is also where the backend reads the JWKS verifying the calls it
receives:
```yaml
YHUB_API_BASE_URL: http://{yhub-service}:443
```
Prefer the internal service url, the routes the backend calls are not meant
to be reachable from the outside. `YHUB_API_TIMEOUT` (30 seconds) and
`YHUB_MIGRATION_TIMEOUT` (600 seconds, the replay of one document's whole
history) bound those calls.
`COLLABORATION_API_URL` is not read by the application anymore — it
configured the safeguard removed below — and only the integration test suite
still looks at it.
- ⚠️ **The backend needs an RSA private key of its own**: `JWT_PRIVATE_KEY`, or
a file `JWT_PRIVATE_KEY_FILE` points at, which is easier since a PEM does not
fit well in an environment variable:
```bash
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out private.pem
```
It signs the short-lived tokens the backend presents to the collaboration
server and to the conversion service, which verify them against the public
half it publishes on `/api/v1.0/jwks` — so no secret is shared with either,
and each token carries an `aud` claim naming the service it was issued for,
so one cannot be replayed against the other. Without the key the backend
cannot call either service at all. `JWT_TOKEN_LIFETIME` (3600 seconds) is
both the `exp` horizon and how long an issued token is cached.
The `y-provider` verifies the same way and only needs to reach the backend
for it: `COLLABORATION_BACKEND_BASE_URL`, from which it derives
`{base}/api/v1.0/jwks`, or `JWKS_URL` when that url is not the right one from
where it runs. In a development environment, `make generate-secret-keys`
creates the key in `data/jwt/`; on a cluster, `jwtKeys.enabled` makes the
chart generate both keys in a secret the services mount read-only.
- ⚠️ The collaboration server now calls the backend on its own, to declare that
a document was edited, and signs those calls: **it needs an RSA private key
of its own**, which it had not before. Generate one and give it to the
@@ -54,7 +193,62 @@ the following command inside your docker container:
drift out of date. In a development environment,
`make generate-secret-keys` creates the key in `data/jwt/`.
### [5.0.0] - 2026-04-30
- ⚠️ **`COLLABORATION_SERVER_SECRET` is gone**, on both sides: remove it from
the backend and from the collaboration server, which authenticate each other
with the signed tokens above. The safeguard it served — "while someone is
connected to the websocket, the users who are not are read-only" — is gone
with it, and the question it answered no longer arises: the content is saved
through the websocket only, so an editor that cannot open one saves nothing
instead of overwriting what the others wrote. Consequently
`COLLABORATION_WS_NOT_CONNECTED_READ_ONLY` (and its misspelled
`COLLABORATION_WS_NOT_CONNECTED_READY_ONLY` alias) and
`NO_WEBSOCKET_CACHE_TIMEOUT` are no longer read, the write-only `websocket`
field disappears from `PATCH /api/v1.0/documents/{document_id}/`, and
`/api/v1.0/documents/{document_id}/can-edit/` is removed along with the
`can_edit` ability in the document payload. The `get-connections` API of the
`y-provider` is dropped for good with them, its only consumer was that
mechanism.
- ⚠️ **The `y-provider` is the conversion service and nothing else** — its
published image no longer serves `/collaboration/ws/`. In the chart,
`yProvider.converter`, its deployment and its service are dropped: the
`yProvider` release *is* the converter, so a deployment that had
`yProvider.converter.enabled: true` loses the `-converter` suffix on the url
the backend calls, and `yProvider.converter.*` values are now ignored, their
`yProvider.*` counterparts taking over:
```yaml
# before
Y_PROVIDER_API_BASE_URL: http://impress-docs-y-provider-converter:443/api/
# now
Y_PROVIDER_API_BASE_URL: http://impress-docs-y-provider:443/api/
```
- The collaboration server can store the document blobs in a bucket instead of
its own PostgreSQL database (`YHUB_S3_PERSISTENCE=true`, plus the
`YHUB_S3_*` settings). It is off by default and nothing about this upgrade
needs it. Read the "Document storage" section of `src/yhub-server/README.md`
before enabling it: a document persisted that way cannot be read back once
the setting is removed, and it is a third bucket, not the backend's
`AWS_S3_*` nor the legacy one the migration reads.
- The endpoint `/api/v1.0/documents/{document_id}/content/`, added in 5.0.0, is
removed, both its `GET` and its `PATCH`. The content of a document is now
saved and served by the collaboration server, the editor exchanging it over
the websocket, so nothing reads or writes it through the API anymore. If you
integrate with Docs, stop calling this endpoint: the `content_patch` and
`content_retrieve` abilities disappear from the document payload along with
it. `/api/v1.0/documents/{document_id}/formatted-content/` is not affected.
The `CONTENT_METADATA_CACHE_TIMEOUT` setting only tuned the cache of the
removed `GET` and is no longer read, you can drop it from your configuration.
- The JWKS of the resource server moved from `/api/{version}/jwks` to
`/external_api/{version}/jwks`, alongside the rest of the resource server
endpoints. `/api/{version}/jwks` now publishes the public key validating the
tokens Docs issues to call external services. If you enabled the resource
server (`OIDC_RESOURCE_SERVER_ENABLED`), update the JWKS URI declared to your
OIDC provider accordingly.
## [5.0.0] - 2026-04-30
We made several changes around document content management leading to several breaking changes in the API.
+5 -1
View File
@@ -908,7 +908,11 @@ jwtKeys:
## live state of a document in redis/valkey and persists it to its own
## PostgreSQL database, so it needs both — set `yhub.envVars.REDIS` and
## `yhub.envVars.POSTGRES`, there is nothing sensible to default them to.
## Disabling it sends the /collaboration/ ingresses back to the y-provider.
## Disabling it drops its deployment, its service and its init-db job, and
## nothing takes that role over: the two /collaboration/ ingresses still name
## its service, and the y-provider does not serve the websocket anymore. Turn
## it off only to run the collaboration server outside of this release, and
## turn `ingressCollaborationWS` and `ingressCollaborationApi` off with it.
##
## Two buckets can be added to that list, each with a prefix of its own so that
## none of them is ambiguous — they may sit on different providers, with