# Deployment & CI/CD planning notes Discussion notes only — nothing here has been implemented yet. Written so a different Claude Code conversation on this repo can pick up where this one left off without re-deriving it. Nothing below is committed to as final; it's a record of what was discussed and concluded, plus open decisions. ## Hosting shape (discussed, not yet 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](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 `href`s 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 plan: Gitea + Woodpecker, pushbutton staging/prod Repo is self-hosted on Gitea (`git.halfbinary.net/aarbit/av-planner`) with Woodpecker CI already in use elsewhere. No `.woodpecker.yml` exists yet in this repo. **Desired flow**: PR merges to `main` → pipeline runs lint/typecheck/build and stops → a human clicks a "Deploy" button to push to staging, and separately clicks it again to promote to production. Not fully automatic on merge — deploys to both environments are manual/pushbutton. **Mechanism (confirmed against current Woodpecker docs)**: Woodpecker has a built-in **deploy event** for exactly this. Enable **"Allow deployments"** in the repo's Woodpecker project settings, and any successful pipeline run gets a **Deploy** button in the UI. Clicking it re-runs that pipeline with `event: deploy` and a `deploy_to` string you type (e.g. `staging` or `production`), exposed to steps as `$CI_PIPELINE_DEPLOY_TARGET`. Steps gate on it, e.g.: ```yaml steps: - name: deploy-staging when: - event: deploy evaluate: 'CI_PIPELINE_DEPLOY_TARGET == "staging"' - name: deploy-prod when: - event: deploy evaluate: 'CI_PIPELINE_DEPLOY_TARGET == "production"' ``` **What needs to exist**: 1. **Two Supabase projects** — staging and prod. This exactly fills the free tier's "2 active projects" cap (see above) — no free-tier headroom left for a third project if one is ever wanted later. 2. **Two frontend hosting targets** — two sites/deployments (SaaS host or self-hosted alongside Gitea/Woodpecker — not yet decided which). 3. **Known wrinkle**: because Vite bakes `VITE_SUPABASE_URL`/`VITE_SUPABASE_ANON_KEY` in at build time, you can't build once and promote the identical artifact to both environments — each deploy target needs its own build with its own env vars baked in. Not hard, just means "deploy" reruns the build rather than reusing one artifact. A non-issue at this app's scale. 4. **Migrations run per-target** via Supabase CLI, scripted per environment: ```bash supabase link --project-ref $SUPABASE_PROJECT_REF supabase db push supabase functions deploy admin-user-action ``` 5. **Secrets needed in Woodpecker** (scoped per target): `SUPABASE_ACCESS_TOKEN`, staging/prod `SUPABASE_PROJECT_REF`, staging/prod `VITE_SUPABASE_URL` / `VITE_SUPABASE_ANON_KEY`, plus whatever the frontend host needs (API token, or SSH key if self-hosted). 6. **Google OAuth**: needs its own authorized redirect URI added in Google Cloud console for each of the staging and prod domains — one-time setup, easy to forget. **Difficulty estimate discussed**: roughly a focused afternoon — nothing here fights the tooling (no containers to orchestrate, no server process to run). Breakdown: `.woodpecker.yml` (~1–2 hrs), second Supabase project + migration check (~30 min), frontend hosting target wiring (~30–60 min), secrets + "Allow deployments" toggle (~15 min), end-to-end test/debug (~1–2 hrs). ## Open decisions (not yet made) - Which frontend host: SaaS (Vercel/Netlify/Cloudflare Pages) vs self-hosted alongside the existing Gitea/Woodpecker infra. - Where the marketing site (`site/`, see above) lives relative to the app — leaning subdomain (`diagrav.com` + `app.diagrav.com`, per the links already in `site/index.html`) over same-domain/different-path, but not finalized — and whether it gets its own line in the eventual `.woodpecker.yml` or piggybacks on the frontend deploy step. - Whether/when to add diagram-snapshot retention (the storage-risk item above) — not done yet, just identified. - Actual `.woodpecker.yml` has not been written yet — this doc captures the plan/shape only, per the user's request to keep this conversation discussion-only and not make repo changes here.