Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
61577b2
feat: prod/staging/local env setup with prod-to-target sync script
claude May 8, 2026
f162cd2
feat(sync): sync auth.users with email rewriting
claude May 8, 2026
56216bd
feat(ci): auto-push migrations to staging on develop, prod on main
claude May 8, 2026
e0d8ead
chore(ci): drop develop branch — main-only flow
claude May 9, 2026
aa706c7
feat(ci): auto-migrate staging on PRs that touch migrations
claude May 9, 2026
7655e83
feat(ci): comment on PR when staging migration fails
claude May 9, 2026
a0b26ce
fix(ci): read PROJECT_REF from vars, not secrets
claude May 9, 2026
9588d37
fix: address PR #30 review feedback
claude May 9, 2026
7b0751c
fix(supabase): throw on missing env vars instead of silently hitting …
claude May 9, 2026
3b2a50b
fix(sync): collapse auth.users dump command to a single line
claude May 9, 2026
7733c4a
fix(sync): use bash expansion for csv path instead of psql :var
claude May 9, 2026
797dec5
fix(sync): use session_replication_role=replica instead of --disable-…
claude May 9, 2026
4642811
fix(sync): anonymize profiles.email to avoid collision on new sign-in
claude May 9, 2026
7f6ce79
feat(auth): commit magic-link email template
claude May 9, 2026
ee50fb2
docs(auth): track enable_confirmations and otp_length in config.toml
claude May 9, 2026
41511ff
docs: rewrite ENVIRONMENTS.md as a concise setup checklist
claude May 9, 2026
5312f18
fix(ci): wire VITE_SUPABASE_* into unit-tests workflow
claude May 9, 2026
a5cbb26
fix(ci): upsert single PR comment for migration status, delete on suc…
claude May 9, 2026
d08abaf
fix(ci): post success comment instead of deleting on green migrate
claude May 9, 2026
d2cf6b7
chore: pin node version via .nvmrc + engines
claude May 9, 2026
e310329
fix: address PR review — drop --mode local and clean env file confusion
claude May 9, 2026
b5f5a83
fix: address PR review — drop dev:prod, drop email from auth dump
claude May 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .env.local.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Personal env file for local development. Loaded by Vite for every mode,
# but mode-specific files (.env.staging, .env.staging.local, etc.) override it.
#
# Copy this file to `.env.local` and adjust as needed. The defaults below
# match the Supabase CLI's local stack (`supabase start`).
VITE_SUPABASE_URL=http://127.0.0.1:54321
VITE_SUPABASE_PUBLISHABLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0
4 changes: 4 additions & 0 deletions .env.staging.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Staging Supabase project (used by `pnpm run dev:staging` and `pnpm run build:staging`)
# Copy this file to `.env.staging.local` and fill in the values.
VITE_SUPABASE_URL=https://your-staging-project.supabase.co
VITE_SUPABASE_PUBLISHABLE_KEY=your-staging-anon-key
81 changes: 81 additions & 0 deletions .github/workflows/db-migrate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
name: DB Migrate

on:
push:
branches: [main]
paths:
- "supabase/migrations/**"
pull_request:
branches: [main]
paths:
- "supabase/migrations/**"
- ".github/workflows/db-migrate.yml"
Comment thread
chiptus marked this conversation as resolved.
workflow_dispatch:
inputs:
target:
description: "Which environment to migrate"
required: true
type: choice
options: [staging, prod]

permissions:
contents: read
pull-requests: write

jobs:
migrate:
name: Push migrations
runs-on: ubuntu-latest
timeout-minutes: 10
environment: ${{ ((github.event_name == 'workflow_dispatch' && github.event.inputs.target == 'prod') || github.event_name == 'push') && 'production' || 'staging' }}

steps:
- uses: actions/checkout@v4

- uses: supabase/setup-cli@v1
with:
version: 2.58.5

