Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 21 additions & 49 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,58 +1,30 @@
# HTTP port the webhook server listens on
# ───────────────────────────────────────────────────────────────────────
# AsyncUp bootstrap configuration.
# Everything else — Google Chat credentials, AI keys, integrations,
# access tokens, default timezone — is configured in the web dashboard
# and stored (encrypted) in the database.
# ───────────────────────────────────────────────────────────────────────

# HTTP port the webhook server + dashboard listen on
PORT=8080

# Path to the SQLite database file (the zero-config default)
# Storage: embedded SQLite file (default), or bring your own PostgreSQL
# by setting DATABASE_URL (managed Postgres, or the bundled compose
# service: docker compose --profile postgres up -d).
DB_PATH=./data/standup.db

# Bring your own database: set a PostgreSQL connection string and the
# embedded SQLite is skipped entirely. Works with managed Postgres
# (RDS, Cloud SQL, Neon, Supabase, …) or the bundled compose service
# (docker compose --profile postgres up -d).
# DATABASE_URL=postgres://asyncup:password@postgres:5432/asyncup
DATABASE_URL=
# POSTGRES_PASSWORD= # only for the bundled compose Postgres

# Password for the optional bundled Postgres compose service
# POSTGRES_PASSWORD=

# Chat adapter: "google" for production, "fake" for local demo (logs to console)
ADAPTER=google

# Your GCP project NUMBER (not ID) — used to verify that incoming webhook
# requests really come from Google Chat. Leave empty to skip verification
# (local development only — never in production).
GOOGLE_CHAT_AUDIENCE=
# Secret for the web dashboard at /dashboard?token=<value>.
# The dashboard is where all app configuration happens — set this!
DASHBOARD_TOKEN=

# Path to the service account key JSON (with Chat API access).
# Standard Google auth env var; not needed for ADAPTER=fake.
GOOGLE_APPLICATION_CREDENTIALS=./service-account.json
# Encrypts secrets stored in the database (AES-256-GCM).
# Generate once: openssl rand -hex 32
SECRET_KEY=

# Default IANA timezone for new standups
DEFAULT_TIMEZONE=Asia/Kolkata
# "google" for production, "fake" for a local console demo
ADAPTER=google

# Tenant identifier (single-tenant self-hosted installs can leave this)
# Tenant identifier single-tenant self-hosted installs leave this alone
TENANT_ID=default

# Optional shared secret for POST /tick — used by external cron (e.g. Cloud
# Scheduler) on scale-to-zero deployments. Callers must send
# "Authorization: Bearer <token>". Empty = endpoint is unauthenticated.
TICK_TOKEN=

# Optional shared secret for GET /export (CSV download of standup answers).
# The endpoint stays disabled until this is set.
EXPORT_TOKEN=

# Optional shared secret for the web dashboard (config + history) at
# /dashboard?token=<value>. Disabled until set.
DASHBOARD_TOKEN=

# Auto-mark participants as away when their Google Calendar has an
# "Out of office" event. Requires domain-wide delegation for the service
# account (scope: calendar.events.readonly) — see the docs.
GOOGLE_CALENDAR_OOO=false

# Optional bring-your-own-key LLM for AI summaries (enable per standup with
# the "ai on" command). Provider: "anthropic" or "openai".
# LLM_MODEL defaults to claude-opus-4-7 for anthropic; required for openai.
LLM_PROVIDER=
LLM_API_KEY=
LLM_MODEL=
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ service-account.json
docs/.vitepress/dist/
docs/.vitepress/cache/
coverage/
.claude/
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Prereq: a one-time Google Chat app configuration (~15 min) — see
**[docs/guide/google-chat-setup.md](docs/guide/google-chat-setup.md)**.

```bash
cp .env.example .env # fill in GOOGLE_CHAT_AUDIENCE, mount your service account key
cp .env.example .env # set DASHBOARD_TOKEN + SECRET_KEY (openssl rand -hex 32)
docker compose up -d # pulls ghcr.io/asyncup-dev/asyncup (amd64 + arm64)
```

Expand Down
5 changes: 3 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ reproduction steps and the deployment mode (Docker, bare Node, proxy setup).
## Scope notes for self-hosters

- `POST /chat/events` should only be reachable via HTTPS, and
`GOOGLE_CHAT_AUDIENCE` must be set in production — it cryptographically
the GCP project number must be set in dashboard settings — it cryptographically
verifies that requests come from Google Chat.
- Set `TICK_TOKEN` if your `/tick` endpoint is internet-reachable.
- Generate a tick token (dashboard → Settings) if `/tick` is internet-reachable.
- Keep `SECRET_KEY` out of database backups — it decrypts stored credentials.
- The SQLite database contains your team's standup answers — treat backups
accordingly.
3 changes: 0 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@ services:
DATABASE_URL: ${DATABASE_URL:-}
volumes:
- standup-data:/data
# Mount your GCP service account key and set
# GOOGLE_APPLICATION_CREDENTIALS=/app/service-account.json in .env:
# - ./service-account.json:/app/service-account.json:ro

