VoidTrack Docs

Self hosting

Docker Compose, Railway, environment variables, and verifying a deployment.

VoidTrack runs as three processes.

ServiceWhat it doesNotes
postgresThe databaseNeeds wal_level=logical for the sync engine
electricElectric sync engineStreams Postgres changes to browsers, using the electricsql/electric image
webSPA plus REST API, a single Node processBuilt from the repo Dockerfile

Only web needs a public domain. Postgres and Electric stay on the private network: clients never talk to Electric directly, they go through the app's authenticated shape proxy.

Electric is optional in the sense that the UI degrades to server rendered data if it is unavailable, but you lose instant navigation and live updates without it.

Anywhere with Docker Compose

git clone https://github.com/DevVoidHQ/voidtrack && cd voidtrack
cp .env.example apps/web/.env     # edit it
docker compose up -d

Compose already sets wal_level=logical and starts all three services. Then seed your first workspace, this prints an API key once:

pnpm seed -- --workspace "My Company" --email you@example.com --name "You" --team ACME

The app container pushes the database schema on boot. Set RUN_MIGRATIONS=false if you would rather manage migrations yourself.

One instance can host many workspaces: run pnpm seed once per company. API keys are workspace scoped.

Railway

brew install railway && railway login
railway init                       # create or choose the project
./scripts/railway-bootstrap.sh staging

That bootstrap script creates Postgres, Electric, and the web service with all the internal wiring set. Two steps cannot be automated.

1. Turn on logical replication. Electric will not boot without it.

railway connect postgres
ALTER SYSTEM SET wal_level = 'logical';
\q

Then restart the Postgres service from the dashboard: a config reload is not enough, the process has to restart. Verify it:

railway connect postgres
SHOW wal_level;      -- must print: logical

2. Deploy and seed.

railway up --service web

Seeding runs on your machine, so it needs the public database URL. The internal *.railway.internal host only resolves inside Railway:

cd apps/web
DATABASE_URL="$(railway variables --service postgres --json | jq -r .DATABASE_PUBLIC_URL)" \
  pnpm seed -- --workspace "My Company" --email you@example.com --name "You" --team ACME

If you created the Electric service before enabling wal_level=logical, it will have exited on startup with Electric requires wal_level >= logical. Redeploy it once Postgres is back up:

railway redeploy --service electric --yes

Generate a domain for the web service only, then set APP_URL to it so email links and webhooks resolve correctly.

Railway's generated *.up.railway.app names are sticky and can misstate the environment. Trust RAILWAY_PUBLIC_DOMAIN, not the name.

Environment variables

Required:

VariableNotes
DATABASE_URLPostgres connection string
ELECTRIC_URLInternal URL of the Electric service, for example http://electric.railway.internal:3000
ELECTRIC_SECRETSame value as Electric's own ELECTRIC_SECRET, the app adds it to every shape request
APP_URLPublic URL of this deployment, used in email links

Strongly recommended:

VariableWithout it
SMTP_URL, MAIL_FROMOTP codes and invite links only print to the server log
R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_BUCKETUploads go to container disk and are lost on redeploy, unless you mount a volume at UPLOAD_DIR

Optional:

VariablePurpose
GITHUB_WEBHOOK_SECRETRequired once you point a GitHub webhook at /api/webhooks/github
R2_PUBLIC_URLServe files straight from a public R2 bucket or CDN domain instead of proxying them through /api/files/:id. Saves egress and a latency hop, but the links are public and skip workspace and team checks
VOIDTRACK_FONT_CSS, VOIDTRACK_FONT_FAMILYCustom UI font, see Fonts
RUN_MIGRATIONSSet to false to disable schema push on boot
UPLOAD_DIRLocal upload path when R2 is not configured

Verifying a deployment

curl https://your-domain/api/health
# {"status":"ok","checks":{"database":"up","electric":"up"}}

A status of degraded with database: down means the app cannot reach Postgres. electric: unreachable means sync is off but the app still works. Check ELECTRIC_URL, and confirm Postgres really is on wal_level=logical.

Point the MCP server at the deployment with the API key from seeding:

pnpm --filter voidtrack-mcp build
VOIDTRACK_URL=https://your-domain VOIDTRACK_API_KEY=vt_... node packages/mcp/dist/index.js

Upgrading

git pull && docker compose up -d --build     # compose
railway up --service web                     # railway

The entrypoint pushes any schema changes before the app starts.

On this page