- name: Resolve target
id: target
run: |
case "${{ github.event_name }}" in
workflow_dispatch) TARGET="${{ github.event.inputs.target }}" ;;
pull_request) TARGET="staging" ;;
push) TARGET="prod" ;;
*) echo "Unknown event"; exit 1 ;;
esac
echo "target=$TARGET" >> "$GITHUB_OUTPUT"
echo "Migrating: $TARGET"

- name: Push migrations
env:
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
PROJECT_REF: ${{ steps.target.outputs.target == 'prod' && vars.PROD_PROJECT_REF || vars.STAGING_PROJECT_REF }}
DB_PASSWORD: ${{ steps.target.outputs.target == 'prod' && secrets.PROD_DB_PASSWORD || secrets.STAGING_DB_PASSWORD }}
run: |
supabase link --project-ref "$PROJECT_REF" --password "$DB_PASSWORD"
supabase db push --password "$DB_PASSWORD"

- name: Update PR comment with migration status
if: always() && github.event_name == 'pull_request'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OUTCOME: ${{ job.status }}
PR: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
TARGET: ${{ steps.target.outputs.target || 'staging' }}
MARKER: "<!-- db-migrate-status -->"
run: |
if [[ "$OUTCOME" == "success" ]]; then
BODY="$MARKER"$'\n'"✅ **DB Migrate succeeded** for \`$TARGET\` — [workflow run]($RUN_URL)."
else
BODY="$MARKER"$'\n'"❌ **DB Migrate failed** for \`$TARGET\` — [workflow run]($RUN_URL)."
fi
EXISTING=$(gh api "repos/$REPO/issues/$PR/comments" --jq ".[] | select(.body | contains(\"$MARKER\")) | .id" | head -n1)
if [[ -n "$EXISTING" ]]; then
gh api -X PATCH "repos/$REPO/issues/comments/$EXISTING" -f body="$BODY"
else
gh pr comment "$PR" --body "$BODY"
fi
11 changes: 7 additions & 4 deletions .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ name: E2E Tests

on:
push:
branches: [main, develop]
branches: [main]
pull_request:
branches: [main, develop]
branches: [main]

jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
env:
VITE_SUPABASE_URL: http://127.0.0.1:54321
VITE_SUPABASE_PUBLISHABLE_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0
strategy:
fail-fast: false
matrix:
Expand All @@ -24,7 +27,7 @@ jobs:

- uses: actions/setup-node@v4
with:
node-version: 22
node-version-file: ".nvmrc"
cache: "pnpm"

- name: Install dependencies
Expand Down Expand Up @@ -65,7 +68,7 @@ jobs:

- uses: actions/setup-node@v4
with:
node-version: 22
node-version-file: ".nvmrc"
cache: "pnpm"

- name: Install dependencies
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: Lint

on:
push:
branches: [main, develop]
branches: [main]
pull_request:
branches: [main, develop]
branches: [main]

jobs:
lint:
Expand All @@ -19,7 +19,7 @@ jobs:

- uses: actions/setup-node@v4
with:
node-version: 22
node-version-file: ".nvmrc"
cache: "pnpm"

- name: Install dependencies
Expand Down
9 changes: 6 additions & 3 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@ name: Unit Tests

on:
push:
branches: [main, develop]
branches: [main]
pull_request:
branches: [main, develop]
branches: [main]

jobs:
test:
name: Run Unit Tests
runs-on: ubuntu-latest
timeout-minutes: 10
env:
VITE_SUPABASE_URL: http://127.0.0.1:54321
VITE_SUPABASE_PUBLISHABLE_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0

steps:
- uses: actions/checkout@v4
Expand All @@ -19,7 +22,7 @@ jobs:

- uses: actions/setup-node@v4
with:
node-version: 22
node-version-file: ".nvmrc"
cache: "pnpm"

- name: Install dependencies
Expand Down
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,7 @@ supabase/.branches
.vercel
dev-dist

dump.sql
dump.sql

# Sync script credentials (DB connection strings — never commit)
scripts/.env.sync
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
22
92 changes: 92 additions & 0 deletions docs/ENVIRONMENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Environments

