Configuration
AsyncUp is configured in two layers:
- Bootstrap: a handful of environment variables (database, port, the operator token, the encryption key). Set once, rarely touched. This page is the one reference for them.
- Everything else: managed in the web app under Settings and stored in your database, secrets encrypted (AES-256-GCM via
SECRET_KEY). Changes apply immediately, no restart.
Environment variables
| Variable | Default | Purpose |
|---|---|---|
PORT | 8080 | Webhook, API and web app port |
DB_PATH | ./data/standup.db | SQLite database file (default storage). The Docker image sets /data/standup.db |
DATABASE_URL | (empty) | Bring-your-own PostgreSQL — when set, SQLite is skipped (see Deployment reference) |
DB_SSL | from URL sslmode | Postgres TLS: require (encrypt, no verify — default for managed DBs), verify-full, or disable |
DB_SSL_CA | (empty) | CA bundle path for DB_SSL=verify-full |
OPERATOR_TOKEN | (empty) | Operator token for the web app and the JSON API — the way in before Google or SAML sign-in works, and break-glass after. Disabled while empty. DASHBOARD_TOKEN is the older name and still works; when both are set, OPERATOR_TOKEN wins, and a blank OPERATOR_TOKEN= falls back to DASHBOARD_TOKEN |
SECRET_KEY | — | Encrypts stored secrets; signs sessions and webhooks. Generate with openssl rand -hex 32. Required (except ADAPTER=fake). Back it up separately from the database |
ADAPTER | google | google for production, fake for a console demo |
TENANT_ID | default | Tenant identifier — leave as is for self-hosted installs |
Changing a variable needs the container recreated, not just restarted: docker compose up -d --force-recreate asyncup.
App settings (stored in the database)
Open https://HOST/app → Settings. Each value saves on its own. Secrets are write-only: the UI shows that they're set (and, for the service account, its email), never the value.
| Where | Setting | What it does |
|---|---|---|
| General | Default timezone for new standups | Assigned to new standups; people can set their own |
| General | Calendar out-of-office | Mark people away on OOO days. Off by default; turn on once the delegation for the Calendar scope exists |
| General | Leave in Chat and Calendar | Days off and vacation mode as a Chat status and an out-of-office event. Off by default; needs two delegated scopes |
| General | Spelling suggestions | Likely typos in Chat answers pointed out once before posting, in British or American English, with a list of words to accept. Off by default. See below |
| Google Chat | Project number (and the events URL) | The audience incoming events are verified against. For an add-on Chat app both are needed — the audience rule |
| Google Chat | Service account | The JSON key used for Chat API calls, group rosters, Directory lookups and Calendar. Empty = Application Default Credentials for Chat only |
| Google Chat | Directory through delegation (legacy) | Optional Workspace admin to act as: moves Directory lookups to domain-wide delegation. Leave it off with the admin role; Calendar out-of-office doesn't use it. The Client ID for domain-wide delegation shows under Service account |
| Sign-in & SSO | Google sign-in | OAuth client ID and secret — Google sign-in |
| Sign-in & SSO | SAML single sign-on | IdP entity ID, SSO URL, certificate, admin attribute (default groups) and admin group (default asyncup-admins) — SAML |
| Sign-in & SSO | Setup token | Whether the operator token still signs in. Can only be switched off while Google or SAML works |
| MCP server | Server, default scopes, access tokens | Claude Code and MCP |
| API & tokens | Machine tokens | Tick (POST /tick), CSV export (GET /export) and SCIM (/scim/v2) tokens, each shown once |
| Danger zone | Disconnect Google Chat; Delete all standup history | Both behind a typed confirmation |
Spelling suggestions
The checker is Harper (Apache-2.0). It runs on your server, in its own process: nothing is sent anywhere. That process starts at the first answer, uses about 350 MB while it runs, and stops after 30 minutes without an answer, giving the memory back. It adds about 72 MB to the installed packages.
It checks spelling and typos only, never grammar or style. Names on any roster are accepted, and admins can add words under Words to accept, one per line. If the checker can't start within ten seconds, or doesn't answer within two, the answer is saved unchecked: spelling never blocks anyone. The web app's page language follows the same setting, so the browser's own spell check uses it too.
Per-standup settings (schedule, questions, mood, people, roster group, escalation, webhook, digest, archive and delete) live on each standup's Settings tab and in the chat commands.
Endpoints
All token comparisons are constant-time. ✱ = covered by the shared rate limiter (60 requests a minute per IP).
| Endpoint | Auth | Purpose |
|---|---|---|
GET /healthz | none | Liveness: pings the database |
GET /health/chat | none | Chat connection state without secrets: audience and key set, last verified or refused event |
POST /chat/events | Google-signed JWT, verified against the audience | Google Chat events — point the Chat app here. Refuses everything until the audience is set |
POST /tick ✱ | Bearer tick token (open until one exists) | Advance the scheduler from an external cron (scale-to-zero) |
GET /export?standupId=N&days=D ✱ | Bearer export token (404 until one exists) | CSV, D clamped to 1–365 |
GET /app[/…] | Session cookie, or the operator token entered on the sign-in page | The web app, including the guided setup at /app/setup and the personal page at /app/me |
/api/v1/… (300 requests a minute) | Session cookie, or bearer operator token; auth/methods and openapi.json are public | The JSON API; OpenAPI 3.1 at GET /api/v1/openapi.json |
/mcp ✱ | Bearer MCP token (503 while switched off; browser Origin refused) | Model Context Protocol for Claude Code and other assistants |
GET /auth/google, GET /auth/callback, POST /auth/logout ✱ | OAuth state nonce | Google sign-in |
GET /auth/saml, POST /auth/saml/acs, GET /auth/saml/metadata ✱ | Signed assertions | SAML sign-in and SP metadata (404 until the IdP is configured) |
/scim/v2/ServiceProviderConfig, /scim/v2/Users[…] ✱ | Bearer SCIM token (404 until one exists) | SCIM 2.0 |
Outbound webhooks
Give a standup a Webhook URL (Standup › Settings › Integrations) and AsyncUp POSTs JSON to it as things happen — the cheap path into Sheets (Apps Script), Zapier/n8n, or your own service:
| Event | When | Body |
|---|---|---|
submission | Every submission and edit | standup, date, user, answers, mood, late, edited, editApprovedBy (who approved an edit after the deadline, otherwise null). Mood comments are never sent |
wrap_up | The run closes | standup, date, summary (the posted wrap-up's numbers, plus teamMood on anonymous standups) |
schedule_change | Someone's days on the standup, or their days off, change | person, summary, who, channel — see Personal schedules |
time_off_request | A request needs approval | person, dates, working, reason, request ids |
poll_closed | A poll with questions closes | poll: id, title, anonymous, asked, answered and each question's counts per option, with names only on a named poll |
test | Send test in the standup's settings | standup |
Deliveries time out after 5 s and failures are only logged — a dead webhook never breaks the standup.
Every delivery is signed: the X-AsyncUp-Signature header carries sha256=<hex>, the HMAC-SHA256 of the raw request body with the standup's signing secret. Copy it from the standup's Settings → Outbound webhook → Show signing secret (admins and that standup's managers), or fetch it with GET /api/v1/standups/:id/webhook/secret. It is derived from SECRET_KEY and the standup's id, so it changes only when SECRET_KEY does.
Verify it in your receiver and reject anything that doesn't match:
const expected = 'sha256=' + crypto.createHmac('sha256', SECRET).update(rawBody).digest('hex');
crypto.timingSafeEqual(Buffer.from(header), Buffer.from(expected));Data
All state — standups, participants, admins, runs, submissions, blockers, app settings — lives either in a single SQLite file (DB_PATH, the default) or in your own PostgreSQL (DATABASE_URL). Schema migrations run automatically on startup in both modes. Backups and upgrades: Operations.
Managed Postgres TLS: RDS / Cloud SQL / Neon / Supabase present a certificate signed by their own CA. AsyncUp treats
sslmode=requireas "encrypt but don't verify the cert" (the libpq default every other client uses), soDATABASE_URL=…?sslmode=requireconnects out of the box. For strict verification setDB_SSL=verify-fulland pointDB_SSL_CAat the provider's CA bundle.
Stored secrets are encrypted, so backups are safe to ship off-box as long as SECRET_KEY stays out of them. Graceful shutdown on SIGTERM included.