Self-hostable availability polls: propose dates, share a link, guests vote yes / if need be / no without an account, you finalize, and everyone gets the event in their calendar.
A single static Go binary. SQLite by default, PostgreSQL when you want it. No Node, no Redis, no external services.
- Create a poll in seconds, with or without an account — time slots (timezone-aware, DST-safe) or whole days
- Guest voting with a personal link to come back and change a vote; live tallies with the leading option highlighted
- Comments, hidden-participants mode, required-email mode
- Accounts via Google, GitHub, Microsoft, any OIDC provider, or email magic links; polls claimable from their admin link
- Spaces (organizations) with
owner/admin/memberroles, email invitations, ownership transfer, per-space retention and timezone - Finalize a date → participants receive the
.icsinvitation (METHOD:REQUEST); cancel → they receive theCANCEL; per-poll calendar feed and CSV export - Background job queue in SQLite with retries and a visible dead-letter
- English and French, automatic with a manual switch; dark mode
- Rate limiting, automatic purge of inactive polls (with a warning email), instance admin page, optional Prometheus metrics
Grab a release — a static binary for your platform from the releases page, or the container image:
docker run -p 8080:8080 -v quorum-data:/data ghcr.io/lporcheron/quorum:latest
docker compose up # or the example docker-compose.ymlFrom source, with Go ≥ 1.26:
make run # builds and starts on :8080
make dev # live-reload development loop
make test lint # what CI runs
make test-postgres # the same suite against PostgreSQLDeploying is copying one binary (or one ~19 MB container image) and mounting one directory for the SQLite file.
Releases are cut manually from the Release GitHub Action: it mints a
CalVer tag (YYYY.MM.DD.HHmmss), publishes cross-compiled binaries
with checksums, and pushes the linux/amd64 + linux/arm64 image to
GHCR. Nothing is published on push.
Everything is optional; defaults in parentheses.
| Variable | Purpose |
|---|---|
QUORUM_ADDR |
Listen address (:8080) |
QUORUM_BASE_URL |
Public URL of the instance (http://localhost:8080) |
QUORUM_DB_PATH |
SQLite database file (quorum.db; /data/quorum.db in Docker) |
QUORUM_LOG_LEVEL |
debug, info, warn, error (info) |
QUORUM_LOG_FORMAT |
json or text (json) |
| Variable | Purpose |
|---|---|
QUORUM_OAUTH_GOOGLE_CLIENT_ID / _CLIENT_SECRET |
Google sign-in |
QUORUM_OAUTH_GITHUB_CLIENT_ID / _CLIENT_SECRET |
GitHub sign-in |
QUORUM_OAUTH_MICROSOFT_CLIENT_ID / _CLIENT_SECRET |
Microsoft sign-in |
QUORUM_OAUTH_MICROSOFT_TENANT |
Entra tenant (common) |
QUORUM_OIDC_ISSUER_URL |
Generic OIDC discovery URL |
QUORUM_OAUTH_OIDC_CLIENT_ID / _CLIENT_SECRET |
Generic OIDC client |
QUORUM_OIDC_NAME |
Label on the OIDC login button (SSO) |
OAuth callback URLs are <base URL>/auth/<google|github|microsoft|oidc>/callback.
| Variable | Purpose |
|---|---|
QUORUM_SMTP_HOST |
SMTP server; setting it enables email |
QUORUM_SMTP_PORT |
SMTP port (587) |
QUORUM_SMTP_USERNAME / QUORUM_SMTP_PASSWORD |
SMTP credentials (optional) |
QUORUM_SMTP_FROM |
Sender address (required with SMTP) |
| Variable | Purpose |
|---|---|
QUORUM_ADMIN_EMAILS |
Comma-separated account emails granted the /admin page |
QUORUM_REGISTRATIONS_OPEN |
Allow new accounts (true); overridable at runtime from /admin; existing users always sign in |
QUORUM_EMAIL_ALLOWED_DOMAINS |
Comma-separated sign-up domain allowlist (empty = all) |
QUORUM_TRUST_PROXY |
Use X-Forwarded-For for rate limiting (false; enable only behind a proxy that sets it) |
QUORUM_METRICS |
Serve Prometheus metrics on /metrics (false) |
SQLite (the default) needs nothing. For bigger instances, point
DATABASE_URL at PostgreSQL (≥ 13) and the same binary uses it — one
schema, one query set, migrations applied at startup either way.
| Variable | Purpose |
|---|---|
DATABASE_URL |
postgres://user:pass@host/db switches the store to PostgreSQL (empty = SQLite at QUORUM_DB_PATH) |
SQLite (the default): the database runs in WAL mode, so never copy
quorum.db alone while the server runs — you would miss the WAL. Use
one of:
sqlite3 /data/quorum.db ".backup '/backups/quorum.db'" # safe online snapshotor Litestream for continuous replication to object storage. Restoring is copying the file back and starting the server.
PostgreSQL: standard tooling — pg_dump/pg_restore or your
provider's snapshots. Quorum keeps no state outside the database.