Skip to content

Deployment reference ​

AsyncUp is one small container — by default with SQLite inside, so there are no external moving parts at all. Google Chat needs to reach it on a public HTTPS URL.

Looking for a walkthrough? Step 1: Host covers every scenario — VPS, Postgres, Cloud Run, homelab — with a decision matrix and production checklist, and Operations covers upgrades and backups. This page is the building-blocks reference.

Database: embedded or bring your own ​

ModeConfigurationWhen to choose it
Embedded SQLite (default)DB_PATH on a persistent volumeSimplest possible ops: one container, one file to back up. Plenty for any team size AsyncUp serves
Bring your own PostgreSQLDATABASE_URL=postgres://…You already run managed Postgres (RDS, Cloud SQL, Neon, Supabase, …) and want its backups/HA, or your platform has no persistent volumes (e.g. some scale-to-zero setups)
Postgres on the same machinedocker compose --profile postgres up -d + DATABASE_URL=postgres://asyncup:…@postgres:5432/asyncupPostgres semantics without leaving the box

Setting DATABASE_URL skips SQLite entirely; the schema is created and migrated automatically on startup in both modes. The full test suite runs against both engines in CI on every change.

System requirements ​

AsyncUp idles at a once-a-minute scheduler tick; load is a few webhook calls per person per day. CPU architecture: amd64 or arm64.

SetupMinimumRecommended
App + SQLite1 vCPU (shared is fine), 256 MB RAM, 1 GB disk1 vCPU, 512 MB RAM, 5 GB disk
App + bundled Postgres1 vCPU, 768 MB RAM, 3 GB disk2 vCPU, 2 GB RAM, 10 GB disk
App with external/managed DB1 vCPU, 256 MB RAM1 vCPU, 512 MB RAM

Realistic sizing: the Bun process uses ~100–150 MB RSS; the image is ~300 MB; a year of standups for a 50-person team is well under 100 MB of data. The smallest VPS tier (or Cloud Run's 512 MB default) is comfortably enough — teams into the hundreds of users don't change this picture.

Prebuilt image ​

Multi-arch images (linux/amd64, linux/arm64) are published to GHCR on every merge to main:

bash
docker pull ghcr.io/asyncup-dev/asyncup:latest

Tags: latest (main), vX.Y.Z + X.Y (releases), sha-… (every build) — pin a digest for reproducible deploys. Every image carries the commit it was built from in the org.opencontainers.image.revision label. Building from source stays a one-liner (docker build -t asyncup .) if you prefer auditing what you run.

Docker Compose (simplest) ​

bash
cp .env.example .env       # set OPERATOR_TOKEN + SECRET_KEY
docker compose up -d       # pulls the GHCR image by default
# then finish setup in https://HOST/app (sign in with the operator token)

The repo's compose file publishes port 8080 on 127.0.0.1 only, so a reverse proxy on the same machine reaches it and the internet doesn't. Don't widen it to every interface: Docker's port publishing bypasses host firewalls such as ufw. Path A publishes nothing but Caddy. Put it behind any HTTPS reverse proxy (Caddy, nginx, Traefik) and point the Chat app at https://HOST/chat/events. The SQLite data lives in the standup-data volume.

A 1-shared-CPU VPS or a free-tier VM is plenty — the bot is idle except for a once-a-minute scheduler tick and your team's submissions.

Scale-to-zero (Cloud Run) ​

Closest thing to "no servers at all", and realistically $0/month at team scale on the free tier:

  1. Deploy the container to Cloud Run (min instances = 0, max instances = 1 — the in-process scheduler must not run twice). Use a managed Postgres via DATABASE_URL (Cloud SQL, or Neon's free tier) — Cloud Run's filesystem is ephemeral and network volumes don't support SQLite locking reliably.
  2. The in-process scheduler only runs while an instance is alive, so drive it externally: create a Cloud Scheduler job (free tier covers it) that hits POST /tick every minute with header Authorization: Bearer <tick token>.
  3. Generate the tick token under Settings › API & tokens.

Webhook events (dialog opens, submissions, commands) spin the instance up on demand; /tick wakes it for prompts, reminders, and deadlines. Ticks are idempotent — overlapping or missed ticks are safe and caught up on the next one.

Bare Bun ​

bash
bun install --frozen-lockfile --production
PORT=8080 DB_PATH=/var/lib/asyncup/standup.db bun src/index.ts

Run it under systemd or any process manager.

Production checklist ​

  • [ ] Guided setup finished at /app/setup: project number and service-account key verified, the events URL saved as an audience, first signed Chat event received
  • [ ] HTTPS in front of /chat/events (Google only delivers to https)
  • [ ] Google or SAML sign-in configured, then the setup token switched off under Settings › Sign-in & SSO
  • [ ] Tick token generated if /tick is internet-reachable; SECRET_KEY backed up
  • [ ] DB_PATH on persistent storage, backed up (or DATABASE_URL pointing at managed Postgres) — Operations

Released under the MIT License.