| Env | Project | Used by |
| --- | --- | --- |
| **local** | Supabase CLI (`supabase start`) | `pnpm run dev` (default), e2e tests |
| **staging** | a second Supabase project | `pnpm run dev:staging`, Vercel preview deploys |
| **prod** | `qssmazlqrmxiudxckxvi` | Vercel production only |

The frontend reads `VITE_SUPABASE_URL` / `VITE_SUPABASE_PUBLISHABLE_KEY` from a Vite env file picked by `--mode`. Vite load order (later overrides earlier):

```
.env -> .env.local -> .env.[mode] -> .env.[mode].local
```

`*.local` files are gitignored; the `*.example` files are templates.

## Setting up a new Supabase project (e.g. staging)

1. **Create the project** in the Supabase dashboard.
2. **Configure auth** (Authentication → Sign In / Providers → Email):
- "Confirm email" → **off**
- "Email OTP Length" → **6**
- Site URL and additional redirect URLs → match prod
3. **Paste the magic-link email template** (Authentication → Email Templates → Magic Link) from `supabase/templates/magic_link.html`. Re-paste on every change.
4. **Apply migrations**: GitHub → Actions → **DB Migrate** → Run workflow → target = `staging`.
5. **Save the database password** (Project Settings → Database). You'll need it for GitHub secrets and `scripts/.env.sync`.

## GitHub Actions config

Settings → Secrets and variables → Actions.

**Variables:**

| Name | Value |
| --- | --- |
| `PROD_PROJECT_REF` | `qssmazlqrmxiudxckxvi` |
| `STAGING_PROJECT_REF` | the staging project's ref |

**Secrets:**

| Name | Source |
| --- | --- |
| `SUPABASE_ACCESS_TOKEN` | https://supabase.com/dashboard/account/tokens |
| `PROD_DB_PASSWORD` | Project Settings → Database |
| `STAGING_DB_PASSWORD` | Same, on the staging project |

Add a **required-reviewer** rule to the `production` GitHub environment (Settings → Environments → production) so prod migrations pause for approval.

## Vercel config

Project Settings → Environment Variables. For each Supabase var, add it twice:

| Variable | Production scope (main) | Preview scope (everything else) |
| --- | --- | --- |
| `VITE_SUPABASE_URL` | prod URL | staging URL |
| `VITE_SUPABASE_PUBLISHABLE_KEY` | prod anon key | staging anon key |
| `VITE_PUBLIC_POSTHOG_KEY` | PostHog key | same |
| `VITE_PUBLIC_POSTHOG_HOST` | PostHog host | same |

## Local prerequisites

- **Supabase CLI** + **Docker** (for `supabase start`)
- **Postgres client tools** for the sync script: `brew install libpq` on macOS (and add `/opt/homebrew/opt/libpq/bin` to your PATH), `apt-get install postgresql-client` on Debian.
- Copy env templates:
```bash
cp .env.local.example .env.local # local supabase
cp .env.staging.example .env.staging.local # staging
cp scripts/.env.sync.example scripts/.env.sync # prod + staging direct DB connection strings (for sync script)
```
Comment thread
chiptus marked this conversation as resolved.

## Day-to-day commands

```bash
pnpm run dev # local supabase (requires `supabase start`)
pnpm run dev:staging # staging
pnpm run db:sync:staging # overwrite staging public schema with prod data, anonymized
pnpm run db:sync:local # same, into local supabase
Comment thread
chiptus marked this conversation as resolved.
```

The sync script syncs `auth.users` (with anonymized emails, no passwords) and `public.*` (PII scrubbed via `scripts/anonymize.sql`). Existing target users on `auth.users` are preserved via `ON CONFLICT DO NOTHING`. Skip auth sync with `SYNC_AUTH=0`.

If you add a public-schema column that holds free-form user input or PII, update `scripts/anonymize.sql`.

## Migration flow

