Postfacto (with a P) was a self-hosted retro tool aimed at helping teams run great retrospectives, remotely or in presence. This project aims to revive it, but in Rust.
Rostfacto (with an R) keeps the retro flow that Postfacto (with a P) made popular — cards, highlighting, timers, action items — and rebuilds it from scratch:
- It's actively maintained. Postfacto is archived; Rostfacto is developed in the open, released on a regular basis, and covered by an end-to-end test suite.
- One binary, one dependency. Rostfacto is written in Rust and ships as a single static binary — all you need besides it is PostgreSQL. No Ruby on Rails, no Node.js build step, no Redis.
- Sign in with GitHub, access via teams. Instead of per-retro passwords and an admin dashboard, sign-in uses GitHub (or GitHub Enterprise), admins are a GitHub team, and every retro belongs to a team whose members can see and edit it.
- Real-time sync you can trust. Every change lands in a durable event log in Postgres and is pushed to all connected clients over server-sent events; a client that reconnects replays everything it missed.
- See who's in the room. A live participants panel shows everyone currently on the board; presence lives in Postgres, so it stays correct even when Rostfacto runs as several processes behind a load balancer and never flickers on reconnects.
- A timer everyone agrees on. The five-minute highlight countdown is server-authoritative: the deadline lives in the database, so every participant sees the same time, and it can be extended by two minutes.
- No analytics, no tracking. Rostfacto ships without analytics or tracking scripts — your retrospectives stay on your server.
brew install postgresql@18
brew services start postgresql@18
# Create a dedicated database role for the app
createuser --createdb rostfacto
createdb -O rostfacto rostfacto-dev
export DATABASE_URL=postgres://rostfacto@localhost/rostfacto-dev
cargo install sqlx-cli
sqlx migrate run
# Without the GitHub variables below the app refuses to start: run unsecured
# local development with DEMO_MODE=1 or configure GITHUB_ADMIN_ORG etc.
export DEMO_MODE=1
cargo watch -x runQuickest way to run the whole stack (database, migrations, app) — no local build required:
cp .env.example .env
# either fill in the GitHub auth values (see the Authentication section below),
# or set DEMO_MODE=1 and leave the GitHub variables blank for an unsecured demo
docker compose up -dAll secrets come from your .env (which is gitignored). The compose file requires only the database passwords: POSTGRES_PASSWORD for the migration user and POSTGRES_APP_PASSWORD for the least-privilege rostfacto_app role the app connects as. The GitHub auth variables are optional: set GITHUB_ADMIN_ORG, GITHUB_ADMIN_TEAM_SLUG, GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET and PUBLIC_URL to run with GitHub authentication, or set DEMO_MODE=1 and leave them blank for an unsecured demo instance (never in production). The app fails closed when neither is configured. On first boot, docker/init-app-role.sh creates the rostfacto_app role with only SELECT/INSERT/UPDATE/DELETE privileges; for an existing database volume, run it manually once:
docker compose exec db /docker-entrypoint-initdb.d/init-app-role.shImages are pulled from the GitHub Container Registry and pinned by digest for reproducible deployments: ghcr.io/suhlig/rostfacto for the app and ghcr.io/suhlig/rostfacto-migrator for the one-shot migration step. Both are also tagged latest and sha-<commit> on every push to main that changes image-relevant files (the Container workflow path filter, which keeps the digest-bump PRs from triggering their own rebuild); every GitHub release additionally tags them with the release version, e.g. ghcr.io/suhlig/rostfacto:v2.0.
The app is published on port 3000. To run it on a different host port, set APP_PORT:
APP_PORT=8080 docker compose up -dor put APP_PORT=8080 in the .env file; the default stays 3000. With GitHub authentication enabled, make sure PUBLIC_URL matches the chosen port (see the table below).
Set DEMO_MODE=1 to run the app without authentication (every visitor is treated as an admin and a red banner warns that the instance is unsecured). Otherwise the app fails closed: it refuses to start until the GitHub authentication environment variables below are configured. To enable GitHub or GitHub Enterprise authentication, configure the following environment variables:
| Variable | Where to get it |
|---|---|
DEMO_MODE |
Set to 1 to run without authentication; never set it in production. |
PUBLIC_URL |
The public base URL of your deployment, e.g. https://rostfacto.example.com. Used to build the OAuth callback URL. Required whenever authentication is enabled. |
GITHUB_CLIENT_ID |
Create an OAuth App at https://github.com/settings/developers (or on your GHE instance). |
GITHUB_CLIENT_SECRET |
Same OAuth App page as above. |
GITHUB_ADMIN_ORG |
The GitHub organization that contains your admin team. You can see your orgs at https://github.com/settings/organizations. |
GITHUB_ADMIN_TEAM_SLUG |
Inside that org, open the team page and take the slug from the URL, e.g. https://github.com/orgs/ORG/teams/TEAM_SLUG. |
GITHUB_USER_ORG |
Colon-separated list of organizations whose teams can be assigned to retros, e.g. org-a:org-b. Usually includes GITHUB_ADMIN_ORG. The retro creation form lists all teams GitHub exposes to the signed-in user across these orgs: you must be a member (or owner) of an org to see its teams, and secret teams you do not belong to are never exposed by GitHub. Teams are always shown and stored org-qualified, e.g. org-a/engineering. |
GITHUB_APP_OWNER |
Optional name or email of the person to contact when an org's teams cannot be listed (e.g. because the org enforces SAML SSO and the app is not authorized for it). Shown on the retro creation form. |
GITHUB_ENTERPRISE_URL |
Base URL of your GitHub Enterprise Server, e.g. https://github.example.com. Leave unset for github.com. |
When you create the OAuth App, set the callback URL to:
<PUBLIC_URL>/auth/callback
Multiple clients on the same retro stay in sync via server-sent events (SSE):
- The board subscribes to
GET /retro/{slug}/events; every mutation (card added, status changed, liked, edited, timer changed, retro archived) is pushed to all connected clients immediately. - Postgres is the hub: each mutation writes its event to an
eventstable in the same transaction andNOTIFYs a channel that a background task fans out to the connected browsers. The event log is durable, so a client that reconnects catches up on everything it missed (Last-Event-IDreplay). - A client's own mutations are deduplicated, so the HTMX response and the SSE event for the same change are applied exactly once.
- The highlight timer is server-authoritative: highlighting a card starts a five-minute countdown in the database, the +2 min button extends it, and a background sweep marks it elapsed so every client sees
0:00at the same time. The countdown ticks locally, but the deadline always comes from the server. - Archiving a retro (or completing the last card, via the all-done modal) clears the board and stops all timers on every connected client.
The integration tests live in tests/integration_test.rs (with the test functions grouped by feature area under tests/integration_test/) and use thirtyfour to drive Firefox via geckodriver. They start their own instance of the app on a random port, so you can keep your dev server running on port 3000.
On macOS:
brew install geckodriver
brew install --cask firefoxOn other systems, install geckodriver and make sure it is on your PATH, and install a recent Firefox.
export DATABASE_URL=postgres://rostfacto@localhost/rostfacto-dev
cargo test --test integration_testSet SHOW_BROWSER to make Firefox visible while the tests run:
SHOW_BROWSER=1 cargo test --test integration_test- Limit growth of the
eventstable - Clean archived retros after e.g. a year
- Periodic cleanup of sessions
- Allow adding a card anonymously
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, either version 3 of the License, or (at your option) any later version.
See the LICENSE file for full license text.
The concept and some of the artwork was inspired by Postfacto (with a P), reproduced under the GNU Affero General Public License.
