This guide covers building, deploying, and operating Turnpike. For usage and event workflows, see [USAGE.md](USAGE.md).
## Requirements
- **Go 1.24+** (for building)
- **Node.js 18+** (for building the frontend)
- No external database — Turnpike uses embedded SQLite via a pure-Go driver
## Building from Source
```sh
git clone <repo-url>
cd turnpike
make build
```
This runs `npm ci && npm run build` in `frontend/`, then `CGO_ENABLED=0 go build -o turnpike .`. The result is a single static binary with the frontend assets embedded. No runtime dependencies.
Other make targets: `make dev` (prints two-terminal dev instructions), `make clean` (removes binary and build artifacts).
The admin account is only created on first startup when no users exist and both `TURNPIKE_ADMIN_USER` and `TURNPIKE_ADMIN_PASSWORD` are set. After the first user is created, these environment variables have no effect.
### JWT Secret
If `--secret` / `TURNPIKE_SECRET` is not provided, Turnpike auto-generates a secret and stores it in the database. This persists across restarts as long as the database file is preserved. To use an explicit secret (recommended for production), pass it via flag or env var.
Changing or losing the JWT secret invalidates all active sessions — users will need to log in again.
The `/api/sync/stream` endpoint uses Server-Sent Events (SSE). The key settings are `proxy_buffering off` and a long `proxy_read_timeout` to keep the connection alive.
## TLS / HTTPS
Use a TLS termination proxy (nginx, Caddy, Traefik) with Let's Encrypt or your own certificates. Turnpike itself serves plain HTTP.
Set `--base-url https://turnpike.example.com` so that volunteer token links in exported CSVs and emails use the correct URL.
## Backup
Turnpike stores all data in a single SQLite file (the path passed to `--db`).
**Simple backup:** copy the `.db` file while the server is running — SQLite WAL mode ensures a consistent snapshot.
**Continuous replication:** [Litestream](https://litestream.io) streams WAL changes to S3, GCS, or Azure Blob Storage with near-zero overhead.
## Upgrading
1. Pull the latest source
2. Rebuild: `make build`
3. Replace the binary and restart the service
Database migrations run automatically on startup. All migrations are additive (`ALTER TABLE ... ADD COLUMN`) — no data is lost.
## Troubleshooting
**Login fails after restart:** If the JWT secret wasn't persisted (`--secret` or `TURNPIKE_SECRET`), a new secret is generated on restart, invalidating all existing tokens. Users need to log in again. To prevent this, always set an explicit secret in production.
**Admin account not created:** The bootstrap admin is only created on first startup when no users exist. If the service started without `TURNPIKE_ADMIN_USER` / `TURNPIKE_ADMIN_PASSWORD`, users were never created. Delete the database file and restart with the env vars set.
**Soft deletes:** Records are marked with `deleted_at`, not removed from the database. This is required for sync — clients need to see deletions to purge their local IndexedDB. Don't hard-delete records directly from the SQLite database.
**`EnvironmentFile` vs `EnvironmentFiles`:** systemd's directive is `EnvironmentFile` (singular). Using the plural form silently ignores the file — the service starts but environment variables are never set.
**SSE disconnections:** If real-time updates stop working behind a proxy, check that `proxy_buffering off` is set for `/api/sync/stream`. The client reconnects automatically, but buffering proxies can prevent events from reaching the browser.