Self-hosting Auto Cal

Auto Cal ships as a single Docker image that serves both the web client and the GraphQL API. Bring a database (embedded PGLite for a demo, or Postgres for real use) and you're running in a couple of minutes. Everything below assumes Docker; see the README for bare-Node development.

Requirements

  • Docker with Compose v2 (docker compose). That's it for the container path.
  • ~256 MB RAM is plenty for a personal instance. Postgres adds a little more.
  • For local development instead of Docker: Node.js 22+ and npm โ€” see the README.

Quickstart

Clone the repo and start one of the Compose files. The web app and API are served together on port 4000.

Embedded database simplest

PGLite stores everything in a Docker volume alongside the app โ€” no separate database container.

git clone https://github.com/cubicecho/auto-cal && cd auto-cal
docker compose -f docker-compose.pglite.yml up -d

Open http://localhost:4000. Data persists in the named volume auto-cal-data.

PostgreSQL recommended

Runs a standalone Postgres you can back up independently โ€” and avoids PGLite's idle-CPU cost.

docker compose -f docker-compose.postgres.yml up -d

Open http://localhost:4000. Migrations run automatically on startup, so there's no separate migrate step.

โš ๏ธ Change the default password before exposing this anywhere. Edit POSTGRES_PASSWORD and the matching DATABASE_URL in docker-compose.postgres.yml.

Choosing a database

BackendSelected byUse for
Postgres DATABASE_URL is set Any real deployment. No idle CPU, back up independently.
PGLite PGLITE_DATA_DIR set, no DATABASE_URL Local dev and single-user demos only.

The runtime picks the backend from the environment: if DATABASE_URL is present it uses the postgres.js driver, otherwise it falls back to PGLite at PGLITE_DATA_DIR.

PGLite compiles Postgres to WebAssembly and busy-waits its event loop, so it burns measurable CPU even when idle. Fine for a laptop demo; use real Postgres for anything long-running. The server logs a warning if PGLite is used with NODE_ENV=production.

Prebuilt image no clone

Every release is published to Docker Hub (vantreeseba/auto-cal) and GHCR (ghcr.io/cubicecho/auto-cal), tagged latest and the semver version. Run the embedded-database variant straight from the registry:

docker run -d -p 4000:4000 -v ./data:/app/pgdata \
  vantreeseba/auto-cal:latest

The image already sets PGLITE_DATA_DIR=/app/pgdata, so the bind mount is all you need for persistence. For Postgres, pass -e DATABASE_URL=โ€ฆ instead (or use the Compose file).

Environment variables

VariableDescriptionDefault
PORTPort the server listens on4000
DATABASE_URLPostgres connection string; when set, uses the Postgres driverโ€”
PGLITE_DATA_DIRPath to embedded-database files; used when DATABASE_URL is unsetโ€”
NODE_ENVSet to production in containersdevelopment
APP_URLPublic URL of the app, used in magic-link emailsderived from request
JWT_SECRETSecret for signing session tokensauto-generated
MAGIC_LINK_SECRETSecret for signing magic-link tokensauto-generated
EXPOSE_MAGIC_LINKReturn the magic link in the API response instead of emailing it โ€” trusted networks onlyoff outside prod
BYPASS_AUTH_UUIDA user UUID accepted directly as a Bearer token โ€” trusted networks onlyโ€”

Set JWT_SECRET and MAGIC_LINK_SECRET explicitly in production. If they're auto-generated, they change on every restart and invalidate existing sessions and pending links.

Logging in

Auto Cal is passwordless. You enter your email, receive a magic link, and clicking it signs you in โ€” the session token is a JWT stored in the browser.

Without an email provider configured, set EXPOSE_MAGIC_LINK=true and the link is returned directly in the requestMagicLink API response so you can copy it and sign in. This is intended for local or secure-network deployments only. For a single trusted user you can instead set BYPASS_AUTH_UUID to that user's UUID and use the UUID as a Bearer token.

Calendar feed

Auto Cal exposes your computed schedule as a read-only iCal feed, so you can see it inside Google Calendar, Apple Calendar, or any client that subscribes to an .ics URL.

  1. In the app, create an API key with the read scope. The token (acal_โ€ฆ) is shown once โ€” copy it.
  2. Subscribe your calendar to the feed URL, passing the key as secret:
# Your scheduled todos + habits (next two weeks)
http://your-host:4000/ical?secret=acal_your_key_here

# Your recurring time blocks instead
http://your-host:4000/ical?secret=acal_your_key_here&view=blocks

The feed is scoped to the user who owns the key and requires the read scope; revoked or expired keys stop working immediately.

Updating

Pull the newest image and recreate the container. Migrations run automatically on startup.

docker compose -f docker-compose.postgres.yml pull
docker compose -f docker-compose.postgres.yml up -d

Your data lives in the Postgres volume (or the PGLite volume), so it survives the recreate. Back the volume up before a major upgrade.

Security notes

  • Don't expose the container directly to the internet. It speaks plain HTTP โ€” put it behind a TLS-terminating reverse proxy, or bind it to localhost / a private network.
  • Set your own secrets. Provide JWT_SECRET and MAGIC_LINK_SECRET so tokens survive restarts, and change the Postgres password from the default.
  • EXPOSE_MAGIC_LINK and BYPASS_AUTH_UUID are trusted-network escape hatches. Either one lets someone sign in without receiving an email โ€” never enable them on a public deployment.
  • Treat API keys and the iCal secret like passwords. Only the SHA-256 hash is stored, the token is shown once, and anyone with the feed URL can read that user's schedule. Revoke keys you no longer use.

Read the full README โ†—