# Optional same-machine PostgreSQL:
# docker compose --profile postgres up -d
Expand Down
18 changes: 8 additions & 10 deletions docs/guide/ai.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,16 @@
AsyncUp can post an **AI TL;DR** under each day's thread and an **AI week in
review** with the weekly digest. This is strictly opt-in, twice:

1. The self-hoster configures an LLM key on the server (below). Without it,
1. The self-hoster adds an LLM key in the dashboard (below). Without it,
nothing ever leaves your infrastructure.
2. A standup admin enables it per standup with `ai on`.

## Server configuration
## Configuration

```bash
# .env
LLM_PROVIDER=anthropic # or "openai"
LLM_API_KEY=sk-ant-...
# LLM_MODEL=claude-opus-4-7 # default for anthropic; required for openai
```
Dashboard → **Settings → AI summaries**: pick the provider (Anthropic or
OpenAI), paste your API key (stored encrypted), optionally set the model
(Anthropic defaults to `claude-opus-4-7`; OpenAI requires an explicit model).
Saving applies immediately.

The integration uses plain HTTPS calls (no SDK dependency) and only ever sends
the standup submissions of standups that have `ai on`. Failures are logged and
Expand All @@ -31,8 +29,8 @@ never block the run from closing.
## Cost & model notes

A daily summary for a 10-person team is roughly 1–2k input tokens — a few
cents per day even on the most capable models. Set `LLM_MODEL` to a smaller
model (e.g. `claude-haiku-4-5`) if you want it near-free.
cents per day even on the most capable models. Set a smaller model
(e.g. `claude-haiku-4-5`) in Settings if you want it near-free.

## Privacy considerations

Expand Down
65 changes: 41 additions & 24 deletions docs/guide/configuration.md
Original file line number Diff line number Diff line change
@@ -1,41 +1,58 @@
# Configuration

Everything is configured via environment variables (see `.env.example`).
AsyncUp is configured in two layers:

1. **Bootstrap** — a handful of environment variables (where's the database,
what port, the dashboard token). Set once, rarely touched.
2. **Everything else** — managed in the **[web dashboard](./dashboard)
Settings page** and stored in your database, with secrets encrypted
(AES-256-GCM via `SECRET_KEY`). Changes apply immediately, no restart.

## Bootstrap environment variables

