Hivelock Get early access

For the person who runs the server

Self-hosted setup

This guide takes you from an empty server to a working Hivelock, then covers looking after it.

Before you start

The Community edition is open source under AGPL-3.0. Get the code on GitHub.

Hivelock has two main processes, and both must run:

Both talk to PostgreSQL, which also holds the job queue. If runs stay waiting and nothing moves, check that a worker is running.

Requirements

There is no sizing guidance yet. scripts/load-test.ts measures throughput on a throwaway database, if you want your own figures.

Environment settings

The app checks its settings when it starts, and refuses unsafe ones. Errors name the setting, never its value. .env.example and .env.production.example list them with comments.

Required

SettingMeaning
DATABASE_URLThe app's limited database role. Migrations create this role from the name and password you give here.
DATABASE_ADMIN_URLA role with SUPERUSER or BYPASSRLS. Used by migrations, backups and key rotation. The app and worker start without it, and should not be given it.
BETTER_AUTH_SECRETAt least 32 characters. Create one with openssl rand -base64 48. Protects sessions and two-step secrets.
SECRETS_MASTER_KEY32 bytes in base64. Create one with openssl rand -base64 32. Without it, stored secrets and run data cannot be read. Back it up apart from the database.
PUBLIC_URLThe address people use. Must be https in production.
BOOTSTRAP_ADMIN_EMAILOnly this address can create the first account, and that person is the platform administrator.

Optional

SettingMeaning
NODE_ENVSet to production on a real server.
PLATFORM_HOST, PLATFORM_PORTWhere the API listens. Defaults are 127.0.0.1 and 4320.
SMTP_URL, MAIL_FROMThe platform's own mail, used when none is saved in Settings. See Mail.
ALLOWED_PRIVATE_HOSTSInternal hosts that connectors and SSO may reach, comma-separated. Empty means none.
AUDIT_LOGA local log file used by the older local app. Keep it off on a server, as .env.production.example does. It can hold prompts in plain text. The worker turns it off unless you set it.
WORKSPACE_DATA_DIRWhere uploaded files go. Default is data/workspaces.
METRICS_TOKENTurns on /metrics. See Monitoring.
ALERT_WEBHOOK_URLReceives a short JSON message when a run fails.
OTEL_EXPORTER_OTLP_ENDPOINTSends span measurements to an OpenTelemetry collector.
MODEL_PRICESPrices per million tokens as JSON, for cost figures. Prices set on a provider in Settings take priority.
LLM_*, KIMI_*Server default models, used only by a workspace with no providers of its own.
SECRETS_MASTER_KEY_PREVIOUSThe old key, only during a key rotation.
HOSTED_MODE and STRIPE_*The multi-tenant cloud version. Leave these out for a normal install. See Plans and billing.

Run with Node

From the project folder:

npm install
cp .env.example .env        # then fill it in
npm run db:migrate
npm run web:build

For development, npm run platform and npm run worker start both processes and restart them on changes. On a real server, use the same commands the Dockerfile uses, and keep them running with your service manager:

node --import tsx src/platform/server.ts
node --import tsx src/platform/worker.ts

Then open your PUBLIC_URL, or http://localhost:4320 in development.

Run with Docker Compose

compose.yaml starts PostgreSQL, a one-time migration job, the API, one worker and Caddy. Only Caddy is meant to be reachable from outside.

  1. Copy .env.production.example to .env.production and fill it in, including DOMAIN and the app's password inside DATABASE_URL.
  2. Copy .env.migrate.example to .env.migrate and set POSTGRES_PASSWORD, using the same password inside DATABASE_ADMIN_URL. Only the database and the migration job see this file. The API and the worker never do.
  3. Fix the network problem described below.
  4. Start it. The --env-file flag matters, as explained below.
docker compose --env-file .env.production --env-file .env.migrate up -d --build

To do more work at once, raise deploy.replicas on worker. Workers share the queue safely.

Docker notes

Inside a container, localhost means the container itself, so use a name the container can reach.

Migrations

npm run db:migrate prepares the database. With Compose, the migrate service does this before the API starts. It:

It is safe to run again. Finished files are skipped.

First administrator

  1. Open /sign-up and create an account with exactly the BOOTSTRAP_ADMIN_EMAIL address. Passwords need at least 12 characters.
  2. Set up two-step verification. The app sends you there, and nothing works until it is done.
  3. Create a workspace. On a self-hosted server, only this person can.
  4. Under Settings, set up mail and invite people. An invitation lasts 48 hours.

Sign-up is closed to everyone else. Each new person needs a real invitation link, and invitation emails need mail to be set up. If mail is not set up, the page shows the link so you can send it yourself.

Two-step verification

Mail

The platform's own mail sends invitations, address confirmations and password resets. It is separate from any email connection in a workspace.

The platform administrator sets it up under Settings, then Mail. SMTP_URL and MAIL_FROM are the fallback. Saved settings apply to the next message.

Send a test email before you restart.

Email confirmation turns on at the first start after mail is set up. After that restart, an account with an unconfirmed address needs a confirmation link to sign in, and that includes the first administrator's. If mail then breaks and you are locked out, an operator can confirm the account in the database:

update "user" set "emailVerified" = true where email = '...';

Reverse proxy and TLS

Run Hivelock behind a reverse proxy that handles TLS. The proxy must do one more job: set X-Real-IP to the real client address, replacing anything the client sent. Rate limits and audit entries depend on it. Without it, everyone counts as one address, or a client can fake theirs.

With Compose, Caddy does this. It gets certificates for DOMAIN by itself:

