Self hosting
Docker Compose, Railway, environment variables, and verifying a deployment.
VoidTrack runs as three processes.
| Service | What it does | Notes |
|---|---|---|
| postgres | The database | Needs wal_level=logical for the sync engine |
| electric | Electric sync engine | Streams Postgres changes to browsers, using the electricsql/electric image |
| web | SPA plus REST API, a single Node process | Built 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 -dCompose 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 ACMEThe 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 stagingThat 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';
\qThen 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: logical2. Deploy and seed.
railway up --service webSeeding 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 ACMEIf 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 --yesGenerate 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:
| Variable | Notes |
|---|---|
DATABASE_URL | Postgres connection string |
ELECTRIC_URL | Internal URL of the Electric service, for example http://electric.railway.internal:3000 |
ELECTRIC_SECRET | Same value as Electric's own ELECTRIC_SECRET, the app adds it to every shape request |
APP_URL | Public URL of this deployment, used in email links |
Strongly recommended:
| Variable | Without it |
|---|---|
SMTP_URL, MAIL_FROM | OTP codes and invite links only print to the server log |
R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_BUCKET | Uploads go to container disk and are lost on redeploy, unless you mount a volume at UPLOAD_DIR |
Optional:
| Variable | Purpose |
|---|---|
GITHUB_WEBHOOK_SECRET | Required once you point a GitHub webhook at /api/webhooks/github |
R2_PUBLIC_URL | Serve 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_FAMILY | Custom UI font, see Fonts |
RUN_MIGRATIONS | Set to false to disable schema push on boot |
UPLOAD_DIR | Local 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.jsUpgrading
git pull && docker compose up -d --build # compose
railway up --service web # railwayThe entrypoint pushes any schema changes before the app starts.