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:
- The API serves the web app and handles requests. It listens on port 4320.
- The worker does the work: runs, chats, document indexing, mailbox checks and nightly cleanup. You can run more than one.
Both talk to PostgreSQL, which also holds the job queue. If runs stay waiting and nothing moves, check that a worker is running.
Requirements
- Node 22 or newer. The
Dockerfileuses Node 26. - PostgreSQL with the
pgvectorextension.compose.yamluses thepgvector/pgvector:pg18image. - A database role that has SUPERUSER or BYPASSRLS, for migrations only.
pg_dump,pg_restore,psql,opensslandbashon the machine that runs backups.- A domain name and a reverse proxy that handles TLS. Production refuses to start unless
PUBLIC_URLuses https. - For containers: Docker with the Compose plugin, and ports 80 and 443 open.
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
| Setting | Meaning |
|---|---|
DATABASE_URL | The app's limited database role. Migrations create this role from the name and password you give here. |
DATABASE_ADMIN_URL | A 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_SECRET | At least 32 characters. Create one with openssl rand -base64 48. Protects sessions and two-step secrets. |
SECRETS_MASTER_KEY | 32 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_URL | The address people use. Must be https in production. |
BOOTSTRAP_ADMIN_EMAIL | Only this address can create the first account, and that person is the platform administrator. |
Optional
| Setting | Meaning |
|---|---|
NODE_ENV | Set to production on a real server. |
PLATFORM_HOST, PLATFORM_PORT | Where the API listens. Defaults are 127.0.0.1 and 4320. |
SMTP_URL, MAIL_FROM | The platform's own mail, used when none is saved in Settings. See Mail. |
ALLOWED_PRIVATE_HOSTS | Internal hosts that connectors and SSO may reach, comma-separated. Empty means none. |
AUDIT_LOG | A 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_DIR | Where uploaded files go. Default is data/workspaces. |
METRICS_TOKEN | Turns on /metrics. See Monitoring. |
ALERT_WEBHOOK_URL | Receives a short JSON message when a run fails. |
OTEL_EXPORTER_OTLP_ENDPOINT | Sends span measurements to an OpenTelemetry collector. |
MODEL_PRICES | Prices 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_PREVIOUS | The 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.
- Copy
.env.production.exampleto.env.productionand fill it in, includingDOMAINand the app's password insideDATABASE_URL. - Copy
.env.migrate.exampleto.env.migrateand setPOSTGRES_PASSWORD, using the same password insideDATABASE_ADMIN_URL. Only the database and the migration job see this file. The API and the worker never do. - Fix the network problem described below.
- Start it. The
--env-fileflag 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:
- creates or updates the limited app role from
DATABASE_URL, with no special powers, - applies each numbered file in
src/platform/migrations/once, in its own transaction, - sets up the job queue, and
- gives the limited role only the rights it needs. It can add to the audit log, but not change it.
It is safe to run again. Finished files are skipped.
First administrator
- Open
/sign-upand create an account with exactly theBOOTSTRAP_ADMIN_EMAILaddress. Passwords need at least 12 characters. - Set up two-step verification. The app sends you there, and nothing works until it is done.
- Create a workspace. On a self-hosted server, only this person can.
- 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
- Everyone who signs in with a password must use an authenticator app. The app shows a QR code and a set of backup codes when it is set up. Each code works once.
- "Remember this device" is refused, so every password sign-in asks for the second step.
- When someone turns it on, their other open sessions are ended.
- People who sign in through single sign-on use their identity provider's second step instead. See Single sign-on.
- It cannot be turned off, and its secret and backup codes cannot be read back or remade, so a stolen password cannot replace the second step.
- Someone who loses their authenticator signs in with a backup code and ticks I lost my authenticator. They then give their password and a second unused backup code, and set up a new app, which gives them new backup codes. Their other sessions end, and the audit log records
auth.two_factor_replaced. - There is no screen for resetting another person's two-step verification, so someone with no backup codes left cannot get back in by themselves. Tell people to keep their backup codes safe, and to replace their authenticator while they still have two.
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.
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:
- Upload size. Allow request bodies of at least 21 MB. A 15 MB file grows when sent as text.
- Live chat. Replies stream over a long-lived connection, so turn off response buffering for it. If streaming fails, chat still works, without the typing effect.
- Public routes. Outside services need to reach
/hooks/for webhooks and/channels/for WhatsApp. - Secure headers. With an https
PUBLIC_URL, the app sends HSTS and secure cookies by itself.
Backups and restore
Back up three things, and keep them apart:
- The database.
scripts/backup.shwrites an encrypted dump. Run it from cron. - Uploaded files. These live in
WORKSPACE_DATA_DIR, or theworkspace-filesvolume with Compose. The script does not cover them, and Hivelock does not encrypt them. - The keys.
SECRETS_MASTER_KEYandBACKUP_PASSPHRASE. Keep these away from the backups. Without the master key, secrets and run data cannot be read.
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
- Stop the app.
- Create an empty database. The limited app role must already exist on the server. The script refuses a database that has tables.
- Run the restore.
- Point
DATABASE_URLandDATABASE_ADMIN_URLat it, and start the app with theSECRETS_MASTER_KEYin 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:
- Writes an encrypted backup and checks it can be read.
- Runs the migrations.
- Runs
RESTART_COMMAND, if you set one. - Checks the app's
/readyaddress.
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.
- Take a backup. Keep the old key for as long as you keep backups made under it.
- Create a new key with
openssl rand -base64 32. - Put the new key in
SECRETS_MASTER_KEY, and the old key inSECRETS_MASTER_KEY_PREVIOUS. - Restart the API and every worker.
- Run
npm run keys:rotate. It needsDATABASE_ADMIN_URL. - 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. - Remove
SECRETS_MASTER_KEY_PREVIOUS, and restart again. - 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
| Tool | What it gives you |
|---|---|
/health | The API process is up. |
/ready | The API can also reach the database. |
/metrics | Off 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_URL | A JSON message when a run fails, with its type, workspace id, run id, step name and time. It holds no content. |
OTEL_EXPORTER_OTLP_ENDPOINT | Spans for each model and tool call: time, tokens, status and risk score. No prompts or results. |
| Logs | JSON 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 see | What 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 once | The proxy is probably not setting X-Real-IP. |
| Invitations are not emailed | Mail 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 progress | Check 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
- Run the Docker files for real, or settle your own deployment.
- Set up monitoring, alerts and off-site backups, and test a restore on the real server.
- Get signed data terms from any provider that will see real documents.
Checked against the code on 6 November 2026.