{$DOMAIN} {
	encode gzip
	reverse_proxy api:4320 {
		header_up X-Real-IP {remote_host}
	}
}

Whichever proxy you use, check these:

Backups and restore

Back up three things, and keep them apart:

DATABASE_ADMIN_URL=... BACKUP_PASSPHRASE=... scripts/backup.sh

Under Docker Compose the database has no open port, so run pg_dump inside its container:

PG_DUMP_COMMAND="docker compose --env-file .env.production --env-file .env.migrate exec -T database pg_dump --format=custom -U agent_platform_admin agent_platform" BACKUP_PASSPHRASE=... scripts/backup.sh

It keeps the newest 14 dumps in backups. Change this with BACKUP_KEEP and BACKUP_DIR. Store copies off the server too. That part is up to you.

Restore

  1. Stop the app.
  2. Create an empty database. The limited app role must already exist on the server. The script refuses a database that has tables.
  3. Run the restore.
  4. Point DATABASE_URL and DATABASE_ADMIN_URL at it, and start the app with the SECRETS_MASTER_KEY in use when the backup was taken.
BACKUP_PASSPHRASE=... RESTORE_DATABASE_URL=<empty database URL> scripts/restore.sh <backup file>

A backup you have never restored is only a hope. Test a restore on the real server before you rely on it.

Upgrades

Run this from the new version's folder, with the app's settings in the environment:

BACKUP_PASSPHRASE=... scripts/upgrade.sh

It stops at the first failure, and does these in order:

  1. Writes an encrypted backup and checks it can be read.
  2. Runs the migrations.
  3. Runs RESTART_COMMAND, if you set one.
  4. Checks the app's /ready address.

If a step after the backup fails, it prints how to restore. /ready only shows that the app is up and can reach the database, so also check the app yourself. To go back, restore the backup into an empty database with the previous version's code. Anything written after the backup is lost.

With containers, build the new images first, then set these.

MIGRATE_COMMAND="docker compose --env-file .env.production --env-file .env.migrate run --rm migrate" \
RESTART_COMMAND="docker compose --env-file .env.production --env-file .env.migrate up -d api worker" \
READY_URL="https://your-domain/ready" \
BACKUP_PASSPHRASE=... scripts/upgrade.sh

Key rotation

This moves stored secrets and sealed data to a new SECRETS_MASTER_KEY. The app keeps running while it happens.

  1. Take a backup. Keep the old key for as long as you keep backups made under it.
  2. Create a new key with openssl rand -base64 32.
  3. Put the new key in SECRETS_MASTER_KEY, and the old key in SECRETS_MASTER_KEY_PREVIOUS.
  4. Restart the API and every worker.
  5. Run npm run keys:rotate. It needs DATABASE_ADMIN_URL.
  6. Check that it ends with Verified: every value opens with the current key alone. If it does not, keep the previous key and find out why.
  7. Remove SECRETS_MASTER_KEY_PREVIOUS, and restart again.
  8. Take a fresh backup.

It can be stopped and run again safely, and it never prints keys or values. It does not cover BETTER_AUTH_SECRET, the search word index, the audit log or uploaded files. If you think the key was stolen, also replace the credentials stored in the platform, because anyone with an old backup and the old key can still read that backup.

Monitoring

ToolWhat it gives you
/healthThe API process is up.
/readyThe API can also reach the database.
/metricsOff until you set METRICS_TOKEN. Then it needs that token as a bearer token. Prometheus text: workspaces, active runs, runs and model calls in the last 24 hours, errors, tokens, waiting approvals and HTTP status counts.
ALERT_WEBHOOK_URLA JSON message when a run fails, with its type, workspace id, run id, step name and time. It holds no content.
OTEL_EXPORTER_OTLP_ENDPOINTSpans for each model and tool call: time, tokens, status and risk score. No prompts or results.
LogsJSON lines on standard output: method, path, status and time. Request bodies and headers are left out.

Nothing watches the worker for you. Watch for runs_active staying high, and use your service manager to restart a stopped worker. Failed-run alerts only fire while a worker is running.

Troubleshooting

You seeWhat to do
"Platform settings are invalid."The message names the setting. Check its length and format.
"PUBLIC_URL must use https in production."Use your https address, or leave NODE_ENV unset for local trials.
"DATABASE_ADMIN_URL needs a role with SUPERUSER or BYPASSRLS."Migrations need more rights than the role you gave.
"Sign-up is by invitation only."The address differs from BOOTSTRAP_ADMIN_EMAIL, an account already exists, or the invitation has expired.
"Turn on two-step verification first."Finish the setup at /setup-2fa.
Everyone is rate limited at onceThe proxy is probably not setting X-Real-IP.
Invitations are not emailedMail is not set up. The log shows mail_not_in_environment. Send the link by hand, or set up mail.
"Connections to ... are not allowed."The address is private. For a connector, add the host to ALLOWED_PRIVATE_HOSTS. AI models on a self-hosted server need no setting, but cloud metadata addresses are always refused.
"Confidential mode is on, but no AI provider is cleared for confidential work."Clear a provider in Settings, or turn confidential mode off. See AI models.
"Confidential mode needs a Kimi key, because NVIDIA models are not allowed."The workspace is using the server's default models. Add a provider in Settings instead.
"This workspace has used its monthly token allowance."Raise the limit under Settings, Usage and limits, or wait for the next month.
Runs stay in progressCheck that a worker is running and can reach the database.
"This action may already have happened, so it was not repeated."A step that changes data stopped without a clear result. Check the other system before you run it again.
"Hosted mode cannot start."The message lists each problem. See Plans and billing.

Before you rely on it

Checked against the code on 6 November 2026.