Skip to content
nickkhgPublic

About

Real-time retrospective tool for teams — React + Rust Axum + Tauri v2

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

Rewind

GitHub release License Docker image

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.

Board view — light mode

Dark mode

Board view — dark mode

Features

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

Home page — create a board

Home page — dark mode

Home page — dark mode

Architecture

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::broadcast per board for WebSocket fan-out
  • Vite proxy in dev so both web and Tauri use relative URLs

Getting Started

Prerequisites

  • Rust (stable)
  • Node.js 18+
  • pnpm
  • PostgreSQL (or Docker)

Development

# 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 dev

Open http://localhost:5173 to use the app.

Desktop App

cargo tauri dev

Environment Variables

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.

Entra Sign-In

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 returns
    • https://<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.

Production Build

cd frontend && pnpm build
cd ../backend && cargo build --release
cargo tauri build  # for macOS .app bundle

Grouping with AI

A 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:

  1. AZURE_AI_API_KEY, for a laptop.
  2. Azure Workload Identity: AZURE_FEDERATED_TOKEN_FILE, AZURE_CLIENT_ID and AZURE_TENANT_ID. On AKS, the webhook sets these.
  3. The App Service / Container Apps identity endpoint (IDENTITY_ENDPOINT, IDENTITY_HEADER).
  4. The instance metadata service of the node. This uses the system-assigned identity, or the user-assigned identity that AZURE_CLIENT_ID names.

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.

Deployment

Option 1: Docker Compose (recommended)

The simplest way to run Rewind. This starts PostgreSQL and the app together:

docker compose up -d

The 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$HASH

Note: If setting ADMIN_TOKEN_HASH directly in docker-compose.yml, double all $ signs to escape YAML variable interpolation (e.g. $$argon2id$$v=19$$...). Using an env_file: avoids this.

Option 2: Docker image only

Pull the pre-built image from GitHub Container Registry (published on each tagged release):

docker pull ghcr.io/nickkhg/rewind:latest

Run 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:latest

The image bundles both the backend binary and the frontend static assets — no separate web server needed.

Option 3: Standalone binary + static files

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

The backend serves the frontend at the same port and handles SPA fallback routing — no Nginx or reverse proxy required for basic setups.

Option 4: Backend only (API server)

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

Then build and deploy the frontend pointing at the backend:

cd frontend
VITE_API_URL=https://api.example.com pnpm build

The resulting dist/ directory can be served by any static file host. WebSocket connections go to the same VITE_API_URL origin.

Database

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.

CI/CD

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

Admin Interface

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.

Setup

1. Generate a token hash

cd backend && cargo run --bin hash_admin_token

Enter 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

2. Configure the backend

Add the hash to backend/.env:

ADMIN_TOKEN_HASH=$argon2id$v=19$m=19456,t=2,p=1$SALT$HASH

The 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

3. Docker Compose

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$$HASH

Alternatively, use an env_file: — .env files don't need escaping.

Usage

  1. Visit the home page and click the Admin link below the form
  2. Enter the plaintext admin token to log in (stored in sessionStorage)
  3. The dashboard shows global stats (boards, tickets, votes, online users) and a board table
  4. Click a board row to see its detail panel (columns, facilitator token, blur state)
  5. Switch to the Templates tab to create, edit, or delete board templates
  6. Switch to the Teams tab to manage teams for the Wheel of Misfortune
  7. Delete boards from the table or detail panel (with confirmation dialog)

API Endpoints

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)

Design

"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

About

Real-time retrospective tool for teams — React + Rust Axum + Tauri v2

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages