Skip to content

Commit 24184b1

Browse files
tyler-daneclaude
andauthored
feat: replace ENV with YAML config (#1755)
* wip: initial commit * refactor(config): move compass.yaml to repo root and use typed config directly * refactor(self-host): inline web build config and simplify installer * fix(e2e): write playwright compass config at config-load time The e2e webServer (dev.ts) now calls loadCompassConfig() instead of reading process.env directly. Write a minimal .playwright-compass.yaml at playwright.config.ts import time and pass its path via COMPASS_CONFIG_FILE so the webServer finds it without a compass.yaml in the repo root. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(config): correct ENV_FILE → CONFIG_FILE and tighten timezone schema Two bugs caught in code review: 1. validate_existing_env_secrets() guarded on \$ENV_FILE which was removed in the env-to-yaml migration. The guard always returned early, silently skipping all secret placeholder checks on re-installs. Changed to \$CONFIG_FILE. 2. CompassConfigSchema accepted any string for runtime.timezone but the backend EnvSchema enforced z.enum(["Etc/UTC","UTC"]). The mismatch produced a confusing TZ enum error at backend startup instead of a clear parse error at YAML load time. Moved the enum constraint into the shared schema so both web and backend catch it early. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(e2e): include ports section in playwright test config dev.ts reads WEB_PORT from config.ports?.web (falls back to 9080), so the test compass.yaml must declare ports.web to match the port Playwright waits on. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(scripts): replace getCliConfig() with module-level cliConfig Load and parse compass.yaml once at module init instead of re-reading the file on every call. Callers now access typed fields directly (cliConfig.urls.backendApi, etc.). DEV_BROWSER remains a plain process.env read since it is not part of the config schema. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(ci): build staging compass.yaml from individual secrets/vars Replaces the COMPASS_YAML blob secret with granular GitHub vars (non-sensitive URLs, client IDs) and secrets (credentials, tokens). The printf pattern mirrors Dockerfile.web — each field is a separate auditable/rotatable value. * fix(deploy): correct environment variable assignments for staging deployment * docs: remove migration note from local-development.md The .env.local → compass.yaml migration steps belong in the PR description, not the long-term developer doc. The doc now covers steady-state setup only. * chore: cleanup configs * refactor(config): reorganize YAML into web/backend containers Move scattered fields into logical containers: - web.port (was ports.web) - backend.port, backend.apiUrl, backend.originsAllowed, backend.compassToken (were ports.backend, urls.backendApi, urls.cors, tokens.compassSync) - tokens section now only holds googleCalendarNotification (optional) Replace getGcalWebhookBaseURL() utility with a schema-level field: GCAL_WEBHOOK_BASEURL is now always set during config parsing (googleWebhook ?? backendApiUrl), making the fallback explicit and removing the need for a lazy accessor function. Also fixes two pre-existing bugs found during the refactor: - google-import.service.ts imported from a non-existent api-base-url.util - gcal.service.test.ts mocked that same phantom module; now mocks CONFIG directly - google.oauth.client, supertokens.middleware, config.controller imported isGoogleConfigured from config.constants instead of config.util Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(tests): update env.constants → config.constants across test files - Replace all imports of ENV from env.constants with CONFIG as ENV from config.constants (compatible alias, no test logic changed) - Fix google-watch-token.test.ts: mock config.constants instead of the now-removed env.constants module - Export parseConfigFromEnv (was private parseConfig) for test access - Rewrite config.constants.test.ts to match current behavior: - GCAL_WEBHOOK_BASEURL now falls back to BASEURL instead of undefined - Non-HTTPS webhook URLs are accepted (HTTPS check is in superRefine) - Remove getApiBaseURL tests (function no longer exists) - Import isGoogleConfigured from config.util - Fix migration/seeder import paths for env.constants → config.constants Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(test): replace ENV with CONFIG * refactor(config): dissolve urls section into web and backend containers Move urls.frontend → web.url, urls.googleWebhook → backend.googleWebhook, urls.health → backend.healthUrl. The urls group no longer exists; all URL fields now live under the container they belong to. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(config): remove unused backend.healthUrl field The field was only read by the self-host shell scripts as an optional override for the health probe URL; the backend never consumed it at runtime. The default (http://localhost:<port>/api/health) is sufficient. Also fix stale urls.frontend / urls.health / ports.backend references in the self-host/compass helper. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(config): move googleWebhook and notificationToken under google section tokens.googleCalendarNotification → google.notificationToken backend.googleWebhook → google.webhookUrl Both fields are only relevant when Google is configured, so they belong under the google group. Removes the now-empty tokens section entirely. Also brings packages/backend/compass.yaml up to date with the current schema (it still used ports/urls/tokens from before earlier migrations). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: remove github section from config.md * fix(config): walk up directory tree when searching for compass.yaml Scripts run from a subdirectory (e.g. bun dev:web cd-s into packages/web) would fail to find compass.yaml at the repo root because the old lookup only checked process.cwd() exactly. Walking up mirrors how dotenv and similar tools discover config files regardless of invocation directory. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(config): fail fast on REPLACE_WITH_* placeholder values at startup Standardise example YAML placeholders to REPLACE_WITH_* (uppercase + underscore) and add a recursive scanner in parseCompassConfigText that fires after Zod validation. Any string value containing REPLACE_WITH_ — including values embedded inside URIs — is collected and thrown as a single error listing every affected field path. Since all entry points (backend, scripts, web dev) call parseCompassConfigText, the guard fires everywhere. The compass-self-host-placeholder Google values intentionally do not use this prefix so leaving Google unconfigured stays valid. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: add comments for compass token * docs: update example config and doc * refactor(config): remove Google OAuth placeholder constants in favor of empty/undefined values * API Error: 529 {"type":"error","error":{"type":"overloaded_error","message":"Overloaded"},"request_id":"req_011Cb3RMWeJgeK1XQGWyx1LF"} * fix(self-host): use localhost URLs by default and prevent config loss with existing volumes * fix(self-host): simplify installer check by removing gcal check no need to overcomplicate with a standalone function for one key * fix(backend): initialize mongo after connect resolves * test: cleanup playwright config --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 77340ab commit 24184b1

81 files changed

Lines changed: 1317 additions & 937 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/deploy-staging.yml

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,47 @@ jobs:
2525
steps:
2626
- name: Deploy release to staging
2727
env:
28+
# Non-sensitive
29+
BACKEND_API_URL: ${{ vars.STAGING_BACKEND_API_URL }}
30+
FRONTEND_URL: ${{ vars.STAGING_FRONTEND_URL }}
31+
GOOGLE_CLIENT_ID: ${{ vars.STAGING_GOOGLE_CLIENT_ID }}
2832
RELEASE_TAG: ${{ inputs.tag }}
33+
SSH_USER: ${{ vars.STAGING_SSH_USER }}
34+
SSH_HOST: ${{ vars.STAGING_SSH_HOST }}
35+
SUPERTOKENS_URI: ${{ vars.STAGING_SUPERTOKENS_URI }}
36+
# Sensitive
37+
COMPASS_SYNC_TOKEN: ${{ secrets.STAGING_COMPASS_SYNC_TOKEN }}
38+
GCAL_NOTIFICATION_TOKEN: ${{ secrets.STAGING_GCAL_NOTIFICATION_TOKEN }}
39+
GOOGLE_CLIENT_SECRET: ${{ secrets.STAGING_GOOGLE_CLIENT_SECRET }}
40+
MONGO_URI: ${{ secrets.STAGING_MONGO_URI }}
2941
SSH_KEY: ${{ secrets.STAGING_SSH_KEY }}
30-
SSH_HOST: ${{ secrets.STAGING_SSH_HOST }}
31-
SSH_USER: ${{ secrets.STAGING_SSH_USER }}
42+
SUPERTOKENS_KEY: ${{ secrets.STAGING_SUPERTOKENS_KEY }}
3243
run: |
3344
echo "Deploying Compass ${RELEASE_TAG} to staging"
3445
mkdir -p ~/.ssh
3546
echo "$SSH_KEY" > ~/.ssh/staging_key
3647
chmod 600 ~/.ssh/staging_key
3748
ssh-keyscan -H "$SSH_HOST" >> ~/.ssh/known_hosts
49+
printf '%s\n' \
50+
'runtime:' \
51+
' nodeEnv: production' \
52+
' timezone: Etc/UTC' \
53+
'web:' \
54+
" url: \"${FRONTEND_URL}\"" \
55+
'backend:' \
56+
" apiUrl: \"${BACKEND_API_URL}\"" \
57+
' originsAllowed:' \
58+
" - \"${FRONTEND_URL}\"" \
59+
" compassToken: \"${COMPASS_SYNC_TOKEN}\"" \
60+
'mongo:' \
61+
" uri: \"${MONGO_URI}\"" \
62+
'supertokens:' \
63+
" uri: \"${SUPERTOKENS_URI}\"" \
64+
" key: \"${SUPERTOKENS_KEY}\"" \
65+
'google:' \
66+
" clientId: \"${GOOGLE_CLIENT_ID}\"" \
67+
" clientSecret: \"${GOOGLE_CLIENT_SECRET}\"" \
68+
" notificationToken: \"${GCAL_NOTIFICATION_TOKEN}\"" \
69+
| ssh -i ~/.ssh/staging_key "$SSH_USER@$SSH_HOST" \
70+
"umask 077 && mkdir -p ~/compass && cat > ~/compass/compass.yaml"
3871
ssh -i ~/.ssh/staging_key "$SSH_USER@$SSH_HOST" "cd ~/compass && ./compass update"

.github/workflows/publish-docker-images.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,10 @@ jobs:
8383
context: .
8484
file: self-host/Dockerfile.web
8585
push: true
86-
# BASEURL and GOOGLE_CLIENT_ID are baked into the web bundle at build time.
86+
# backend.apiUrl and google.clientId are baked into the web bundle at build time.
8787
# The published image ships with localhost defaults, which work for local installs.
8888
# Users who need a custom API domain or real Google credentials must rebuild
89-
# the web image locally using the build: blocks in docker-compose.yml.
89+
# the web image locally using the build: blocks in compose.yaml.
9090
build-args: |
9191
BASEURL=http://localhost:3000/api
9292
GOOGLE_CLIENT_ID=compass-self-host-placeholder.apps.googleusercontent.com

.github/workflows/test-unit.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,6 @@ jobs:
7474
7575
- name: Run ${{ matrix.project }} tests
7676
env:
77-
TZ: ${{ vars.TZ }}
77+
TZ: Etc/UTC
7878
run: |
7979
bun run test:${{ matrix.project }}

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ credentials.txt
1414
*.mongodb
1515
*.tsbuildinfo
1616
*.mjs
17+
compass.yaml
18+
.playwright-compass.yaml
1719

1820
########
1921
# DIRS #
@@ -55,4 +57,4 @@ packages/backend/.prod.env
5557

5658

5759
tmp/
58-
!.env.local.example
60+
!compass.example.yaml

AGENTS.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55
- Frontend-only work usually starts with `bun run dev:web`; it does not require
66
backend services.
77
- Backend, auth, MongoDB, Google sync, and SSE work require
8-
`packages/backend/.env.local`. Bootstrap with:
8+
a `compass.yaml` at the repo root. Bootstrap with:
99

1010
```bash
11-
cp packages/backend/.env.local.example packages/backend/.env.local
11+
cp compass.example.yaml compass.yaml
1212
```
1313

14+
- `compass.yaml` contains secrets. Do not commit it.
15+
1416
- Avoid defaulting to `bun run test`; use the focused package test first.
1517
- Formatting is handled by the repo-local Codex Stop hook after each agent turn.
1618
- Use `bun run lint` and relevant verification before push or handoff.

bun.lock

Lines changed: 7 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

compass.example.yaml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Compass Config
2+
3+
# Notes
4+
# - Copy this file to compass.yaml and replace values for your local setup.
5+
# - Do not commit this file; it contains secrets.
6+
# - For detailed explanations, see: https://docs.compasscalendar.com/docs/config
7+
8+
compose:
9+
version: latest
10+
11+
runtime:
12+
nodeEnv: development
13+
timezone: Etc/UTC
14+
logLevel: debug
15+
16+
web:
17+
port: 9080
18+
url: http://localhost:9080
19+
20+
backend:
21+
port: 3000
22+
apiUrl: http://localhost:3000/api
23+
originsAllowed:
24+
- http://localhost:3000
25+
- http://localhost:9080
26+
compassToken: REPLACE_WITH_COMPASS_TOKEN # any string will do
27+
28+
mongo:
29+
username: compass
30+
password: local-mongo-password
31+
replicaSetKey: local-mongo-replica-set-key
32+
uri: mongodb+srv://admin:REPLACE_WITH_MONGO_PASSWORD@cluster0.m99yy.mongodb.net/dev_calendar?authSource=admin&retryWrites=true&w=majority&tls=true
33+
34+
supertokens:
35+
uri: REPLACE_WITH_SUPERTOKENS_URI
36+
key: REPLACE_WITH_SUPERTOKENS_KEY
37+
38+
# google:
39+
# clientId: REPLACE_WITH_GOOGLE_CLIENT_ID # e.g. your-id.apps.googleusercontent.com
40+
# clientSecret: REPLACE_WITH_GOOGLE_CLIENT_SECRET
41+
# channelExpirationMin: 10
42+
# webhookUrl: REPLACE_WITH_GOOGLE_WEBHOOK_URL # e.g. https://example.trycloudflare.com/api
43+
# notificationToken: REPLACE_WITH_NOTIFICATION_TOKEN
44+
45+
# email:
46+
# kitApiSecret: REPLACE_WITH_KIT_API_SECRET
47+
# kitUserTagId: REPLACE_WITH_KIT_USER_TAG_ID
48+
49+
# posthog:
50+
# key: REPLACE_WITH_POSTHOG_KEY
51+
# host: REPLACE_WITH_POSTHOG_HOST

docs/CI-CD/cli-and-maintenance-commands.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ Primary file:
1616

1717
Environment loading:
1818

19-
- `bun run cli` loads `packages/backend/.env.local`.
20-
- Keep local development variables in `packages/backend/.env.local` (bootstrap from `.env.local.example`).
19+
- `bun run cli` loads `compass.yaml` from the repo root.
20+
- Keep local development config in `compass.yaml` at the repo root (bootstrap from `compass.example.yaml`).
2121

2222
## CLI URL Resolution Contract
2323

@@ -27,14 +27,14 @@ Primary source:
2727

2828
How API base URLs are resolved:
2929

30-
- local (`--environment local`): returns `BASEURL` directly (trailing slash removed)
31-
- staging/production: derives `https://<domain>/api` from `FRONTEND_URL`
30+
- local (`--environment local`): returns `backend.apiUrl` directly (trailing slash removed)
31+
- staging/production: derives `https://<domain>/api` from `web.url`
3232

3333
Fallback behavior:
3434

35-
- if `FRONTEND_URL` points at `localhost`, CLI prompts for a domain and builds `https://<domain>/api`
36-
- if `FRONTEND_URL` is already a non-localhost URL, CLI uses that hostname directly
37-
- local mode does not prompt for a domain; it depends on `BASEURL`
35+
- if `web.url` points at `localhost`, CLI prompts for a domain and builds `https://<domain>/api`
36+
- if `web.url` is already a non-localhost URL, CLI uses that hostname directly
37+
- local mode does not prompt for a domain; it depends on `backend.apiUrl`
3838

3939
## Commands To Know
4040

docs/CI-CD/workflows.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ publishing images.
7373
Source: [`.github/workflows/deploy-staging.yml`](../../.github/workflows/deploy-staging.yml)
7474

7575
The deploy workflow SSHes into the staging VPS and runs `./compass update`,
76-
which pulls the Docker Hub image tag configured by the staging `.env` file and
76+
which pulls the Docker Hub image tag configured by the staging `compass.yaml` file and
7777
restarts the stack. The workflow accepts a release tag input so the Actions logs
7878
show which release triggered or motivated the deploy.
7979

docs/Config/README.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Compass YAML Configuration
2+
3+
Compass uses `compass.yaml` for self-hosting and local development. The file is visible, diffable, and contains secrets, so keep it out of git and back it up with the Docker volumes.
4+
5+
Examples:
6+
7+
- local development: `packages/backend/compass.example.yaml`
8+
- self-hosting: `self-host/compass.example.yaml`
9+
10+
## Top-Level Sections
11+
12+
| key | Default | Description |
13+
|---|---|---|
14+
| `compose.version` | `latest` | Docker image tag used by the self-host compose stack. Pin this for reproducible installs. |
15+
| `runtime.nodeEnv` | `production` | Runtime mode. Self-hosted installs should use `production`; local development uses `development`. |
16+
| `runtime.timezone` | `Etc/UTC` | Backend timezone. Only `Etc/UTC` and `UTC` are accepted. |
17+
| `runtime.logLevel` | `info` | Winston log level. |
18+
19+
## Web
20+
21+
| key | Required | Description |
22+
|---|---|---|
23+
| `web.port` | `9080` | Host port bound to the web container on `127.0.0.1`. |
24+
| `web.url` | Yes | Public frontend URL as seen by the backend. Example: `https://compass.example.com`. |
25+
26+
## Backend
27+
28+
| key | Required | Description |
29+
|---|---|---|
30+
| `backend.port` | `3000` | Host port bound to the backend container on `127.0.0.1`. |
31+
| `backend.apiUrl` | Yes | Public API URL. Example: `https://compass.example.com/api`. This is baked into the web bundle when the web image is rebuilt. |
32+
| `backend.originsAllowed` | Yes | YAML list of allowed CORS origins. Include `web.url`. |
33+
| `backend.compassToken` | Yes | Bearer token protecting internal sync endpoints. |
34+
35+
## MongoDB
36+
37+
| key | Required | Description |
38+
|---|---|---|
39+
| `mongo.username` | Self-host | MongoDB root username created on first container startup. Must match `mongo.uri`. |
40+
| `mongo.password` | Self-host | MongoDB root password. Changing it after first startup requires a MongoDB user migration. |
41+
| `mongo.replicaSetKey` | Self-host | MongoDB replica set key for the single-node `rs0` replica set. |
42+
| `mongo.uri` | Yes | Backend MongoDB connection string. Self-hosted installs must include `authSource=admin` and `replicaSet=rs0`. |
43+
44+
## SuperTokens
45+
46+
SuperTokens handles user-sessions for us.
47+
48+
| key | Required | Description |
49+
|---|---|---|
50+
| `supertokens.uri` | Yes | SuperTokens Core URL as seen by the backend. Self-hosted Docker uses `http://supertokens:3567`. |
51+
| `supertokens.key` | Yes | API key shared by backend and SuperTokens Core. |
52+
| `supertokens.postgres.user` | Self-host | Postgres user for the SuperTokens database container. |
53+
| `supertokens.postgres.password` | Self-host | Postgres password for the SuperTokens database container. |
54+
| `supertokens.postgres.database` | Self-host | Postgres database name for SuperTokens. |
55+
56+
## Google
57+
These values are only necessary if you want to enable Google Oauth and/or 2-way sync between Compass and Google Calendar
58+
59+
Both `google.clientId` and `google.clientSecret` must be real values for Google features to activate. Setting only one causes backend startup to fail.
60+
61+
| key | Required | Description |
62+
|---|---|---|
63+
| `google.clientId` | No | Google OAuth client ID. Rebuild the web image after changing it. |
64+
| `google.clientSecret` | No | Google OAuth client secret. Backend-only. |
65+
| `google.channelExpirationMin` | No | Google Calendar watch channel lifetime in minutes. |
66+
| `google.webhookUrl` | No | Public HTTPS API URL for Google Calendar push notifications. When omitted, Compass uses `backend.apiUrl`. |
67+
| `google.notificationToken` | Required for HTTPS Google webhooks | Token used to verify Google Calendar webhook requests. |
68+
69+
See [Google Calendar](./google-calendar.md) for full setup instructions.
70+
71+
## Optional Integrations
72+
73+
| key | Required | Description |
74+
|---|---|---|
75+
| `email.kitApiSecret` | No | Kit.com API secret key. |
76+
| `email.kitUserTagId` | No | Kit.com tag ID applied to users on signup. |
77+
| `posthog.key` | No | PostHog project key injected into the web bundle. |
78+
| `posthog.host` | No | PostHog host injected into the web bundle. |

0 commit comments

Comments
 (0)