TIM-46: drop Cloudflare, run as Node service in Docker Compose#72
Conversation
Stage 1b of the full-Docker rewrite (TIM-45 / TIM-46). Replaces
@sveltejs/adapter-cloudflare with @sveltejs/adapter-node so the same
binary serves both docker compose and the future Helm chart.
- adapter: swap @sveltejs/adapter-cloudflare -> @sveltejs/adapter-node
(out: 'build', precompress: false).
- build: 'node build/index.js' is now the entry; PORT/HOST env vars are
honoured by the adapter.
- scripts: drop the Cloudflare 'pnpm deploy' script; add 'pnpm start'.
- deps: remove wrangler (now unused).
- files: delete wrangler.toml.
- Dockerfile: multi-stage on node:24-alpine, corepack-pinned pnpm@10.30.3,
/pnpm/store BuildKit cache mount, 'pnpm prune --prod' after build,
non-root 'node' user, HEALTHCHECK hitting /healthz via wget.
- .dockerignore: trim node_modules, .svelte-kit, build artefacts,
env files, secrets, tests, docs, source control.
- docker-compose.yml: app only (env_file, HOST/PORT overrides, restart,
published port).
- docker-compose.postgres.yml: Docker Compose v2.20+ 'include:' overlay
that adds a bundled postgres:16-alpine with pg_isready healthcheck and
a named 'pgdata' volume. Brings in 'depends_on: postgres healthy' on
the app service.
- /healthz: unauthenticated, no-DB liveness/readiness endpoint returning
{ ok: true } (Kubernetes/Compose probe-friendly).
- .env.example: rewrite to list every env var the app reads
(DATABASE_URL, BETTER_AUTH_*, GITHUB_*, PUBLIC_APP_URL, MISTRAL_API_KEY,
HOST/PORT) with two commented DATABASE_URL forms for bundled vs
external Postgres; Supabase vars kept and marked DEPRECATED (Stage 4
will remove them).
Verification:
- pnpm check: 0 errors (1 pre-existing svelte warning unrelated).
- pnpm lint: clean.
- pnpm test: 148/148 pass.
- pnpm build: produces a Node-runnable bundle in build/.
- docker build .: succeeds.
- docker compose -f docker-compose.yml -f docker-compose.postgres.yml up:
both services healthy, curl http://localhost:3000/healthz -> 200 with
{"ok":true}.
Non-goals respected: Supabase wiring (locals.supabase.*) untouched.
README, AGENTS.md, CONTRIBUTING.md, .cursor/rules and the deploy GH
workflow will be reworked in Stage 4.
📝 WalkthroughWalkthroughMigrates the SvelteKit app from Cloudflare Pages ( ChangesNode Adapter and Docker Deployment
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Suggested labels
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #72 +/- ##
=======================================
Coverage 88.89% 88.89%
=======================================
Files 6 6
Lines 1369 1369
Branches 309 309
=======================================
Hits 1217 1217
Misses 152 152 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
.env.example (1)
66-74: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAdd trailing newline at end of file.
The file is missing a final newline. This is a minor formatting fix to satisfy POSIX text file conventions and quiet dotenv-linter.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.env.example around lines 66 - 74, The .env.example file is missing a final trailing newline, so update the file ending to include one. Keep the existing Supabase env entries unchanged and ensure the file terminates cleanly to satisfy dotenv-linter and POSIX text conventions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.env.example:
- Around line 1-74: Add the missing AI_OPTIMIZATION_MODEL placeholder to
.env.example so it matches src/lib/server/config/app-config.ts and the README.
Place it near the other app/server configuration variables, using a clear
commented example value so local setup users know to set the override
consistently.
In `@docker-compose.yml`:
- Around line 29-36: The container’s internal listen port is being changed by
the PORT environment override while the service mapping and health probe still
assume port 3000, so make the app listen on a fixed container port unless the
whole stack is updated together. In docker-compose.yml, keep PORT aligned with
the published container port or stop using it to control the in-container bind
port, and ensure the Dockerfile health check/probe for the app matches the same
fixed port used by the service.
In `@Dockerfile`:
- Around line 32-38: Remove the placeholder PUBLIC_* ENV block from the
Dockerfile and stop relying on build-time inlined public env values; the issue
is that these values are baked into the SvelteKit bundle via $env/static/public,
so runtime overrides cannot replace them. Update the Supabase/public env usage
in the relevant app code to use $env/dynamic/public where appropriate, or
otherwise ensure the build does not require placeholder values in the image. Use
the Dockerfile public env section and the SvelteKit env imports as the key
places to adjust.
In `@svelte.config.js`:
- Around line 1-8: The Svelte config has been switched from the Cloudflare Pages
adapter to the Node adapter, which changes the deployment model. Revert the
adapter change in config and keep using the existing Cloudflare Pages setup by
preserving the adapter configuration in svelte.config.js, ensuring the
deployment contract remains unchanged.
---
Nitpick comments:
In @.env.example:
- Around line 66-74: The .env.example file is missing a final trailing newline,
so update the file ending to include one. Keep the existing Supabase env entries
unchanged and ensure the file terminates cleanly to satisfy dotenv-linter and
POSIX text conventions.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 53fb3b4a-7c1f-4e4a-a1dd-e9643443721e
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (9)
.dockerignore.env.exampleDockerfiledocker-compose.postgres.ymldocker-compose.ymlpackage.jsonsrc/routes/healthz/+server.tssvelte.config.jswrangler.toml
💤 Files with no reviewable changes (1)
- wrangler.toml
| # ============================================================================= | ||
| # DATABASE — pick ONE of the two blocks below and delete the other. | ||
| # ============================================================================= | ||
|
|
||
| # --- (A) Bundled Postgres (docker compose with the postgres overlay) ------- | ||
| # Use this when `docker compose -f docker-compose.yml -f docker-compose.postgres.yml up`. | ||
| # The hostname `postgres` is the service name in docker-compose.postgres.yml. | ||
| # POSTGRES_PASSWORD is also defined below for the postgres container itself. | ||
| DATABASE_URL=postgres://workflow_metrics:${POSTGRES_PASSWORD}@postgres:5432/workflow_metrics | ||
|
|
||
| # --- (B) External Postgres (managed, K8s, or your own instance) ----------- | ||
| # Uncomment and replace the placeholders. sslmode=require is recommended for | ||
| # managed Postgres providers (Neon, RDS, Crunchy Bridge, Supabase pooled, …). | ||
| # DATABASE_URL=postgres://workflow_metrics:CHANGE_ME@db.example.com:5432/workflow_metrics?sslmode=require | ||
|
|
||
| # Required only when using bundled mode (block A). The compose overlay reads | ||
| # this to provision the postgres container. | ||
| POSTGRES_PASSWORD=change-me-local-dev-password | ||
| # Optional — defaults are workflow_metrics / workflow_metrics if unset. | ||
| # POSTGRES_DB=workflow_metrics | ||
| # POSTGRES_USER=workflow_metrics | ||
|
|
||
| # ============================================================================= | ||
| # BETTER AUTH — Stage 2a wires these. Listed here so docker compose / Helm | ||
| # can inject them now without a second round of edits. | ||
| # Generate BETTER_AUTH_SECRET with: openssl rand -base64 32 | ||
| # ============================================================================= | ||
| BETTER_AUTH_SECRET=change-me-run-openssl-rand-base64-32 | ||
| # The public URL where the app is reachable. Used as the OAuth callback base. | ||
| BETTER_AUTH_URL=http://localhost:3000 | ||
|
|
||
| # ============================================================================= | ||
| # GITHUB OAUTH (login) | ||
| # Create an OAuth App at https://github.com/settings/applications/new. | ||
| # Authorization callback URL (after Better Auth lands) = ${BETTER_AUTH_URL}/api/auth/callback/github | ||
| # ============================================================================= | ||
| GITHUB_CLIENT_ID=your-github-oauth-app-client-id | ||
| GITHUB_CLIENT_SECRET=your-github-oauth-app-client-secret | ||
|
|
||
| # ============================================================================= | ||
| # GITHUB APP (Apply as PR) | ||
| # Separate from the OAuth App above. Create at https://github.com/settings/apps/new. | ||
| # Repository permissions: Contents (R/W), Pull requests (R/W), Workflows (R/W), Actions (R). | ||
| # ============================================================================= | ||
| GITHUB_APP_ID=your-github-app-id | ||
| # Multi-line PEM is fine. Or collapse newlines to literal \n for single-line secrets. | ||
| GITHUB_APP_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----" | ||
| GITHUB_APP_SLUG=your-github-app-slug | ||
|
|
||
| # ============================================================================= | ||
| # APP | ||
| # ============================================================================= | ||
| # Production URL of the app (no trailing slash). OAuth redirects back here. | ||
| PUBLIC_APP_URL=http://localhost:3000 | ||
| # Node adapter binds to HOST:PORT (defaults 0.0.0.0:3000). Override per-env here. | ||
| HOST=0.0.0.0 | ||
| PORT=3000 | ||
|
|
||
| # ============================================================================= | ||
| # MISTRAL AI (optional) | ||
| # Per-user keys are entered in Settings and stored in the DB. This server-wide | ||
| # fallback is used when no per-user key is set (e.g. demos). | ||
| # ============================================================================= | ||
| # MISTRAL_API_KEY=your-mistral-api-key | ||
|
|
||
| # ============================================================================= | ||
| # DEPRECATED — Supabase | ||
| # The app still calls `locals.supabase.*`. These are required until Stage 4 | ||
| # removes the Supabase wiring. Do not delete yet. | ||
| # ============================================================================= | ||
| PUBLIC_SUPABASE_URL=https://your-project.supabase.co | ||
| PUBLIC_SUPABASE_ANON_KEY=your-anon-key | ||
| # Server-only; bypasses RLS for cache writes. Never expose to the browser. | ||
| SUPABASE_SERVICE_ROLE_KEY=your-service-role-key No newline at end of file |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Find all env variable accesses in server code and compare with .env.example
# Extract env keys from .env.example (non-comment, non-empty lines)
grep -E '^[A-Z_]+=' .env.example | sed 's/=.*//' | sort -u > /tmp/env_example_keys.txt
# Find all env.* and env['.*'] accesses in server code
rg -nP 'env\.[A-Z_]+|env\[[\'"][A-Z_]+[\'"]\]' src/lib/server src/routes --type=ts | \
grep -oP 'env\.\K[A-Z_]+|env\[\K[\'"]\K[A-Z_]+' | sort -u > /tmp/env_code_keys.txt
echo "=== In code but not in .env.example ==="
comm -23 /tmp/env_code_keys.txt /tmp/env_example_keys.txt
echo "=== In .env.example but maybe not in code ==="
comm -13 /tmp/env_code_keys.txt /tmp/env_example_keys.txtRepository: timoa/workflow-metrics
Length of output: 221
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Files =="
git ls-files .env.example README* src | sed -n '1,200p'
echo
echo "== .env.example keys =="
grep -E '^[A-Z_][A-Z0-9_]*=' .env.example | sed 's/=.*//' | sort -u
echo
echo "== Server env usages (src/lib/server, src/routes) =="
python3 - <<'PY'
import re, pathlib
roots = [pathlib.Path('src/lib/server'), pathlib.Path('src/routes')]
pat = re.compile(r'\benv\.([A-Z_][A-Z0-9_]*)|\benv\[[\'"]([A-Z_][A-Z0-9_]*)[\'"]\]')
keys = set()
for root in roots:
if not root.exists():
continue
for path in root.rglob('*.ts'):
txt = path.read_text(encoding='utf-8', errors='ignore')
for m in pat.finditer(txt):
keys.add(m.group(1) or m.group(2))
for k in sorted(keys):
print(k)
PY
echo
echo "== README env mentions (best effort) =="
rg -n --hidden -S 'BETTER_AUTH_SECRET|BETTER_AUTH_URL|GITHUB_CLIENT_ID|GITHUB_CLIENT_SECRET|GITHUB_APP_ID|GITHUB_APP_PRIVATE_KEY|GITHUB_APP_SLUG|PUBLIC_APP_URL|HOST|PORT|MISTRAL_API_KEY|DATABASE_URL|POSTGRES_PASSWORD|PUBLIC_SUPABASE_URL|PUBLIC_SUPABASE_ANON_KEY|SUPABASE_SERVICE_ROLE_KEY' README* .github docs 2>/dev/null || trueRepository: timoa/workflow-metrics
Length of output: 8278
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Call sites =="
rg -n -C 3 '\b(AI_OPTIMIZATION_MODEL|CACHE)\b' src/lib/server src/routes README.md .github/workflows || true
echo
echo "== README env table area =="
sed -n '150,250p' README.mdRepository: timoa/workflow-metrics
Length of output: 6014
Add AI_OPTIMIZATION_MODEL to .env.example
src/lib/server/config/app-config.ts reads this override, and the README already documents it, but the example file does not. Add a placeholder so local setup stays in sync.
🧰 Tools
🪛 dotenv-linter (4.0.0)
[warning] 56-56: [UnorderedKey] The HOST key should go before the PUBLIC_APP_URL key
(UnorderedKey)
[warning] 57-57: [UnorderedKey] The PORT key should go before the PUBLIC_APP_URL key
(UnorderedKey)
[warning] 72-72: [UnorderedKey] The PUBLIC_SUPABASE_ANON_KEY key should go before the PUBLIC_SUPABASE_URL key
(UnorderedKey)
[warning] 74-74: [EndingBlankLine] No blank line at the end of the file
(EndingBlankLine)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.env.example around lines 1 - 74, Add the missing AI_OPTIMIZATION_MODEL
placeholder to .env.example so it matches src/lib/server/config/app-config.ts
and the README. Place it near the other app/server configuration variables,
using a clear commented example value so local setup users know to set the
override consistently.
Source: Coding guidelines
| environment: | ||
| # Ensure the runtime picks up the right interface and port even if the | ||
| # operator forgets to export them in .env. | ||
| HOST: ${HOST:-0.0.0.0} | ||
| PORT: ${PORT:-3000} | ||
| NODE_ENV: production | ||
| ports: | ||
| - "${PORT:-3000}:3000" |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Keep the container's listen port fixed unless the rest of the stack follows it.
Line 32-L33 can move the app to a different internal port, but Line 36 still publishes container port 3000, and Dockerfile Line 72 probes 3000. Any PORT override makes the service unreachable and unhealthy.
Suggested fix
environment:
# Ensure the runtime picks up the right interface and port even if the
# operator forgets to export them in .env.
HOST: ${HOST:-0.0.0.0}
- PORT: ${PORT:-3000}
+ PORT: 3000
NODE_ENV: production
ports:
- "${PORT:-3000}:3000"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| environment: | |
| # Ensure the runtime picks up the right interface and port even if the | |
| # operator forgets to export them in .env. | |
| HOST: ${HOST:-0.0.0.0} | |
| PORT: ${PORT:-3000} | |
| NODE_ENV: production | |
| ports: | |
| - "${PORT:-3000}:3000" | |
| environment: | |
| # Ensure the runtime picks up the right interface and port even if the | |
| # operator forgets to export them in .env. | |
| HOST: ${HOST:-0.0.0.0} | |
| PORT: 3000 | |
| NODE_ENV: production | |
| ports: | |
| - "${PORT:-3000}:3000" |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docker-compose.yml` around lines 29 - 36, The container’s internal listen
port is being changed by the PORT environment override while the service mapping
and health probe still assume port 3000, so make the app listen on a fixed
container port unless the whole stack is updated together. In
docker-compose.yml, keep PORT aligned with the published container port or stop
using it to control the in-container bind port, and ensure the Dockerfile health
check/probe for the app matches the same fixed port used by the service.
| # SvelteKit's `$env/static/public` requires PUBLIC_* vars at build time. | ||
| # The Supabase wiring still imports them (Stage 4 will remove it). Provide | ||
| # placeholders so `pnpm build` succeeds without a `.env` in the image. Real | ||
| # values come from runtime env_file in docker-compose.yml / the Helm chart. | ||
| ENV PUBLIC_SUPABASE_URL=https://placeholder.supabase.co \ | ||
| PUBLIC_SUPABASE_ANON_KEY=placeholder-anon-key \ | ||
| PUBLIC_APP_URL=http://localhost:3000 |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🌐 Web query:
In SvelteKit 2.x, are $env/static/publicvalues substituted at build time, and can runtime environment variables override them in a built@sveltejs/adapter-node app?
💡 Result:
In SvelteKit 2.x, values from $env/static/public are substituted into your code at build time [1]. Because they are statically replaced with their values during the build process, they cannot be overridden by runtime environment variables in a built application [1][2]. If you need to access environment variables that can be changed at runtime, you should use $env/dynamic/public instead [2][3]. Unlike the static variant, $env/dynamic/public reads values from the environment at runtime (e.g., from process.env in a Node.js environment) [3]. Summary of differences: - $env/static/public: Values are baked into the bundle at build time, allowing for build-time optimizations (like dead code elimination). They are fixed once the build is complete [1][2]. - $env/dynamic/public: Values are accessed at runtime. This is the correct choice if you need to configure the application using environment variables after it has been built, such as when deploying the same build artifact to different environments [2][3].
Citations:
- 1: https://svelte.dev/docs/kit/$env-static-public
- 2: What's the difference between "dynamic" and "static" env variables in SvelteKit? sveltejs/kit#6718
- 3: https://svelte.dev/docs/kit/$env-dynamic-public
Avoid baking placeholder PUBLIC_* values into the image. Dockerfile:32-38 will inline these $env/static/public values at build time, so the placeholders ship in the bundle and runtime env_file/Helm env vars cannot override them. Switch this code to $env/dynamic/public or keep real build-time values out of the image.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Dockerfile` around lines 32 - 38, Remove the placeholder PUBLIC_* ENV block
from the Dockerfile and stop relying on build-time inlined public env values;
the issue is that these values are baked into the SvelteKit bundle via
$env/static/public, so runtime overrides cannot replace them. Update the
Supabase/public env usage in the relevant app code to use $env/dynamic/public
where appropriate, or otherwise ensure the build does not require placeholder
values in the image. Use the Dockerfile public env section and the SvelteKit env
imports as the key places to adjust.
| import adapter from '@sveltejs/adapter-node'; | ||
|
|
||
| /** @type {import('@sveltejs/kit').Config} */ | ||
| const config = { | ||
| kit: { | ||
| adapter: adapter({ | ||
| platformProxy: { | ||
| persist: { path: '.wrangler/state/v3' } | ||
| } | ||
| out: 'build', | ||
| precompress: false |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
Don't switch this repo off the Cloudflare adapter.
This changes the deployment artifact from Cloudflare Pages to a long-running Node server, which breaks the repo's current deployment contract.
As per coding guidelines, svelte.config.js: "Use Cloudflare Pages for deployment with @sveltejs/adapter-cloudflare" and "Do not switch adapters."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@svelte.config.js` around lines 1 - 8, The Svelte config has been switched
from the Cloudflare Pages adapter to the Node adapter, which changes the
deployment model. Revert the adapter change in config and keep using the
existing Cloudflare Pages setup by preserving the adapter configuration in
svelte.config.js, ensuring the deployment contract remains unchanged.
Source: Coding guidelines
Stage 1b — Node runtime + Docker Compose
Resolves TIM-46 (parallel sub-issue of TIM-45).
Replaces
@sveltejs/adapter-cloudflarewith@sveltejs/adapter-nodeso the same binary serves bothdocker compose(this PR) and the future Helm chart (Stage 3).What changed
@sveltejs/adapter-cloudflare→@sveltejs/adapter-node(out: 'build',precompress: false).PORT/HOSTenv vars are honoured by the adapter.pnpm deploy(Cloudflare Pages deploy); addedpnpm start(node build/index.js).wrangler(now unused).wrangler.toml.node:24-alpine, corepack-pinnedpnpm@10.30.3, BuildKit cache mount for/pnpm/store,pnpm prune --prodafter build, non-rootnodeuser,HEALTHCHECKhitting/healthzviawget.node_modules,.svelte-kit, build artefacts, env files, secrets, tests, docs, source control.env_file,HOST/PORToverrides,restart: unless-stopped, published port.include:overlay that adds a bundledpostgres:16-alpinewithpg_isreadyhealthcheck and a namedpgdatavolume. Also brings independs_on: postgres healthyon the app service so it waits for Postgres to be ready.{ ok: true }— Kubernetes/Compose probe-friendly.DATABASE_URL,BETTER_AUTH_*,GITHUB_*,PUBLIC_APP_URL,MISTRAL_API_KEY,HOST/PORT) with two commentedDATABASE_URLforms for bundled vs external Postgres. Supabase vars kept and marked DEPRECATED (Stage 4 will remove them).Verification
pnpm check: 0 errors (1 pre-existing svelte warning unrelated).pnpm lint: clean.pnpm test: 148/148 pass.pnpm build: produces a Node-runnable bundle inbuild/.docker build .: succeeds.docker compose -f docker-compose.yml -f docker-compose.postgres.yml up: both services healthy;curl http://localhost:3000/healthz→200with{"ok":true}.Usage
Out of scope (handled in later stages)
.cursor/rulesrewrite → Stage 4 (TIM-50)..github/workflows/deploy.yml(Cloudflare deploy workflow) → Stage 4 (TIM-50).Ready for review by @staff-engineer.
Summary by CodeRabbit
New Features
Bug Fixes
Chores