- PR → `main`: Vercel preview points at staging. If the PR touches `supabase/migrations/**`, **DB Migrate** auto-pushes to staging. `supabase db push` is idempotent so re-runs on each commit are safe.
- Merge to `main`: **DB Migrate** auto-pushes to prod (gated by the `production` environment's reviewer rule, if configured).
- Manual: Actions → **DB Migrate** → Run workflow → pick target.

## Auth email template

`supabase/templates/magic_link.html` is the source of truth. `supabase/config.toml` wires it into local supabase automatically. For staging and prod, paste it into the dashboard manually after each change (Authentication → Email Templates → Magic Link). Automating this via the Supabase Management API is a future improvement.
10 changes: 9 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
"type": "module",
"scripts": {
"dev": "vite",
"dev:staging": "vite --mode staging",
"build": "vite build",
"build:dev": "vite build --mode development",
"build:staging": "vite build --mode staging",
Comment thread
chiptus marked this conversation as resolved.
"lint": "oxlint . --fix",
"format": "prettier --write .",
"format:check": "prettier --check .",
Expand All @@ -24,6 +26,8 @@
"test:setup:full": "bash scripts/setup-local-supabase.sh",
"types:generate": "supabase gen types typescript --project-id qssmazlqrmxiudxckxvi > src/integrations/supabase/types.ts",
"types:generate:local": "supabase gen types typescript --local > src/integrations/supabase/types.ts",
"db:sync:staging": "bash scripts/sync-from-prod.sh staging",
"db:sync:local": "bash scripts/sync-from-prod.sh local",
"typecheck": "tsc --noEmit --project tsconfig.app.json",
"prepare": "husky"
},
Expand Down Expand Up @@ -138,5 +142,9 @@
"prettier --write"
]
},
"packageManager": "pnpm@10.12.4"
"packageManager": "pnpm@10.12.4",
"engines": {
"node": ">=22 <23",
"pnpm": ">=10"
}
}
11 changes: 11 additions & 0 deletions scripts/.env.sync.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Copy this file to scripts/.env.sync and fill in values.
# scripts/.env.sync is gitignored — never commit real connection strings.
#
# Find these in Supabase Dashboard -> Project Settings -> Database
# -> Connection string -> URI (use the "Direct connection" string, not the pooler).

PROD_DB_URL="postgresql://postgres:PASSWORD@db.qssmazlqrmxiudxckxvi.supabase.co:5432/postgres"
STAGING_DB_URL="postgresql://postgres:PASSWORD@db.YOUR-STAGING-REF.supabase.co:5432/postgres"

# Optional — defaults to the Supabase CLI's local DB.
# LOCAL_DB_URL="postgresql://postgres:postgres@127.0.0.1:54322/postgres"
30 changes: 30 additions & 0 deletions scripts/anonymize.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
-- Scrubs PII from a freshly-restored prod dump (public schema only).
-- Run after restoring data into staging or local. Idempotent.
--
-- auth.users IS synced separately by sync-from-prod.sh, with emails rewritten
-- to user-<short-id>@example.test at load time and no password set. Existing
-- target accounts are preserved via ON CONFLICT (id) DO NOTHING. Skip the auth
-- sync entirely with SYNC_AUTH=0.
--
-- Any new public-schema column that holds free-text user input should be
-- added below.

BEGIN;

-- profiles.username may contain a real handle, and profiles.email is UNIQUE
-- and would collide if a real user signs in to staging (the new auth.users
-- row created by Supabase would conflict with the synced profile's email).
-- Both replaced with synthetic values matching the auth.users anonymization.
UPDATE public.profiles
SET username = 'user_' || substring(id::text, 1, 8),
email = 'user-' || substring(id::text, 1, 8) || '@example.test';

-- artist_notes.note_content is free-form user text. Wipe it.
UPDATE public.artist_notes
SET note_content = '[redacted]';

-- group_invites.invite_token is a live secret — rotate so old links are dead.
UPDATE public.group_invites
SET invite_token = encode(gen_random_bytes(16), 'hex');

COMMIT;
Loading
Loading