Self-hosted upgrades do not use the Formbricks Cloud bridge. Stop all writers, run the
release-matched preparation once, verify the result, and start v6. The bridge is a temporary Cloud
rollout artifact, not a product dependency or a step hidden inside the Helm chart.
What runs where
Deployment tooling owns maintenance, workload shutdown, infrastructure, and restart. The release image owns database migrations and authorization preparation. Neither the application nor the chart maintains an activation receipt, a migration authority switch, or a second authorization engine for this process.formbricks-authzed-prepare is a preparation command, not an unattended installer. It does not
discover or stop writers, suspend controllers, take backups, or restart workloads. Its confirmations are
operator acknowledgements, not proof that maintenance is active. Complete and verify those steps first.
The command reuses the image’s existing migration runner and formbricks-authzed upgrade prepare and
upgrade check implementations. It does not automatically prune unexplained relationships.
Before the maintenance window
- Rehearse with a restored copy of the installation. Measure migrations, backfill, verification, and restoration time; use those measurements to communicate the maintenance window.
- Pin the target Formbricks image by digest and the matching chart/Compose files. Preserve the current images, chart, values, secrets, and volume names. Do not combine this change with a PostgreSQL major upgrade, a storage migration, or unrelated Hub/Cube changes.
- Treat the Helm chart version and application image version as separate pins. A chart-only patch can
keep the same Formbricks
appVersion; verify both values and the rendered image before upgrading. - Prepare private SpiceDB connectivity, dedicated datastore credentials, its reviewed version, and completed datastore migrations. Retain existing operator ownership; never install a second operator over an existing cluster-wide operator. See SpiceDB operations.
Perform the upgrade
1
Enter and verify maintenance
Serve a human-readable maintenance page at the ingress/reverse proxy and stop accepting new work.
Do not use an application API’s JSON maintenance response as the public landing page. Suspend the
identified controllers and schedules, stop all writers, and wait for in-flight work and database
transactions to finish. Suspending a CronJob does not stop an already-running Job. Scaling a
Deployment to zero is insufficient if an autoscaler or GitOps controller can restore it.Verify there are no remaining writer pods, containers, Jobs, external scripts, or active writer
transactions. Keep only required infrastructure and the dedicated preparation process running.
Keep maintenance active through preparation, restart, and access verification.
2
Take a recoverable backup
Take consistent backups after writers stop, including the application database, existing SpiceDB
datastore, and any other datastore changed by the selected release. Retain secrets, configuration,
storage state, and exact source images. Confirm the restore procedure has been tested.Online backups taken earlier are useful but are not a substitute for a known recovery point that
includes the final writes accepted before maintenance.
3
Prepare using the target image
Run the command below using the exact target image and the same application database and AuthZed
configuration that v6 will use. It first validates configuration and authenticated SpiceDB health,
then runs application migrations, schema preparation, outbox drain, relationship repair, and a
separate full verification. Preparation reconciles the graph and requires a clean full dry-run
audit; the wrapper then performs a second independent full dry-run audit through A non-empty, different SpiceDB schema is not overwritten automatically. Review
upgrade check.
AUTHZED_CONSISTENCY must be fully_consistent. An existing .env that still sets
AUTHZED_CONSISTENCY=minimize_latency must be changed first: preparation and server startup both
reject it.schema check and
the release’s schema changes, then repeat with --expected-current-digest sha256:<reviewed-digest>.
Only one preparation/schema-writing process may run at a time.4
Deploy and test v6 while maintenance remains active
Only after preparation exits
0, deploy the same image digest. Retain fully_consistent and the
same datastore bindings. With Helm, set authzed.migrationAcknowledged=true; this is an explicit
operator acknowledgement, not a database activation record. The upgrade hook only verifies the
prepared graph; it does not apply schemas or backfill while old workloads are running.Verify the actual pod/container image IDs. Through restricted access, test an allowed user, a
denied user, a scoped API key, organization/workspace lists, and a grant revocation. Verify outbox
delivery, rerun formbricks-authzed upgrade check, and check operational-error metrics. Test a new
browser session, then hard-refresh an old tab: cached v5 assets or Server Action references are not
a valid v6 readiness signal. /health alone does not establish authorization readiness.5
Resume service
Restore controller and worker settings only after their desired manifests point to the tested v6
release. Remove maintenance after the checks pass. Monitor authorization errors, outbox age,
revocation delays, and scheduled relationship audits. Keep backups until upgrade acceptance ends.
Docker and one-click
Keep the same Compose project name, database images, and volumes. Prepare a target Compose file that retains customized SMTP, storage, networking, and Traefik settings. Pinformbricks,
formbricks-migrate, authzed-ops, and authzed-initialize to the same target image digest. Do not copy a
fresh-install Compose file over an existing installation without reviewing those differences.
After stopping the old project’s writers and verifying external writers/controllers are stopped,
start only the required infrastructure from the reviewed target configuration. Never use down -v.
With infrastructure already healthy, run the existing operations service without starting dependencies:
--project-name, --env-file, and -f options when customized. The operator service
must receive the application database URL, migration credentials when different, and the six AuthZed
client variables. The distributed default uses the bundled PostgreSQL database; customize it when your
installation uses a managed database. No database or SpiceDB port needs public exposure.
After successful preparation, a one-click installation also sets
FORMBRICKS_AUTHZED_V6_MIGRATION_ACKNOWLEDGED=true before its explicit v6 update. Do not invoke an
unattended update or move a floating image tag until the release-specific installation tests pass.
Helm and GitOps
Prepare SpiceDB before the application upgrade. Keep the current database/storage settings and existing Secrets. During maintenance, use an explicit, temporary Kubernetes Job with the target Formbricks image, the same database/AuthZed environment bindings, and this command:formbricks-maintenance-env Secret holds what preparation uses, with the values your release already uses:
DATABASE_URLAUTHZED_ENABLED=true,AUTHZED_ENDPOINT,AUTHZED_TOKEN,AUTHZED_SYSTEM_KEY,AUTHZED_INSECURE, andAUTHZED_CONSISTENCY=fully_consistentWEBAPP_URL, in every case. Preparation runs the database migrations, and the OAuth resource-link data migration (eng_2343_backfill_oauth_resource_links, first released in 5.4.0) derives each MCP OAuth client’s resource link from it. Whenever that migration runs on a database with MCP OAuth clients, as in any upgrade from before 5.4.0, preparation stops withupgrade_database_migration_failedwithout it.
DATABASE_URL or the AuthZed values, which the
chart injects separately. Prefer explicit valueFrom.secretKeyRef bindings to existing Secrets when possible.
Never put secrets on a command line or commit their values. If migrations use a different database credential,
bind MIGRATE_DATABASE_URL as well as DATABASE_URL; the target image’s migration runner uses it when set. If
the configuration is still incomplete, the Job reports upgrade_configuration_invalid with the names of the
variables to fix.
Wait for Job completion and inspect its sanitized result before running helm upgrade or resuming an
Argo CD/Flux reconciliation. Preparation is not a Helm migration hook or a permanent chart component.
Delete the Job and any temporary environment Secret after acceptance; retain only sanitized evidence.
There is no activation table to clean up.
Exit checklist
Keep maintenance active until all of these are true:formbricks-authzed-prepareexited0, including its two clean full audits, and the v6 migration acknowledgement is set only for this prepared upgrade.- The running image ID, chart values, schema, outbox, and a fresh
formbricks-authzed upgrade checkmatch the reviewed target. - Allowed and denied users, a scoped API key, organization/workspace lists, and a grant revocation work from a fresh browser session; an old tab recovers after a hard refresh.
Results and recovery
Success prints one JSON result and exits0:
2, without a restartRequired field. It never authorizes restarting workloads,
including when arguments were rejected before any work ran. The code identifies the failed step:
A blocked result can carry more fields, never values from your data:
"reason":"timeout"andtimeoutSeconds: the step ran out its time limit. It did not report a failure, so the code names the step, not a problem in it. Raise the limit and run preparation again.causeandretryable: the step’s own error code, such asauthzed_unavailable, and whether that error is transient. Retry a"retryable":trueresult once its cause, such as SpiceDB availability, has recovered.
FORMBRICKS_AUTHZED_PREPARE_STEP_TIMEOUT_SECONDS (60 to 86400) to change
it. The graph step rewrites every organization’s relationships and then audits them all, so its runtime grows
with the number of organizations: an instance with about 50,000 organizations needed about 35 minutes. Time the
rehearsal, and keep the Job’s activeDeadlineSeconds above the sum of its steps: a Job deadline stops the
container before the wrapper can print a result. Interrupted containers can exit with a signal code rather
than JSON; any nonzero exit remains blocked.
The command never restarts a workload on success, failure, or interruption. Retry preparation with the
same image after correcting the cause, provided the interrupted migration is safe to rerun. Never mark
a failed Prisma migration applied without investigating it.
Diagnose migration failures privately
The wrapper deliberately does not offer raw output in shared Job logs: migration errors can contain credentials, SQL, or customer identifiers. In a restricted PostgreSQL operator session, inspect failed Prisma migration records in_prisma_migrations (migration_name, started_at, finished_at,
rolled_back_at, and logs). Treat logs as sensitive; do not paste it into CI, tickets, or Slack.
For failures before Prisma records the attempt, check connectivity and permissions in that restricted
session. If a migration must be retried, first establish whether partial changes need repair, then use
the target image’s node /home/nextjs/packages/database/dist/scripts/apply-migrations.js under maintenance.
Capture any diagnostic output only in operator-controlled storage; never enable it on normal startup.
If a migration fails shows how to find the real error,
check what the failed migration left behind, and clear its failed record before retrying.
After database changes, helm rollback or changing the image is not necessarily a safe rollback.
Remain in maintenance and either complete the v6 preparation or restore the coordinated backup and
matching v5 images/configuration. Resuming writes before restoration risks losing those new writes.
Later v6 upgrades
The same read-onlyupgrade check gate also applies to v6.x updates. With an unchanged canonical
SpiceDB schema and a clean graph/outbox, no additional authorization preparation is needed. One-click
stops the stack before application migrations and starts it only after they succeed. Helm keeps its
existing application-migration lifecycle; follow the target release’s compatibility and maintenance notes.
If a release changes the canonical SpiceDB schema, this flow deliberately blocks the upgrade until
explicit maintenance preparation has completed with that target image and a reviewed current digest.
Use the procedure above again; the persistent acknowledgement is not permission to skip preparation.
An online, backward-compatible schema rollout would need its own reviewed and tested release procedure;
neither the Helm hook nor the one-click updater infers that a schema change is safe while writers run.
Survey visibility
The release that introduces survey visibility changes the canonical SpiceDB schema (it adds survey ownership and visibility), so it goes through the maintenance preparation above. Surveys are then projected into SpiceDB as their own scope. Nothing about who can see a survey changes until you opt in:Once the marker is set, every survey a person creates starts Restricted to its author and the
organization’s owners and managers. They can make it visible to the workspace from Collaborate or
when activating it. See Survey visibility.
-
Once every container or pod runs the new version, check durable delivery and replay any dead-lettered
survey events. The migration adds the survey trigger while the previous version may still be delivering
outbox events, and that version dead-letters the
surveyevent type it does not know. A dead-lettered survey revocation makes every enforced authorization check fail closed until it is replayed:replayresets every unresolved dead letter, so investigate any that are not survey events first. See AuthZed operations for how to list them. -
After the upgrade, repair the survey scope and mark it ready. This runs the repair, then two further dry
runs, and sets the marker only if both come back clean:
-
Confirm
formbricks-authzed upgrade checkreportsscopes.surveyas ready.
formbricks-authzed backfill --scope=survey --clear-ready. See
AuthZed operations for the audit
and repair commands.