A web application for running Swiss-system tournaments. Built with Go, PostgreSQL, and server-rendered HTML with htmx.
- Tournament management — Create and run Swiss-system tournaments with configurable points, rounds, and top cut
- Player registration — Preregistration with optional decklist submission
- Live standings — Real-time standings with tiebreakers (opponent match win %, game win %, opponent game win %)
- Playoff brackets — Top-cut single elimination playoffs
- OTR export — Export tournament results in Open Tournament Results v1 format
- REST API — Full API for programmatic tournament management
- Metrics — Built-in
/metricsendpoint with request counts, latency, status codes, and Go runtime stats (admin-only) - Mobile-friendly — Responsive design optimized for phone and tablet use
- Go 1.21+
- Docker (for running PostgreSQL)
# Clone the repository
git clone https://github.com/dstathis/openswiss.git
cd openswiss
# Start PostgreSQL in Docker
docker run -d --name openswiss-db \
-e POSTGRES_USER=openswiss \
-e POSTGRES_PASSWORD=openswiss \
-e POSTGRES_DB=openswiss \
-p 5432:5432 \
postgres:18
# Run the server
export DATABASE_URL="postgres://openswiss:openswiss@localhost:5432/openswiss?sslmode=disable"
go run ./cmd/openswissThe server starts on http://localhost:8080 by default.
Register an account through the web UI, then promote it to admin:
docker exec openswiss-db psql -U openswiss -c \
"UPDATE users SET roles = '{player,organizer,admin}' WHERE email = 'your@email.com';"All configuration is through environment variables:
| Variable | Default | Description |
|---|---|---|
DATABASE_URL |
postgres://localhost:5432/openswiss?sslmode=disable |
PostgreSQL connection string |
LISTEN_ADDR |
:8080 |
Address and port to listen on |
MIGRATIONS_PATH |
file://migrations |
Path to migration files |
RATE_LIMIT_PER_MIN |
60 |
API rate limit per IP per minute |
BASE_URL |
http://localhost:8080 |
Public base URL (used in password reset emails) |
SMTP_HOST |
(empty) | SMTP server hostname (enables password reset when set with SMTP_FROM) |
SMTP_PORT |
587 |
SMTP server port (587 for STARTTLS, 465 for implicit TLS) |
SMTP_USER |
(empty) | SMTP username (omit for unauthenticated relay) |
SMTP_PASSWORD |
(empty) | SMTP password |
SMTP_FROM |
(empty) | Sender email address for outgoing mail |
SECURE_COOKIES |
false |
Set to true to mark session cookies as Secure (requires HTTPS) |
cmd/openswiss/ # Application entry point
internal/
api/ # REST API handlers
auth/ # Password hashing, session/API key generation
db/ # Database access layer
engine/ # swisstools engine wrapper
export/ # OTR export
handlers/ # Web UI handlers
middleware/ # Auth, rate limiting middleware
models/ # Domain types
migrations/ # SQL migrations
templates/ # HTML templates
static/ # CSS and static assets
Run the unit tests (no external services required):
go test ./...The db and engine packages have integration tests that run against a real PostgreSQL database. These are gated behind a build tag and skipped by default.
The Makefile targets automatically create and tear down a test PostgreSQL container:
# Run integration tests
make test-integration
# Run 5000-player load test
make test-loadThe REST API is available under /api/v1/. Authenticate with a Bearer token (API keys can be created from the user dashboard or via the API).
See SPEC.md for the full API reference.
A Makefile provides shortcuts for common tasks. Run make help to see all targets.
Docker Compose sets up PostgreSQL, OpenSwiss, and Caddy (automatic HTTPS) together. The compose file pulls the pre-built image from Docker Hub, so no build step is needed on the server.
# Local development (self-signed TLS on localhost)
make dev
# Production — set your domain to get a real Let's Encrypt certificate
make deploy DOMAIN=tournaments.example.com
# Tail logs
make dev-logs # or: make deploy-logsThe DOMAIN environment variable controls the Caddy server name. When set to a
public domain, Caddy automatically obtains and renews TLS certificates from
Let's Encrypt. When omitted it defaults to localhost with a self-signed cert.
You can pin a specific image version with IMAGE_TAG:
make deploy DOMAIN=tournaments.example.com IMAGE_TAG=v1.2.0To pass additional OpenSwiss configuration (e.g. SMTP), copy the example environment file and edit it:
cp .env.example .envDocker Compose reads .env automatically. See .env.example for
all available options.
After startup, register an account and promote it to admin:
make promote-admin EMAIL=your@email.commake build # Build the Docker image (tagged dstathis/openswiss:latest)
make push # Build and push to Docker Hub
make push IMAGE_TAG=v1.2.0 # Build and push a specific tagBuild and run with Docker:
docker build -t openswiss .
docker run -d --name openswiss \
-e DATABASE_URL="postgres://openswiss:openswiss@db:5432/openswiss?sslmode=disable" \
-e SECURE_COOKIES=true \
-e BASE_URL="https://tournaments.example.com" \
-p 8080:8080 \
openswissIn production, run OpenSwiss behind a reverse proxy that handles TLS termination. Example nginx configuration:
server {
listen 443 ssl http2;
server_name tournaments.example.com;
ssl_certificate /etc/letsencrypt/live/tournaments.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/tournaments.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8080;
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;
}
}
server {
listen 80;
server_name tournaments.example.com;
return 301 https://$host$request_uri;
}When running behind a reverse proxy with TLS, set SECURE_COOKIES=true so session cookies are marked Secure.
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
See LICENSE for the full license text.