Files
av-planner/deployment-plan.md
T
aarbitandClaude Sonnet 5 d847a0f255
ci/woodpecker/push/woodpecker Pipeline was successful
Add CI/CD pipeline: Woodpecker, auto-staging / manual-prod
Adds .woodpecker.yml (lint + typecheck on every push, auto-deploy to a
shared staging environment on every push, manual Deploy-button promotion to
production on main) and env.staging blocks in both wrangler configs so
staging gets its own Workers (diagrav-app-staging/diagrav-site-staging at
staging-app.diagrav.com/staging.diagrav.com) rather than sharing anything
with production.

Staging also got its own fully separate Supabase Cloud project (own
database, own Auth config reusing the same Google OAuth client with an
extra redirect URI, own Resend-backed SMTP) — migrated, seeded with the
public catalog, and verified end-to-end with a real sign-in through the
deployed app before wiring any of this into CI.

Updates deployment-plan.md's CI/CD section to match what actually got
built, superseding the earlier manual-pushbutton-for-both-environments
version of the plan.

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

191 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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](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: 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 deploy 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.