| Variable | Default | Purpose |
| --- | --- | --- |
| `PORT` | `8080` | Webhook port |
| `PORT` | `8080` | Webhook + dashboard port |
| `DB_PATH` | `./data/standup.db` | SQLite database file (default storage) |
| `DATABASE_URL` | *(empty)* | Bring-your-own PostgreSQL connection string — when set, SQLite is skipped (see [Deployment](./deployment#database-embedded-or-bring-your-own)) |
| `DATABASE_URL` | *(empty)* | Bring-your-own PostgreSQL — when set, SQLite is skipped (see [Deployment](./deployment#database-embedded-or-bring-your-own)) |
| `DASHBOARD_TOKEN` | *(empty)* | Secret for `/dashboard` — **required** to configure the app. Disabled while empty |
| `SECRET_KEY` | — | Encrypts stored secrets. Generate with `openssl rand -hex 32`. Required (except `ADAPTER=fake`) |
| `ADAPTER` | `google` | `google` for production, `fake` for a console demo |
| `GOOGLE_CHAT_AUDIENCE` | *(empty)* | Your GCP project **number**. Verifies incoming requests are signed by Google Chat. Empty skips verification — local development only |
| `GOOGLE_APPLICATION_CREDENTIALS` | — | Path to the service account key JSON |
| `DEFAULT_TIMEZONE` | `UTC` | Timezone assigned to newly created standups |
| `TENANT_ID` | `default` | Tenant identifier — leave as is for self-hosted installs |
| `TICK_TOKEN` | *(empty)* | Shared secret for `POST /tick` (see [Deployment](./deployment#scale-to-zero-cloud-run)) |
| `EXPORT_TOKEN` | *(empty)* | Shared secret for `GET /export`. **Endpoint disabled while empty** |
| `DASHBOARD_TOKEN` | *(empty)* | Shared secret for the [web dashboard](./dashboard). **Disabled while empty** |
| `GOOGLE_CALENDAR_OOO` | `false` | Auto-mark participants away when their Google Calendar has an *Out of office* event (needs [domain-wide delegation](./google-chat-setup#calendar-ooo)) |
| `LLM_PROVIDER` | *(empty)* | `anthropic` or `openai` — enables [AI summaries](./ai) |
| `LLM_API_KEY` | — | Your LLM provider API key |
| `LLM_MODEL` | `claude-opus-4-7` (anthropic) | Model override; required for openai |

## Dashboard settings (stored in the database)

Open `https://<your-host>/dashboard?token=<DASHBOARD_TOKEN>` → **Settings**:

| Setting | What it does |
| --- | --- |
| GCP project number | Verifies incoming webhooks are signed by Google Chat |
| Service-account key (JSON) | Paste the downloaded key file — used for Chat API calls and Calendar OOO. Empty = [Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials) (e.g. Cloud Run service identity) |
| AI provider / API key / model | Bring-your-own-key [AI summaries](./ai) |
| Default timezone | Assigned to newly created standups |
| Calendar OOO sync | Auto-mark people away on out-of-office days |
| Scheduler tick token | Authorizes `POST /tick` for external cron |
| CSV export token | Enables `GET /export` (off until generated) |

Secrets are write-only: the UI shows *that* they're configured (and e.g. the
service account's email), never the material itself.

## Endpoints

| Endpoint | Purpose |
| --- | --- |
| `POST /chat/events` | Google Chat webhook — point the Chat app here |
| `POST /tick` | Manually advance the scheduler (for external cron). Requires `Authorization: Bearer $TICK_TOKEN` when set |
| `GET /export?standupId=N&days=30` | CSV download of submissions (long format: one row per answer). Requires `Authorization: Bearer $EXPORT_TOKEN`; disabled when unset |
| `GET /dashboard` | [Web dashboard](./dashboard) — config + history. Requires `DASHBOARD_TOKEN`; disabled when unset |
| `GET /healthz` | Liveness check |
| `POST /tick` | Manually advance the scheduler (for external cron). Requires `Authorization: Bearer <tick token>` when one is set |
| `GET /export?standupId=N&days=30` | CSV download (long format). Requires the export token; disabled until one is generated |
| `GET /dashboard` | [Web dashboard](./dashboard) — settings, config, history |
| `GET /healthz` | Liveness check (pings the database) |

## Data

All state — standups, participants, admins, runs, submissions, blockers, and
the DM-space cache — lives either in a single SQLite file (`DB_PATH`, the
default) or in your own PostgreSQL (`DATABASE_URL`). Back up the file or use
your database's backup story; the process can restart at any time without
losing or double-sending prompts (graceful shutdown on SIGTERM included).
Schema migrations run automatically on startup in both modes, so upgrading
AsyncUp is just deploying the new image.
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`). Back up the file or use your database's
backup story; stored secrets are encrypted, so backups are safe to ship
off-box as long as `SECRET_KEY` stays out of them. Schema migrations run
automatically on startup in both modes, so upgrading AsyncUp is just
deploying the new image. Graceful shutdown on SIGTERM included.
7 changes: 7 additions & 0 deletions docs/guide/dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ doesn't need the query parameter.

## What's there

- **First-run checklist** — a setup meter (connect Google Chat, create a
standup, add your team, optional AI) that disappears once you're rolling.
- **Settings** — *all app configuration lives here*: Google Chat connection
(project number + paste-in service-account key), AI provider and key,
default timezone, Calendar OOO sync, and the machine tokens for `/tick`
and `/export` (generate/clear; shown exactly once). Secrets are stored
encrypted and never echoed back.
- **Standup list** — every standup with schedule and today's progress.
- **Standup detail** — edit name, times, timezone, days, reminder, questions,
and toggles (mood / anonymous mood / digest / AI / escalation threshold);
Expand Down
12 changes: 6 additions & 6 deletions docs/guide/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ one-liner (`docker build -t asyncup .`) if you prefer auditing what you run.
## Docker Compose (simplest)

```bash
cp .env.example .env # set GOOGLE_CHAT_AUDIENCE, credentials path
# uncomment the service-account.json mount in docker-compose.yml
cp .env.example .env # set DASHBOARD_TOKEN + SECRET_KEY
docker compose up -d # pulls the GHCR image by default
# then finish setup in https://<host>/dashboard?token=<DASHBOARD_TOKEN>
```

Put it behind any HTTPS reverse proxy (Caddy, nginx, Traefik) and point the
Expand All @@ -71,8 +71,8 @@ scale on the free tier:
2. The in-process scheduler only runs while an instance is alive, so drive
it externally: create a **Cloud Scheduler** job (free tier: 3 jobs) that
hits `POST /tick` every minute with header
`Authorization: Bearer <TICK_TOKEN>`.
3. Set `TICK_TOKEN` in the service env.
`Authorization: Bearer <tick token>`.
3. Generate the tick token in dashboard → Settings → Access tokens.

Webhook events (dialog opens, submissions, commands) spin the instance up
on demand; `/tick` wakes it for prompts, reminders, and deadlines. Ticks are
Expand All @@ -90,7 +90,7 @@ Run it under systemd or any process manager.

## Production checklist

- [ ] `GOOGLE_CHAT_AUDIENCE` set (request verification on)
- [ ] GCP project number set in dashboard settings (request verification on)
- [ ] HTTPS in front of `/chat/events`
- [ ] `TICK_TOKEN` set if `/tick` is internet-reachable
- [ ] Tick token generated if `/tick` is internet-reachable; `SECRET_KEY` backed up
- [ ] `DB_PATH` on persistent storage, backed up
2 changes: 1 addition & 1 deletion docs/guide/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ the names of mandatory participants who didn't fill it in.
```bash
git clone https://github.com/asyncup-dev/asyncup
cd asyncup
cp .env.example .env # set GOOGLE_CHAT_AUDIENCE + service account key
cp .env.example .env # set DASHBOARD_TOKEN + SECRET_KEY
docker compose up -d
```

Expand Down
33 changes: 20 additions & 13 deletions docs/guide/google-chat-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,39 @@ Workspace admin (for domain-wide install) and have a Google Cloud project.
## 1. Create a GCP project and enable the Chat API

1. Go to [console.cloud.google.com](https://console.cloud.google.com) and create a project (e.g. `asyncup`).
2. Note the **project number** (Dashboard → Project info) — this is your `GOOGLE_CHAT_AUDIENCE`.
2. Note the **project number** (Dashboard → Project info) — you'll paste it into AsyncUp's settings.
3. Enable the API: **APIs & Services → Library → Google Chat API → Enable**.

## 2. Create a service account

1. **IAM & Admin → Service Accounts → Create service account** (e.g. `asyncup`).
No project roles are needed — Chat API access comes from the app configuration.
2. Open the account → **Keys → Add key → JSON**. Download the file as
`service-account.json` next to `docker-compose.yml` (it is gitignored).
2. Open the account → **Keys → Add key → JSON** and download the key file.
You'll paste its contents into the dashboard in step 3 — no file mounting.

## 3. Deploy the bot and get an HTTPS URL

Google Chat must reach your webhook over **public HTTPS**:
## 3. Deploy the bot and connect it

```bash
cp .env.example .env
# set GOOGLE_CHAT_AUDIENCE=<project number>
# set GOOGLE_APPLICATION_CREDENTIALS=/app/service-account.json
# uncomment the service-account.json volume mount in docker-compose.yml
# set DASHBOARD_TOKEN (any long random string)
# set SECRET_KEY (openssl rand -hex 32)
docker compose up -d
```

Put it behind your reverse proxy (Caddy/nginx/Traefik) or, for a quick test,
a tunnel like `cloudflared tunnel --url http://localhost:8080`.
Expose it over **public HTTPS** behind your reverse proxy
(Caddy/nginx/Traefik) or, for a quick test, a tunnel like
`cloudflared tunnel --url http://localhost:8080`.
Your event URL is `https://<your-host>/chat/events`.

Then open `https://<your-host>/dashboard?token=<DASHBOARD_TOKEN>` →
**Settings → Google Chat** and paste:

- the **project number** from step 1
- the **service-account key JSON** from step 2 (stored encrypted)

Changes apply immediately — no restart. (On Cloud Run you can skip the key
and use the service's own identity via Application Default Credentials.)

## 4. Configure the Chat app

**APIs & Services → Google Chat API → Configuration** tab:
Expand Down Expand Up @@ -81,7 +88,7 @@ event, the service account needs **domain-wide delegation**:
- Client ID: the number from step 1
- Scope: `https://www.googleapis.com/auth/calendar.events.readonly`
3. Enable the **Google Calendar API** in your GCP project.
4. Set `GOOGLE_CALENDAR_OOO=true` in your `.env` and restart.
4. Dashboard → **Settings → Workspace** → tick *Google Calendar OOO sync*.

AsyncUp learns each person's email the first time they interact with the bot,
then checks their primary calendar for OOO events when a run opens. People who
Expand All @@ -90,6 +97,6 @@ are OOO are listed as 🏖️ away — never as missing.
## Troubleshooting

- **"No DM space with users/…"** in logs → that user doesn't have the app installed; see step 5.
- **401 on events** → `GOOGLE_CHAT_AUDIENCE` must be the project *number*, not the project ID.
- **401 on events** → the value in Settings → Google Chat must be the project *number*, not the project ID.
- **No prompts arriving** → check `docker compose logs`; the scheduler logs every run open/close. Verify the standup `status`, days, and timezone.
- **Replies not threading** → the bot posts with `threadKey`, which threads correctly even if the parent message failed; check the space's history settings.
Loading