ci/woodpecker/push/woodpecker Pipeline was successful
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
191 lines
10 KiB
Markdown
191 lines
10 KiB
Markdown
# 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 `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.
|