Files
av-planner/deployment-plan.md
aarbitandClaude Sonnet 5 7fbe53f025
ci/woodpecker/push/woodpecker Pipeline was successful
Fix Woodpecker event name: deployment, not deploy
Caught from the linter warnings on the first real pipeline run (#1) — the
production-promotion step's event filter used \`deploy\`, an invalid event
name, so it would never have matched Woodpecker's actual deploy-button
event (\`deployment\`) and the production step would have silently never
run. Staging's own deploy (a push-triggered step) was unaffected and
verified working on that same run.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017DUU6CnxECCDeqDNYJgr5x
2026-09-29 15:49:44 -05:00

10 KiB
Raw Permalink Blame History

Deployment & CI/CD planning notes

Started as discussion-only notes; hosting and CI/CD are now both live (see their sections below for what's actually implemented vs. still open). Written so a different Claude Code conversation on this repo can pick up where this one left off without re-deriving it.

Hosting shape — implemented

The app is a static frontend (Vite build, no server of its own) + Supabase backend (Postgres, Auth, RLS, one Edge Function). organized-ideas.md already commits to this being provider-agnostic (Supabase Cloud, self-hosted Supabase Docker stack, or export the Postgres DB elsewhere — no app code changes needed either way).

To go live:

  1. Backend: create a Supabase Cloud project, supabase link + supabase db push to apply the 15 existing migrations, supabase functions deploy admin-user-action. Set real Google OAuth credentials for the production redirect URL — right now supabase/config.toml and vite.config.ts are hardcoded to 127.0.0.1 with skip_nonce_check = true for local dev; this needs a production config pass. Needs a real SMTP provider for auth emails (Resend suggested in organized-ideas.md as the default) since Inbucket (local fake mail catcher) doesn't exist in the cloud.
  2. Frontend: npm run build → static dist/, deploy to any static host (Vercel/Netlify/Cloudflare Pages, or self-hosted). Set VITE_SUPABASE_URL / VITE_SUPABASE_ANON_KEY as build-time env vars pointing at the real Supabase project (Vite bakes these in at build time — not runtime-configurable without a refactor).

Marketing site (site/)

A separate, informational "what is this thing" page lives in site/ at the repo root — plain hand-written HTML/CSS (index.html + style.css + images/), no build step, not part of the Vite app or its src/ tree at all. Screenshots under images/ are real captures from the app (a demo DAWless rig wired up for the purpose), referenced with relative paths, so the folder is fully self-contained.

Deploys the same way the app's frontend would, just simpler — no env vars to bake in, no build command, just serve the static files as-is. Same free-tier host options apply (Vercel/Netlify/Cloudflare Pages, or self-hosted).

Domain purchased: diagrav.com. The app was renamed from "AV Planner" to Diagrav to match. The site's nav ("Log in" / "Sign up") and its closing CTA ("Sign up free") now link to https://app.diagrav.com/ — so the subdomain-split option below is the working assumption baked into the site as of now, even though neither diagrav.com nor app.diagrav.com is actually deployed anywhere yet. Those links will 404 until the app is actually live at that subdomain; treat this as a leaning, not a final decision — swapping to the path-based option only means changing the few hrefs in site/index.html, nothing structural.

  • Subdomain split (current assumption): bare diagrav.com for the marketing site, app.diagrav.com for the actual tool — cleanest separation, two hosting targets instead of one.
  • Same domain, different path (marketing at /, app at /app or similar) — one hosting target, but means the static host needs to route / to site/ and everything else to the Vite dist/ output, which the simple "just deploy dist/" flow above doesn't currently account for.

Whichever is chosen, this needs its own line in the CI/CD plan below (currently only the app's frontend/backend deploy steps are scoped) — it's a third deployable alongside "frontend" and "backend," not an afterthought folded into the frontend step.

Cost / free-tier findings

Provider: Supabase, since the app already codes directly against its SDK/Auth/RLS model (src/data/Supabase*.ts) — switching providers would mean rewriting those repositories.

Current Supabase Free plan limits (confirmed against supabase.com/pricing, not from training data — re-verify if this doc is read much later):

Resource Free limit
Database size 500 MB
Egress 10 GB/mo (5 GB cached + 5 GB uncached)
Monthly active users 50,000
Edge Function invocations 500,000/mo
File storage 1 GB
Realtime concurrent connections 200
Inactivity paused after 1 week idle, max 2 active projects

Conclusion: this app is storage-bound, not bandwidth-bound. File storage, realtime, and edge function limits are all non-issues (no file uploads, no supabase.channel(...) usage anywhere in src/, and the one edge function is admin-only/rare). Egress is generous relative to what this app transfers per session. The one limit likely to actually bite is database size (500 MB) — driven less by user-count/traffic than by long-term accumulation in diagrams + diagram_snapshots.

Specific risk found: SupabaseDiagramRepository.ts (maybeWriteSnapshot, ~line 70-93) writes a new version-history snapshot row every 5 minutes of active editing, with no retention/expiry — they accumulate indefinitely. A user who edits diagrams regularly for months will grow the DB more than a burst of new signups would. Suggested follow-up (not yet done): add snapshot retention — e.g. keep the last N snapshots per diagram, or prune anything older than ~90 days. This is the highest-leverage lever for keeping DB size (and therefore cost/free-tier headroom) under control.

Rough scale ballpark discussed: solo/small-team use won't come close to any limit for years. Somewhere in the ~300–1,500 regular-active-user range with several diagrams each is roughly where the 500MB DB cap would start to bite, well before the 10GB egress cap or 50k MAU cap would matter. Treat this as a rough estimate, not a measured number — worth checking real diagram JSON size in Studio once there's real usage.

Estimated cost: $0/mo to start (Supabase Free + Vercel/Netlify/Cloudflare Pages free tier + Resend free tier for auth email), $25/mo once you want Supabase Pro (no project auto-pause, higher limits across the board).

CI/CD: Woodpecker, auto-staging / manual-prod — implemented

Repo is self-hosted on Gitea (git.halfbinary.net/aarbit/av-planner) with Woodpecker CI already in use elsewhere. .woodpecker.yml now exists at the repo root. The flow below supersedes an earlier version of this section that proposed manual pushbutton deploys for both staging and production — revisited and simplified to auto-deploy staging instead.

Actual flow: any push (any branch) runs lint + typecheck, then auto-deploys to a shared staging environment — no button needed. Production is never touched automatically; promoting to it is a manual Deploy button click (Woodpecker's built-in deployment event) on a main-branch pipeline run, typing production as the deploy target ($CI_PIPELINE_DEPLOY_TARGET). Requires "Allow deployments" enabled in the repo's Woodpecker project settings.

Two fully separate environments exist, each with its own everything (no shared data or infra between them, or with production):

Staging Production
Supabase project onovxozixdxpdngxafxt ("Diagrav Staging") wmombshptajyrawdbroj ("Diagrav")
App Worker diagrav-app-staging → staging-app.diagrav.com diagrav-app → app.diagrav.com
Site Worker diagrav-site-staging → staging.diagrav.com diagrav-site → diagrav.com

This uses both of Supabase Free's "2 active projects" slots — no headroom for a third project if one's ever wanted later (see cost findings above).

How each piece works:

  • Wrangler: wrangler.app.jsonc / wrangler.site.jsonc each gained an env.staging block (own name + own routes) — assets.directory isn't per-environment in Wrangler, which is fine, since it's rebuilt with different VITE_* values before each deploy anyway (the "known wrinkle" below).
  • Known wrinkle (as anticipated): Vite bakes VITE_SUPABASE_URL/ VITE_SUPABASE_ANON_KEY in at build time, so npm run build reruns once per target with that target's env vars set, rather than promoting one artifact between environments.
  • Migrations: supabase db push --project-ref $REF --password $SUPABASE_DB_PASSWORD per target (no persistent supabase link — every command that needs a project takes --project-ref directly, which also meant this never touched the local repo's own supabase link state, still pointed at production for everyday local dev).
  • Google OAuth: staging reuses production's same OAuth client rather than a new one — just one more authorized redirect URI (https://onovxozixdxpdngxafxt.supabase.co/auth/v1/callback) added to it in Google Cloud Console, since a client can authorize many redirect URIs.
  • SMTP: staging reuses the same Resend account/verified domain as production, with a distinct smtp_sender_name ("Diagrav (Staging)") so the two are distinguishable in an inbox.
  • Staging catalog data: seeded once by applying supabase/seed.sql directly against the new project (not part of the pipeline — a one-time setup step, same as production's original seeding).

Secrets in Woodpecker (repo Settings → Secrets): cloudflare_api_token, cloudflare_account_id, supabase_access_token (shared); per-target {staging,prod}_supabase_url, {staging,prod}_supabase_anon_key, {staging,prod}_supabase_project_ref, {staging,prod}_supabase_db_password.

Outstanding: the production supabase_db_password secret — Supabase doesn't expose an existing project's DB password via the Management API, so this needs either the password from wherever it was originally saved, or a deliberate decision to rotate it (a live production credential change, not something to do silently). The staging path has been fully verified end-to-end (migrations, seed, Auth, a real sign-in through the deployed app); production's manual deploy path has not been exercised yet — no reason to touch real prod while proving the pipeline out.

Open decisions (not yet made)

  • Frontend host and the marketing site's placement are both decided and live, superseding the two bullets that used to be here: Cloudflare Workers (assets-only, no server code) for both, subdomain split (diagrav.com marketing site + app.diagrav.com app), matching the links already in site/index.html.
  • Whether/when to add diagram-snapshot retention (the storage-risk item above) — not done yet, just identified.
  • The production supabase_db_password gap noted above.