Download macOS DMG | Docker Image | Linux Binary
A real-time retrospective tool for teams. Run column-based retro sessions where everyone collaborates live — as a website or macOS desktop app.
No accounts needed. The facilitator creates a board, shares a link, and the team adds cards, votes, and reveals together. Boards are persisted in PostgreSQL so they survive server restarts.
- Real-time collaboration — cards, votes, and blur state sync instantly via WebSocket
- Blur / Reveal — facilitator controls card visibility; authors always see their own cards. Facilitators and editors can peek at blurred cards without revealing them to everyone
- Voting — toggle votes on any card, sort by most votes or newest
- Editor access requests — participants can request editor privileges; the facilitator approves or declines from the settings menu. Editors get the same controls as the facilitator (blur, vote limits, timer, delete/split cards). On anonymous boards, requesters provide a display name. The facilitator can revoke editor access at any time
- Board templates — Classic, Start/Stop/Continue, 4Ls, Mad Sad Glad, Sailboat, DAKI, Level 10 — managed in the database
- Level 10 meetings — a board made from the Level 10 template adds a scorecard (metric, goal, this week, on track) kept by the facilitator, an on-track / off-track mark on the cards in the Rocks column, and a 1–10 meeting rating from each participant with the average in the header. Every other board shows none of these
- Custom columns — or define your own column layout
- Anonymous boards — optional name-free mode (enabled by default)
- Entra sign-in (optional) — name an Entra app registration and the whole app goes behind a work account; name none and it stays open, as it has always been. The signed-in name pre-fills the join field (see Entra Sign-In)
- Share link — one-click copy to clipboard
- Dark mode — light and dark themes with system preference detection
- Desktop app — native macOS window via Tauri v2 with
rewind://deep links - Wheel of Misfortune — facilitators can spin a carnival-themed wheel to randomly pick who runs the next retro. Select a whole team or individual members, spin the wheel, and accept the result to auto-create a ticket in the last column
- Admin CMS — view all boards and manage templates and teams (see below)
frontend/ React + Vite + Tailwind v4 + Zustand
backend/ Rust Axum server (REST + WebSocket) + PostgreSQL
src-tauri/ Tauri v2 desktop wrapper
- REST for board creation, WebSocket for everything else
- Full board state broadcast on every mutation (no diffs — boards are small)
tokio::sync::broadcastper board for WebSocket fan-out- Vite proxy in dev so both web and Tauri use relative URLs
- Rust (stable)
- Node.js 18+
- pnpm
- PostgreSQL (or Docker)
# Start PostgreSQL (if using Docker)
docker compose up db -d
# Install frontend dependencies
cd frontend && pnpm install && cd ..
# Terminal 1: Backend (port 3001)
cd backend && cargo run
# Terminal 2: Frontend (port 5173)
cd frontend && pnpm devOpen http://localhost:5173 to use the app.
cargo tauri dev| Variable | Required | Default | Description |
|---|---|---|---|
DATABASE_URL |
Yes | — | PostgreSQL connection string (e.g. postgres://user:pass@localhost:5432/rewind) |
PORT |
No | 3001 |
Port the backend listens on |
STATIC_DIR |
No | — | Path to built frontend assets. When set, the backend serves them and handles SPA routing |
ADMIN_TOKEN_HASH |
No | — | Argon2id hash for admin access (see Admin Interface). If omitted, admin routes return 404 |
GIPHY_API_KEY |
No | — | GIPHY web SDK key. Omit and the GIF controls stay hidden. The browser holds this key, so use a domain-restricted one |
ENTRA_TENANT_ID |
No | — | Directory (tenant) ID of the Entra app registration (see Entra Sign-In) |
ENTRA_CLIENT_ID |
No | — | Application (client) ID |
ENTRA_CLIENT_SECRET |
No | — | Client secret. It stays in the pod; the browser never sees it |
AZURE_AI_ENDPOINT |
No | — | Endpoint of a Microsoft Foundry resource, e.g. https://my-team.services.ai.azure.com (see Grouping with AI) |
AZURE_AI_DEPLOYMENT |
No | — | Name of the model deployment on that resource, e.g. claude-opus-5-5 |
AZURE_AI_API |
No | from the name | anthropic for a Claude deployment, openai for any other. Omitted, a name that starts with claude means anthropic |
AZURE_AI_API_KEY |
No | — | A key of the Foundry resource, for local development. In a cluster, leave it out and use a managed identity |
PUBLIC_URL |
No | — | The origin browsers reach Rewind on, e.g. https://rewind.example.com. Only needed when a proxy rewrites the host — otherwise the redirect URI is derived from X-Forwarded-Proto / X-Forwarded-Host |
VITE_API_URL |
No | — | Frontend override for backend URL (only needed if the frontend is hosted separately from the backend) |
RUST_LOG |
No | info |
Log level filter (e.g. debug, rewind_backend=debug) |
The three ENTRA_* variables go together: set all three to put the app behind a work
account, or none to leave it open. Setting one or two stops the server on purpose, rather
than serving every board to anyone while looking configured.
By default a board is open to whoever holds its link. Set the three ENTRA_* variables and
the whole app goes behind Microsoft Entra sign-in instead — the API, the WebSocket, and the
page itself. Anyone in the tenant gets in; who may do what on a board is still the
facilitator token, the editor list and the board password, exactly as before.
The server runs the OIDC authorization code flow with PKCE itself, so the client secret never reaches the browser. What the browser holds is one encrypted cookie the server wrote, good for 12 hours.
In Entra, create an app registration:
- Single tenant, platform Web — only a confidential client may hold a secret.
- Redirect URIs:
https://<your host>/api/auth/callback— where sign-in returnshttps://<your host>/— where sign-out returns. Without it Entra ends the sign-out on a page of its own
- Add a client secret.
In the chart, fill in secrets.entra:
secrets:
entra:
tenantId: "<directory (tenant) ID>"
clientId: "<application (client) ID>"
clientSecret: "<client secret>"Two things worth knowing:
- The name comes from Entra. The join field is pre-filled with your display name and the header shows who you are, with a way out. It is still a field — a person may write what the board should call them, and an anonymous board shows no name at all.
- The desktop app cannot sign in. It loads its pages from disk and talks to the server from another origin, so there is nowhere for the cookie to live. Against a server that asks for an account it says so and points at the browser. Use the browser for a signed-in deployment.
Health checks read GET /api/health, which answers whoever asks and reports whether the
server wants an account. Everything else answers 401 — a probe pointed at a real route
would restart the pod for ever.
cd frontend && pnpm build
cd ../backend && cargo build --release
cargo tauri build # for macOS .app bundleA retro spends its first minutes merging cards that say the same thing. With a model on Microsoft Foundry, the facilitator or an editor can press Group with AI on a column. The model reads the cards and suggests which ones make the same point. The cards of each suggested group come together in a dashed outline. The reviewer accepts or rejects each group, or takes single cards out with Leave out, then presses Merge. The accepted groups merge as a drag merge would, and Undo takes back the whole grouping in one step. A suggestion goes only to the person who asked for it, and nothing changes on the board before the merge.
The control is there only when all of these are true: the server names a Foundry deployment, the reader is the facilitator or an editor, and the column holds two cards or more. A blurred board refuses the request, because merging is off while cards are hidden. When the button is pressed, the text of every card in that column goes to the model, so pick a deployment that your organisation is content to send retro notes to.
Set AZURE_AI_ENDPOINT and AZURE_AI_DEPLOYMENT together, or leave both empty to turn the
feature off. If you set only one, the server stops. Claude deployments use the Messages API
under /anthropic. All other deployments use OpenAI chat completions under /openai/v1.
Signing in to Foundry. The server uses a managed identity, so no key goes into the chart. It looks for these, in this order:
AZURE_AI_API_KEY, for a laptop.- Azure Workload Identity:
AZURE_FEDERATED_TOKEN_FILE,AZURE_CLIENT_IDandAZURE_TENANT_ID. On AKS, the webhook sets these. - The App Service / Container Apps identity endpoint (
IDENTITY_ENDPOINT,IDENTITY_HEADER). - The instance metadata service of the node. This uses the system-assigned identity, or the
user-assigned identity that
AZURE_CLIENT_IDnames.
The identity needs a data-plane role on the Foundry resource, such as Azure AI User or Cognitive Services User. On AKS, fill in the chart:
ai:
endpoint: "https://my-team.services.ai.azure.com"
deployment: "claude-opus-5-5"
workloadIdentity:
clientId: "<client ID of the user-assigned managed identity>"The chart then creates a service account annotated with that client ID, and labels the pod for
the workload identity webhook. On the managed identity, add a federated credential for
system:serviceaccount:<namespace>:<release fullname> with the cluster's OIDC issuer.
The simplest way to run Rewind. This starts PostgreSQL and the app together:
docker compose up -dThe app is available at http://localhost:3001. The Compose file includes a health check on the database — the app waits for it before starting.
To customize, copy the environment block from docker-compose.yml or use an .env file:
DATABASE_URL=postgres://rewind:rewind@db:5432/rewind
ADMIN_TOKEN_HASH=$argon2id$v=19$m=19456,t=2,p=1$SALT$HASHNote: If setting
ADMIN_TOKEN_HASHdirectly indocker-compose.yml, double all$signs to escape YAML variable interpolation (e.g.$$argon2id$$v=19$$...). Using anenv_file:avoids this.
Pull the pre-built image from GitHub Container Registry (published on each tagged release):
docker pull ghcr.io/nickkhg/rewind:latestRun it against your own PostgreSQL instance:
docker run -d -p 3001:3001 \
-e DATABASE_URL=postgres://user:pass@host:5432/rewind \
-e ADMIN_TOKEN_HASH='$argon2id$...' \
ghcr.io/nickkhg/rewind:latestThe image bundles both the backend binary and the frontend static assets — no separate web server needed.
Each GitHub release includes:
rewind-backend— statically-linked Linux binary (x86_64 musl)frontend-dist.tar.gz— pre-built frontend assets
Deploy them together:
# Extract frontend assets
mkdir -p /srv/rewind/static
tar -xzf frontend-dist.tar.gz -C /srv/rewind/static
# Run the server
DATABASE_URL=postgres://user:pass@localhost:5432/rewind \
STATIC_DIR=/srv/rewind/static \
PORT=3001 \
./rewind-backendThe backend serves the frontend at the same port and handles SPA fallback routing — no Nginx or reverse proxy required for basic setups.
If you host the frontend separately (e.g. on a CDN or different server), run the backend without STATIC_DIR:
DATABASE_URL=postgres://user:pass@localhost:5432/rewind \
PORT=3001 \
./rewind-backendThen build and deploy the frontend pointing at the backend:
cd frontend
VITE_API_URL=https://api.example.com pnpm buildThe resulting dist/ directory can be served by any static file host. WebSocket connections go to the same VITE_API_URL origin.
Rewind requires PostgreSQL (tested with 15–17). Migrations run automatically on startup — no manual schema setup needed. The backend creates all tables, indexes, and seed data (default templates) on first launch.
The GitHub Actions workflow (.github/workflows/release.yml) builds all artifacts on a tagged push (v*):
| Artifact | Description |
|---|---|
| macOS DMG | Signed & notarized Tauri desktop app |
| Linux binary | Static musl binary for x86_64 |
| Frontend tarball | Pre-built Vite output |
| Docker image | Multi-stage image pushed to ghcr.io |
To create a release: git tag v1.1.0 && git push --tags
An optional admin dashboard at /admin lets a privileged user view all boards, manage templates, and delete boards. Access is gated by an Argon2-hashed secret token.
cd backend && cargo run --bin hash_admin_tokenEnter a plaintext token when prompted. The tool outputs an Argon2id PHC string like:
ADMIN_TOKEN_HASH=$argon2id$v=19$m=19456,t=2,p=1$SALT$HASH
Add the hash to backend/.env:
ADMIN_TOKEN_HASH=$argon2id$v=19$m=19456,t=2,p=1$SALT$HASHThe env var is optional — if omitted, the admin routes return 404.
When the backend starts with a valid hash, it logs:
INFO admin interface enabled
In docker-compose.yml, $ signs must be doubled to escape YAML variable interpolation:
environment:
ADMIN_TOKEN_HASH: $$argon2id$$v=19$$m=19456,t=2,p=1$$SALT$$HASHAlternatively, use an env_file: — .env files don't need escaping.
- Visit the home page and click the Admin link below the form
- Enter the plaintext admin token to log in (stored in
sessionStorage) - The dashboard shows global stats (boards, tickets, votes, online users) and a board table
- Click a board row to see its detail panel (columns, facilitator token, blur state)
- Switch to the Templates tab to create, edit, or delete board templates
- Switch to the Teams tab to manage teams for the Wheel of Misfortune
- Delete boards from the table or detail panel (with confirmation dialog)
All admin endpoints require Authorization: Bearer <plaintext-token>.
| Method | Path | Description |
|---|---|---|
POST |
/api/admin/verify |
Verify token (200 or 401) |
GET |
/api/admin/stats |
Global counts (boards, tickets, votes, online) |
GET |
/api/admin/boards |
List all boards with stats |
GET |
/api/admin/boards/:id |
Board detail (columns, facilitator token) |
DELETE |
/api/admin/boards/:id |
Delete board (cascades tickets/votes) |
GET |
/api/admin/templates |
List all templates |
POST |
/api/admin/templates |
Create a template |
PUT |
/api/admin/templates/:id |
Update a template |
DELETE |
/api/admin/templates/:id |
Delete a template |
GET |
/api/admin/teams |
List all teams |
POST |
/api/admin/teams |
Create a team |
PUT |
/api/admin/teams/:id |
Update a team |
DELETE |
/api/admin/teams/:id |
Delete a team |
GET |
/api/teams |
List all teams (public, for board owners) |
"Warm Workshop" aesthetic — sticky notes on a real whiteboard.
- Fonts: Fraunces (display) + Plus Jakarta Sans (body)
- Palette: warm canvas
#faf8f5, terracotta accent#e07a5f, column colors (green, rose, amber, blue, purple) - Details: colored left borders, blur reveal transition, vote bounce animation, subtle noise texture



