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
| Backend | Selected by | Use 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
| Variable | Description | Default |
|---|---|---|
PORT | Port the server listens on | 4000 |
DATABASE_URL | Postgres connection string; when set, uses the Postgres driver | โ |
PGLITE_DATA_DIR | Path to embedded-database files; used when DATABASE_URL is unset | โ |
NODE_ENV | Set to production in containers | development |
APP_URL | Public URL of the app, used in magic-link emails | derived from request |
JWT_SECRET | Secret for signing session tokens | auto-generated |
MAGIC_LINK_SECRET | Secret for signing magic-link tokens | auto-generated |
EXPOSE_MAGIC_LINK | Return the magic link in the API response instead of emailing it โ trusted networks only | off outside prod |
BYPASS_AUTH_UUID | A 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.
- In the app, create an API key with the
readscope. The token (acal_โฆ) is shown once โ copy it. - 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_SECRETandMAGIC_LINK_SECRETso tokens survive restarts, and change the Postgres password from the default. EXPOSE_MAGIC_LINKandBYPASS_AUTH_UUIDare 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
secretlike 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.