Operate a self-hosted server
Upgrade a self-hosted install, back up its data, set up analytics users, and rotate its secrets.
A self-hosted install keeps its state in four services: PostgreSQL, ClickHouse, Redis and S3-compatible storage. This page covers upgrading it, backing it up, setting up its analytics users and rotating its secrets. To set one up, see Self-hosting.
Self-hosting isn't yet supported for production. There are no versioned server releases, no container images and no upgrade guarantees between commits.
Upgrade
The server has no release versions or changelog yet: you upgrade by moving to a newer commit of
main, which is also the only line that receives security fixes. The SDKs, server libraries and CLI
are versioned separately; see Releases.
Database migrations only move forward. There are no down migrations, and a migration can remove tables or columns the previous commit still uses. Upgrades therefore aren't zero-downtime, and going back means restoring a backup.
Back up PostgreSQL
Take a backup first; see Back up. It is your only way back.
Stop the API and workers
Stop every API and worker process, so no old code runs against the new schema.
Update the source
git pull
pnpm install --frozen-lockfileRebuild Studio as well: it reads AUTH_PROVIDER when it is built.
Apply the migrations
On an evaluation install, pnpm dev applies pending migrations every time it starts. Against any
other server, run:
pnpm db:migrate --forceIt applies each pending migration in its own transaction and records it in the
__alchemy_migrations table. It reads the connection from DATABASE_HOST, DATABASE_PORT,
DATABASE_NAME, DATABASE_USERNAME, DATABASE_PASSWORD and DATABASE_SSL, not from the
BACKEND_DATABASE_* settings the API uses, and refuses a non-local host without --force. Run it once, from one machine; it takes no lock.
Start the API and workers
ClickHouse needs no command: each API and worker process applies its pending ClickHouse migrations before its first analytics read or write. The tables for background jobs in PostgreSQL are also created at start.
GET /health on the API answers {"status":"ok"} once the process serves requests. It doesn't
check the database or other services.
Back up
| Store | What it holds | Back up |
|---|---|---|
| PostgreSQL | Projects, people, purchases, subscriptions, settings, encrypted credentials, sessions, and queued and scheduled work. | Yes. It is the system of record. |
| ClickHouse | Analytics events and ad delivery, plus copies of PostgreSQL tables that reports join with. | Yes. Events exist nowhere else. A daily check copies missing rows of the copied tables again. |
| S3-compatible storage | Uploaded files, project and paywall assets, compiled paywall builds and components. | Yes. There is no tool to rebuild them. Skip the uploads/ prefix, which expires in two days. |
| Redis | Rate limits, analytics query quotas and caches. | Optional. No purchase, person or workflow state lives there. |
Also keep ENCRYPTION_KEY with your backups, but stored apart from them. Without it, the store,
payment, push and webhook secrets in a PostgreSQL backup can't be read.
Use each store's own tools, such as pg_dump or volume snapshots for PostgreSQL and BACKUP for
ClickHouse. Voidhash has no backup command of its own.
Run Redis with append-only persistence and the noeviction policy. If you lose its data, stop the
API and workers, start Redis empty and start them again: rate limits and quotas start over, and
caches refill.
Restore
Restore PostgreSQL, ClickHouse and the bucket from backups taken at about the same time, with the
same commit of the source that wrote them, and the same ENCRYPTION_KEY.
Analytics users
Reports and VoidQL can read ClickHouse as two read-only users: a reader and an agent. Without them, reads run as the main user with the same per-query limits, but ClickHouse doesn't filter rows by project and doesn't apply the per-team VoidQL quota; the server logs a warning. VoidQL still limits every query to the project it runs in, since it adds that filter to each table it reads. To create them, run this from the checkout with your ClickHouse settings in the environment:
pnpm --filter @voidhash/backend-app analytics:clickhouse-accessIt prints the SQL to run as a ClickHouse administrator, or runs it with --apply. For a user
without a password in ANALYTICS_CLICKHOUSE_READER_* or ANALYTICS_CLICKHOUSE_AGENT_*, it
generates one and prints the values to add to your configuration. Add --quotas=false if your
administrator can't create quotas.
Rotate secrets
ENCRYPTION_KEY. Set the new key asENCRYPTION_KEY, and the old one inENCRYPTION_KEY_PREVIOUS(comma-separated if several). Restart the API and workers. Every 10 minutes a worker re-encrypts stored secrets with the new key and logs how many remain. When it logs thatENCRYPTION_KEY_PREVIOUScan be removed, remove it and restart again. A key set for the first time also encrypts secrets stored before as plain text.VOIDHASH_AUTH_SECRET. Used only withAUTH_PROVIDER=local. Changing it signs everyone out; there is no overlap period.
Next steps
Something wrong or missing on this page? Tell us by email or on Discord.