Upgrade from 0.13 to 0.14
0.14 is a large release. Redis is gone, the bundled object storage changed, and the first start runs around 140 database migrations. Read this page before you upgrade, and plan a short maintenance window.
There is no rolling upgrade and no downgrade. Migrations can't be reversed, so the only way back is restoring the backup you take in step 1. Never run 0.13 and 0.14 against the same database.
Before you start
Check that your setup meets the 0.14 requirements:
- PostgreSQL 14 or newer, with pgvector 0.5.0 or newer and the
pg_trgmextension available - The database user has the
TEMPORARYprivilege (PostgreSQL grants it by default) DATABASE_URLis a direct or session-mode connection, not a transaction-mode pooler (for example a pooler on port 6543). Realtime usesLISTEN/NOTIFY, which a transaction pooler drops.SECRET_KEYis set to the value your 0.13 install used, and is at least 32 characters
If something is missing, the migrator stops before changing anything and lists each problem with its fix.
Upgrade with Docker Compose
1. Stop the app and back up
With your 0.13 files still in place, stop the app so nothing writes. Don't use -v: the volumes must stay.
docker compose -f docker-compose.prod.yml stop appBack up your configuration, PostgreSQL, and object storage into a directory outside the repository. The bundled storage server changes in this release (see Bundled object storage is now Silo), so take an offline copy of the whole storage volume:
umask 077
BACKUP="../quackback-0.13-backup-$(date -u +%Y%m%dT%H%M%SZ)"
mkdir "$BACKUP"
cp .env "$BACKUP/environment.env"
cp docker-compose.prod.yml "$BACKUP/compose-before.yml"
# Save the current storage server image, needed for a rollback
docker image save "$(docker inspect quackback-minio --format '{{.Image}}')" \
> "$BACKUP/server-image.tar"
# Keep PostgreSQL running; stop writers first (stopping a stopped service is harmless)
docker compose -f docker-compose.prod.yml stop app minio
docker compose -f docker-compose.prod.yml exec -T postgres \
sh -c 'exec pg_dump -Fc -U "$POSTGRES_USER" "$POSTGRES_DB"' > "$BACKUP/database.dump"
docker cp quackback-minio:/data/. - > "$BACKUP/data.tar"
tar -tf "$BACKUP/data.tar" > /dev/nullKeep the backup private: the storage archive includes credentials.
Then stop the rest of the stack, still without -v:
docker compose -f docker-compose.prod.yml down2. Update the files
Pull the new source with git pull. If you don't deploy from a git checkout, download the new docker-compose.prod.yml, .env.prod.example, and the docker/postgres/ directory instead. Set QUACKBACK_TAG in .env to the 0.14 release, then make these .env changes.
Finish every .env change before you start 0.14. Some settings, such as the one-email-provider rule, are only checked after the migrations have committed, so a mistake there stops the app on an already-migrated database.
- If
.envhas aREDIS_URLline, delete it. Redis and Dragonfly are no longer used. The 0.13 compose file set it for you, so most installs don't have one. - If you set
MINIO_IMAGE_TAGorMC_IMAGE_TAG, remove them. They're ignored now. - Set
SECRET_KEYto the value your 0.13 install used. The compose file refuses to start without it. - Set
TRUSTED_PROXY_HOPS. Behind Nginx, Caddy, Traefik, or a Cloudflare Tunnel, set1. Behind a CDN plus a proxy, set2. If clients connect straight to the app port, keep0. If your proxy sets a single client-IP header such asX-Real-IP, you can setTRUSTED_CLIENT_IP_HEADERinstead. See Trusted proxy hops. - Keep only one email provider:
EMAIL_SMTP_HOST, theEMAIL_SES_*keys, orEMAIL_RESEND_API_KEY. Check this now, because the app stops after migrations if more than one is set. See Email.
3. Pull the new image
docker compose -f docker-compose.prod.yml pull4. Start once and wait
docker compose -f docker-compose.prod.yml up -d --remove-orphans
docker compose -f docker-compose.prod.yml logs -f appThe first start runs about 140 migrations in one transaction and logs each one as [n/total]. On a large database this can take several minutes.
Don't stop or restart the container while migrations run. An interrupted run rolls back completely and starts over on the next start.
After the migrations commit, the app builds its search indexes, then starts serving. Wait for the healthcheck (/api/health/ready) to pass.
In the background, files uploaded before 0.14 are copied into the new storage layout. The originals are kept, and links keep working while the copy runs.
5. Check and clean up
-
Sign in, open an existing post with an attachment, and upload a new file.
-
Remove the unused Dragonfly volume. The project name defaults to the directory name, so look it up first:
docker volume ls | grep dragonfly docker volume rm <project>_dragonfly_data -
Keep your backups until you're happy with the upgrade.
Upgrade other installs
The same order applies to every deployment: stop the old version, back up the database and object storage, change the configuration, start 0.14 once, and wait for migrations.
-
docker runor Kubernetes: removeREDIS_URLand your Redis or Dragonfly service, addTRUSTED_PROXY_HOPS, and run migrations once before starting replicas. With several replicas, setSKIP_MIGRATIONS=trueon all of them and run the migrate-only container first:docker run --rm -e DATABASE_URL="..." --entrypoint bun \ ghcr.io/quackbackio/quackback:<0.14 tag> /app/migrate.mjsThen make sure at least one replica runs with
QUACKBACK_ROLE=workerorall. Background jobs now run from a queue in PostgreSQL. See Scale with multiple replicas. -
Railway: delete the Redis service and the
REDIS_URLvariable, addTRUSTED_PROXY_HOPS=1(2if Railway's CDN is on), then deploy the 0.14 image. Give the health check a long timeout for the first start. -
Without Docker: stop the service, back up,
git pull,bun install,bun run db:migrate,bun run build, then start. Uninstall Redis or Dragonfly if nothing else uses it.
What changed for operators
Redis is gone
Background jobs, caches, rate limits, and realtime events now run on PostgreSQL. There is nothing to replace Redis with. Jobs run in the app's worker role (or the default all role). /api/health/ready now checks the database, migrations, and workers. /api/health and /api/health/live remain liveness checks.
Bundled object storage is now Silo
The Docker Compose stack runs PGSTY Silo, a maintained fork of MinIO, instead of MinIO. The minio service name, the minio_data volume, MINIO_ROOT_USER, MINIO_ROOT_PASSWORD, and the S3 endpoint stay the same, and Silo reads the existing data. Both images are pinned by digest. To use a mirror, set SILO_IMAGE and SILO_CLIENT_IMAGE to full image references.
If you use an external S3 provider, nothing changes here. S3_PUBLIC_URL no longer changes the URLs of new files: every file is served through /api/storage. Keep it set only if older content links to your public bucket or CDN URL.
Trusted proxy hops
Rate limits are per client IP. Behind a proxy, the app only sees the real IP if you tell it how many proxies to skip. Left at 0 behind a proxy, every visitor shares one rate-limit bucket and the app logs a warning. Never set it above 0 if clients can reach the app port directly, because anyone could then pick the IP the app sees. If your proxy sets one client-IP header (X-Real-IP from Nginx, CF-Connecting-IP from Cloudflare), you can set TRUSTED_CLIENT_IP_HEADER to its name instead. See Set the trusted proxy hops.
Configure one email provider
You can send through SMTP, Amazon SES (EMAIL_SES_ACCESS_KEY_ID, EMAIL_SES_SECRET_ACCESS_KEY, and EMAIL_SES_REGION, all required), or Resend (EMAIL_RESEND_API_KEY or RESEND_API_KEY). Configure exactly one. If more than one is set, the app refuses to start and names the variables.
The one exception: if you receive inbound email through Resend while SMTP or SES sends, keep the Resend key and set EMAIL_INBOUND_PROVIDER=resend. The key is then used only for receiving. See Email.
Telemetry
The anonymous daily snapshot now reports usage in bands (such as 11-50) and goes to the project's PostHog analytics. It never includes names, emails, URLs, hostnames, or content. Opt out with DISABLE_TELEMETRY=true. See Telemetry.
Renamed tables
If you run SQL reports against the database, update table names. The main renames:
| 0.13 | 0.14 |
|---|---|
votes | post_votes |
comments | post_comments |
tags | post_tags |
post_tags (post-to-tag join) | post_tag_assignments |
comment_reactions | post_comment_reactions |
merge_suggestions | post_merge_suggestions |
chat_messages | conversation_messages |
chat_tags | conversation_tags |
conversation_tags (conversation-to-tag join) | conversation_tag_assignments |
What changed for admins
Roles
Existing admins become Owners and existing members become Managers. Review roles in Admin → Settings → Members & Teams after the upgrade. See Roles and permissions.
Roadmaps follow statuses
Roadmaps are now built from post statuses, so you no longer place posts on a roadmap by hand. Old manual placements are kept in a post_roadmaps_archived table for reference until a later release. Nothing reads it. See Build a public roadmap.
Conversation status "pending" is now "snoozed"
Conversations that were pending are now snoozed. Conversations are open, snoozed, or closed.
SSO and OIDC redirect URIs
New sign-in providers use the redirect URI /api/auth/callback/<id>. Providers you added before 0.14, including the portal's custom OIDC provider, keep working with their original /api/auth/oauth2/callback/<id>, so nothing changes at your identity provider on upgrade. To move one to the new URI, register the new URI at your identity provider first, click Switch to new URI in the provider's Edit view in Quackback, then test the connection again. See Set up single sign-on.
Slack
The Slack app's request URLs changed to:
https://<your domain>/api/integrations/slack/hooks/eventshttps://<your domain>/api/integrations/slack/hooks/interactionshttps://<your domain>/api/integrations/slack/hooks/commandshttps://<your domain>/api/integrations/slack/hooks/options
Update them in your Slack app's settings, then reconnect Slack in Admin → Settings → Integrations to re-authorise it. See Slack.
Imports
The importers for other feedback tools were removed. CSV import remains in Admin → Settings → Imports & exports.
AI spam filter
With AI configured, Quackback can send new email and Messenger conversations to the model to file obvious spam. Workspaces upgrading from 0.13 start with this switched off; new workspaces start with it on. Turn it on with AI spam filter in Admin → Settings → Channels → Email. Filed spam is deleted after 30 days. Trusted senders and the sender checks that don't use AI work either way. See Set up the email channel.
Switched-off surfaces stay hidden
0.14 replaces the separate Help Center and Messenger on/off switches with the module toggles in Admin → Settings → General and the widget's Messages tab. If your Help Center or Messenger was switched off in 0.13, the upgrade keeps it off: the Help Center module or the Messages tab is turned off, so nothing goes public. Your articles and conversations stay. Turn them back on when you're ready.
What changed for developers
-
Widget identify needs a signed token.
identifyonly accepts a signedssoTokengenerated on your server. Unsigned{ id, email, name }payloads are rejected. See Identify users. -
Retired ID prefixes still work. Several ID prefixes were renamed. The API still accepts IDs you stored from 0.13 and returns them with the current prefix:
Old prefix Current prefix status_post_status_tag_post_tag_comment_post_comment_vote_post_vote_reaction_post_comment_reaction_comment_edit_post_comment_edit_note_post_note_activity_post_activity_merge_sug_post_merge_sug_linked_entity_post_external_link_chat_msg_conversation_msg_chat_tag_conversation_tag_chat_msg_mention_conversation_msg_mention_category_kb_category_kb_article_article_article_feedback_kb_article_feedback_Store the current form when you next write IDs back.
-
Roadmap
isPublicis deprecated.POSTandPATCH /api/v1/roadmapsstill accept it as an alias ofvisibility:truemeanspublicandfalsemeansteam. If you send both,visibilitywins. Usevisibility(public,team, orsegment) in new code. -
Permission checks follow roles.
/api/v1/apps/linkand/api/v1/apps/unlinkneed thepost.vote_on_behalfpermission (Owner, Admin, Manager, and Contributor have it), so sidebar apps installed with a Manager's key keep working./api/export(posts CSV) needspost.export(Owner, Admin, and Manager). See Roles and permissions. -
Webhooks and events cover more of the product. See Webhooks for the full catalogue.
If something goes wrong
- Migrations stop with a list of problems: fix each item (PostgreSQL version, pgvector,
pg_trgm,TEMPORARY) and start again. Nothing was changed. - A migration fails: the log names it, and the whole run is rolled back. Check the error, then start again. If you're stuck, restore the backup and run 0.13 while you ask for help on GitHub.
- The app exits naming email variables: you have more than one email provider configured. Keep one.
- Realtime updates don't arrive:
DATABASE_URLpoints at a transaction-mode pooler. Use a direct or session-mode connection.
Roll back
Migrations can't be undone, so rolling back means restoring the backup from step 1 into fresh volumes and running 0.13 again. This sequence deletes the current volumes, so run it only when you mean to roll back. Uploads and credential changes made after the backup are lost.
Replace 0.13.2 with the exact 0.13 release you ran. In a new shell, set BACKUP to your backup directory first, for example BACKUP=../quackback-0.13-backup-20261005T120000Z.
docker compose -f docker-compose.prod.yml down
docker volume ls | grep -E 'postgres_data|minio_data' # find <project>
docker volume rm <project>_postgres_data <project>_minio_data
cp "$BACKUP/compose-before.yml" docker-compose.prod.yml
cp "$BACKUP/environment.env" .env
# Pin the exact release you're returning to. A restored QUACKBACK_TAG=latest would
# start the 0.14 image you already pulled and migrate the database forward again.
sed -i 's/^QUACKBACK_TAG=.*/QUACKBACK_TAG=0.13.2/' .env
docker image load < "$BACKUP/server-image.tar"
docker compose -f docker-compose.prod.yml create
docker compose -f docker-compose.prod.yml up -d --wait postgres
# "already exists" errors for the vector and pg_cron extensions are harmless.
docker compose -f docker-compose.prod.yml exec -T postgres \
sh -c 'pg_restore --no-owner -h 127.0.0.1 -U "$POSTGRES_USER" -d "$POSTGRES_DB"' \
< "$BACKUP/database.dump"
docker cp - quackback-minio:/data < "$BACKUP/data.tar"
docker compose -f docker-compose.prod.yml up -dNever run the 0.13 image against a database 0.14 has migrated.
Next steps
- Deploy with Docker: The full Compose guide
- Environment Variables: Every configuration option
- Troubleshooting: Common issues and fixes