Turnpike/docs/INSTALLATION.md
Pen Anderson 5d56ba8112 Created Turnpike, event attendee and volunteer management
Built after prototype, Traverse, an attendee and volunteer list
maintainer.
2026-03-03 13:05:50 -06:00

7.3 KiB

Turnpike Installation Guide

This guide covers building, deploying, and operating Turnpike. For usage and event workflows, see 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

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).

Configuration Reference

Flag Env var Default Description
--addr 0.0.0.0:8180 Listen address
--db turnpike.db SQLite database path
--secret TURNPIKE_SECRET auto-generated JWT signing secret
--token-expiry 24 JWT lifetime in hours
--base-url Public URL for volunteer token links
--smtp-host SMTP server hostname
--smtp-port 587 SMTP port (587 = STARTTLS, 465 = implicit TLS)
--smtp-user SMTP username
--smtp-password SMTP password
--smtp-from Sender email address
--smtp-from-name Sender display name
TURNPIKE_ADMIN_USER Bootstrap admin username (first run only)
TURNPIKE_ADMIN_PASSWORD Bootstrap admin password (first run only)

SMTP settings can also be configured at runtime through the Settings page (admin only). CLI flags override stored database values.

Running

# Minimal startup (creates admin user on first run)
TURNPIKE_ADMIN_USER=admin TURNPIKE_ADMIN_PASSWORD=changeme ./turnpike

# With explicit options
./turnpike \
  --addr 0.0.0.0:8180 \
  --db /var/lib/turnpike/turnpike.db \
  --secret your-jwt-secret \
  --base-url https://turnpike.example.com

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.

Systemd

[Unit]
Description=Turnpike event management
After=network.target

[Service]
ExecStart=/usr/local/bin/turnpike --db /var/lib/turnpike/turnpike.db --base-url https://turnpike.example.com
Environment=TURNPIKE_SECRET=your-secret-here
EnvironmentFile=/etc/turnpike/admin.env
StateDirectory=turnpike
DynamicUser=true
Restart=on-failure

[Install]
WantedBy=multi-user.target

The admin.env file should contain:

TURNPIKE_ADMIN_USER=admin
TURNPIKE_ADMIN_PASSWORD=your-password

Note: systemd uses EnvironmentFile (singular). The plural EnvironmentFiles is silently ignored.

Docker

A multi-stage Dockerfile is included in the repo (node build, Go build, scratch final image):

docker build -t turnpike .
docker run -p 8180:8180 \
  -v turnpike-data:/data \
  -e TURNPIKE_ADMIN_USER=admin \
  -e TURNPIKE_ADMIN_PASSWORD=changeme \
  -e TURNPIKE_SECRET=your-secret \
  turnpike --db /data/turnpike.db

NixOS

Turnpike builds with buildGoModule using the pure-Go SQLite driver (no CGO):

turnpike = pkgs.buildGoModule {
  pname = "turnpike";
  version = "0.1.0";
  src = ./path/to/turnpike;  # must include vendor/ and frontend/dist/
  vendorHash = null;
  env.CGO_ENABLED = 0;
};

The source directory must contain:

  • Go source files and vendor/ (run go mod vendor)
  • Pre-built frontend at frontend/dist/ (run cd frontend && npm run build)

A complete NixOS module example with DynamicUser, StateDirectory, and agenix secrets is in the project's homelab/turnpike.nix.

Reverse Proxy

Turnpike serves both the API and frontend on a single port. Behind a reverse proxy, ensure SSE connections for real-time sync are not buffered.

nginx

server {
    listen 443 ssl;
    server_name turnpike.example.com;

    location / {
        proxy_pass http://127.0.0.1:8180;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    location /api/sync/stream {
        proxy_pass http://127.0.0.1:8180;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_buffering off;
        proxy_cache off;
        proxy_read_timeout 86400s;
    }
}

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 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.