Retrograde is mission control for retrospectives: collaborative boards with sticky notes, voting, timers and action items, shared by people and AI agents. It runs as a hosted service at retrograde.sh, or on your own infrastructure.
This README is the self-hosting guide. If you are working on the code, start with CLAUDE.md and docs/STATE.md.
A self-hosted instance is one Node.js server backed by PostgreSQL, with sign-in through your own OpenID Connect provider. Every account on it gets the full product — named crews, members-only boards, crew action items and AI crewmates — with no billing. You configure it entirely through environment variables; you never edit source code.
- Node.js 20, or Docker. The Docker image is built on
node:20-alpine. - PostgreSQL with TLS enabled. Retrograde always connects over TLS and verifies the server's certificate. It creates and upgrades its own schema every time it starts, so its database user needs permission to create and alter tables.
- An OpenID Connect provider (Keycloak, Okta, Microsoft Entra ID, Auth0, …) that supports the authorization code flow with a client secret and has a userinfo endpoint.
- HTTPS in front of the app. The server runs in production mode, where the session cookie is marked
Secure. Over plain HTTP, browsers drop that cookie and nobody stays signed in.
Create a confidential client with:
- Redirect URI:
https://retro.example.com/auth/callback - Scopes:
openid profile email - Grant type: authorization code
At sign-in Retrograde reads these userinfo claims: sub (the stable account id), email, email_verified, name, given_name, family_name, and nickname or preferred_username.
Retrograde does not read a .env file itself. Pass variables through your process manager, docker run --env-file, or your orchestrator's secrets.
Required
| Variable | Value |
|---|---|
SELF_HOSTED |
true |
DATABASE_URL |
postgresql://USER:PASSWORD@HOST:5432/DATABASE. The connection always uses TLS, and the database server's certificate must be trusted by this machine's CA store. For a private CA, append ?sslrootcert=/path/to/ca.pem (in Docker, mount that file into the container). ?sslmode=no-verify encrypts without checking the certificate; sslmode=disable is refused. Instead of DATABASE_URL you can set all four of PG_HOST, PG_USER, PG_PASSWORD and PG_SCHEMA (the database name) — not both. |
SESSION_SECRET |
A long random string that signs session cookies, for example the output of openssl rand -hex 32. Changing it signs everyone out. |
OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET |
From your identity provider. |
OAUTH_AUTHORIZATION_URL, OAUTH_TOKEN_URL, OAUTH_USERINFO_URL |
Your provider's endpoints, listed in its /.well-known/openid-configuration. |
OAUTH_REDIRECT_URI |
https://retro.example.com/auth/callback. It must match the provider's setting exactly. |
OAUTH_SCOPES |
openid profile email |
SITE_ADMIN_IDS |
Comma-separated sub values of the accounts that administer this instance — at least one. |
Must not be set: STRIPE_RESTRICTED_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_PRICE_ID and CRON_SECRET. A self-hosted instance takes no payments and runs no scheduled cleanup, and startup refuses to continue if any of them is present.
Optional
| Variable | Default | Value |
|---|---|---|
PORT |
3000 |
Port the server listens on, 1–65535. |
NODE_ENV |
production |
Leave it unset. development turns off TLS to the database and the Secure session cookie, and is only for local development. |
HOST |
all interfaces | Interface to bind to. |
HIDE_LOGOUT |
false |
true hides the logout button. Use it when your provider signs people straight back in, which makes logging out pointless. |
OAUTH_LOGOUT_REDIRECT_URL |
/ |
Where the browser goes after logging out: an http(s) URL, such as your provider's end-session endpoint, or a path on this instance. |
OAUTH_USERNAME_FIELD |
preferred_username |
Which stored profile field is shown as a person's name in Retrograde: preferred_username, email, name, given_name or family_name. |
SITE_LOGO_LIGHT_URL, SITE_LOGO_DARK_URL, SITE_LOGO_ALT |
none | Your logo beside the Retrograde mark, as a light-theme image, a dark-theme image and alt text. Set all three or none. See Branding. |
Startup checks every variable: a required one that is missing, or any value that is malformed, stops the server with a message naming it.
With Docker, from a checkout of this repository:
docker build -t retrograde .
docker run -d --name retrograde -p 3000:3000 --env-file retrograde.env retrogradeWrite retrograde.env as plain NAME=value lines. Docker does not strip quotes, so do not quote the values.
Without Docker, with the variables exported in your shell or service definition:
npm ci
npm run build
npm startEither way, put your HTTPS proxy or load balancer (nginx, Caddy, a cloud load balancer) in front of the port and point it at GET /healthcheck, which answers 200 OK.
If a setting is missing, malformed or contradictory, the server logs a line beginning FATAL: that names the variable, then exits. The common ones:
| Message | Fix |
|---|---|
missing required environment variable STRIPE_… (… set SELF_HOSTED=true …) |
Set SELF_HOSTED=true. |
missing required environment variable … |
Set the variable it names. |
missing database configuration … |
Set DATABASE_URL, or all four PG_* variables. |
could not connect to the database |
Check the host and credentials, and that the database accepts TLS with a certificate this server trusts. |
SELF_HOSTED=true but STRIPE_… is set |
Remove the Stripe variables. |
SELF_HOSTED=true but CRON_SECRET is set |
Remove CRON_SECRET. |
incomplete site logo (missing …) |
Set all three SITE_LOGO_* variables, or none. |
Then open https://retro.example.com/. On a self-hosted instance / goes straight to the dashboard, which sends you to your provider to sign in.
On the hosted service, boards created without signing in and not in any crew are archived 30 days after they were created. A self-hosted instance does not do this: those boards stay open until someone archives or deletes them, and there is nothing to schedule.
The SITE_LOGO_* URLs are fetched by your users' browsers, so they must be reachable from wherever your users are. Host the images anywhere, or put them in the app's static directory: files under build/client are served from /, so build/client/branding/logo-light.svg is available at /branding/logo-light.svg without rebuilding. In Docker, mount a directory there:
docker run -d --name retrograde -p 3000:3000 --env-file retrograde.env \
-v /srv/retrograde/branding:/app/build/client/branding:ro retrogradewith SITE_LOGO_LIGHT_URL=/branding/logo-light.svg and SITE_LOGO_DARK_URL=/branding/logo-dark.svg.
- Everyone gets every feature. Any account can create named crews, and crews never freeze for lack of a subscription. Each account's personal crew still holds one AI crewmate key, as it does for a paying account on the hosted service; more come from named crews.
- No guests. Every page and API call needs a signed-in account. Agents use API keys a signed-in crew owner creates; the unauthenticated trial API is closed. Only sign-in and
/healthcheckare open. - No billing. The billing pages and the Stripe webhook return
404, and there is no subscribe prompt. - No marketing site.
/,/about,/contact,/terms-of-serviceand/privacy-policyredirect to the dashboard,/sitemap.xmlreturns404,/robots.txtdisallows all crawling, and the header has no About or Contact links. - No Vercel Analytics. The instance never loads Vercel's analytics script.
- No board cleanup. Boards created without signing in are never archived automatically.
On the hosted service, anyone with a board's link can open that board and take part without signing in, unless the board belongs to a named crew — those are members-only by default. A self-hosted instance has no guests: everyone signs in through your identity provider first, and boards in a named crew are still members-only among the people who have.
Replace the image or the build and restart. Your configuration lives in the environment, so nothing needs to be re-applied. Retrograde upgrades its database schema when it starts, so back up the database before upgrading.
From 1.x to 2.0:
-
Set
SELF_HOSTED=trueandSITE_ADMIN_IDS, make sure every required variable above is set, and make sure your database accepts TLS connections. 2.0 refuses to start otherwise. -
app/config/siteConfig.tsno longer exists. Drop any edits you made to it and use environment variables instead: -
If you set
LOGOUT_REDIRECT_URL, rename it toOAUTH_LOGOUT_REDIRECT_URL.
docs/STATE.md— the current project snapshot: what's shipped and known rough edges.docs/adr/— Architecture Decision Records: the decisions that shape the product and architecture, and why.docs/spec/0001-action-registry.md— every action in the product, who may take it, and what enforces that.docs/AI_AGENT_API.md— JSON API reference for AI agents.CLAUDE.md— engineering rules, architecture and conventions for contributors.
