Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
24 changes: 24 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Vanilla Postgres 17 for Phase 0 migration compatibility testing.
# Does NOT include GoTrue, PostgREST, Studio, Realtime, or Storage API.
# Use `bun run postgres:vanilla:push` to apply supabase/migrations/ via the Supabase CLI.
name: capgo-vanilla
services:
postgres:
image: postgres:17-alpine
container_name: capgo-vanilla-postgres
ports:
- '127.0.0.1:5432:5432'
environment:
POSTGRES_USER: ${VANILLA_POSTGRES_USER}
POSTGRES_PASSWORD: ${VANILLA_POSTGRES_PASSWORD}
POSTGRES_DB: ${VANILLA_POSTGRES_DB}
volumes:
- capgo_vanilla_postgres_data:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U postgres -d capgo']
interval: 2s
timeout: 5s
retries: 15

volumes:
capgo_vanilla_postgres_data:
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
"supabase:db:reset": "bun scripts/supabase-worktree.ts db reset",
"supabase:functions:serve": "bun scripts/supabase-worktree.ts functions serve",
"supabase:with-env": "bun scripts/supabase-worktree.ts with-env",
"postgres:vanilla:up": "bash scripts/vanilla-postgres-up.sh",
"postgres:vanilla:down": "bash scripts/vanilla-postgres-down.sh",
"postgres:vanilla:push": "bash scripts/vanilla-postgres-push.sh",
"env:hard-setup": "bun run supabase:stop && bun run supabase:start && bun run supabase:db:reset",
"readreplicate:add-table": "bash read_replicate/replicate_add_table.sh",
"preview": "vite preview",
Expand Down
11 changes: 11 additions & 0 deletions scripts/vanilla-postgres-down.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck source=scripts/vanilla-postgres-env.sh
source "$ROOT_DIR/scripts/vanilla-postgres-env.sh"
cd "$ROOT_DIR"

COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.yml}"

docker compose -f "$COMPOSE_FILE" down "$@"
5 changes: 5 additions & 0 deletions scripts/vanilla-postgres-env.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Local-only defaults for the vanilla Postgres compose service.
# Sourced by postgres:vanilla:* scripts so docker-compose.yml stays credential-free.
export VANILLA_POSTGRES_USER="${VANILLA_POSTGRES_USER:-postgres}"
export VANILLA_POSTGRES_PASSWORD="${VANILLA_POSTGRES_PASSWORD:-postgres}"
export VANILLA_POSTGRES_DB="${VANILLA_POSTGRES_DB:-capgo}"
68 changes: 68 additions & 0 deletions scripts/vanilla-postgres-push.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
set -euo pipefail

# Apply supabase/migrations/ to the vanilla Postgres 17 container from docker-compose.yml.
# Requires: Docker, docker compose, and the Supabase CLI (bunx supabase).

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck source=scripts/vanilla-postgres-env.sh
source "$ROOT_DIR/scripts/vanilla-postgres-env.sh"
cd "$ROOT_DIR"

COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.yml}"
SERVICE="${VANILLA_POSTGRES_SERVICE:-postgres}"

if [[ -n "${DATABASE_URL:-}" && -z "${VANILLA_POSTGRES_DATABASE_URL:-}" ]]; then
echo "Refusing to use inherited DATABASE_URL for vanilla Postgres migrations." >&2
echo "Unset DATABASE_URL or set VANILLA_POSTGRES_DATABASE_URL to opt in." >&2
exit 1
fi

if [[ -n "${VANILLA_POSTGRES_DATABASE_URL:-}" ]]; then
DATABASE_URL="$VANILLA_POSTGRES_DATABASE_URL"
else
DATABASE_URL="$(bun -e 'const encode = encodeURIComponent; console.log(`postgresql://${encode(process.env.VANILLA_POSTGRES_USER)}:${encode(process.env.VANILLA_POSTGRES_PASSWORD)}@127.0.0.1:5432/${encode(process.env.VANILLA_POSTGRES_DB)}?sslmode=disable`)')"
fi

compose() {
docker compose -f "$COMPOSE_FILE" "$@"
}

ensure_postgres() {
if ! compose ps --status running --services 2>/dev/null | grep -qx "$SERVICE"; then
echo "Starting vanilla Postgres ($SERVICE) from $COMPOSE_FILE ..."
compose up -d "$SERVICE"
fi

echo "Waiting for Postgres to accept connections ..."
for _ in $(seq 1 60); do
if compose exec -T "$SERVICE" pg_isready -U "$VANILLA_POSTGRES_USER" -d "$VANILLA_POSTGRES_DB" >/dev/null 2>&1; then
return 0
fi
sleep 1
done
echo "Postgres did not become ready in time." >&2
exit 1
}

ensure_postgres

LOG_DIR="${ROOT_DIR}/.context/vanilla-postgres"
mkdir -p "$LOG_DIR"
LOG_FILE="${LOG_DIR}/db-push-$(date -u +%Y%m%dT%H%M%SZ).log"

echo "Applying migrations with: bunx supabase db push --db-url <redacted>"
echo "Full log: $LOG_FILE"

set +e
bunx supabase db push --db-url "$DATABASE_URL" 2>&1 | tee "$LOG_FILE"
EXIT_CODE=${PIPESTATUS[0]}
set -e

if [[ $EXIT_CODE -eq 0 ]]; then
echo "All migrations applied successfully."
else
echo "Migration apply failed (exit $EXIT_CODE). See $LOG_FILE for details."
fi

exit "$EXIT_CODE"
12 changes: 12 additions & 0 deletions scripts/vanilla-postgres-up.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -euo pipefail

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck source=scripts/vanilla-postgres-env.sh
source "$ROOT_DIR/scripts/vanilla-postgres-env.sh"
cd "$ROOT_DIR"

COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.yml}"
SERVICE="${VANILLA_POSTGRES_SERVICE:-postgres}"

docker compose -f "$COMPOSE_FILE" up -d "$SERVICE"
49 changes: 49 additions & 0 deletions supabase/migration_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,52 @@ This command will clear all data and revert schema changes made to the local dat


By following these steps, you can safely add and deploy Supabase migration changes to your project's database schema.

## Vanilla Postgres 17 (Phase 0 compatibility path)

Capgo keeps the Supabase CLI as the migration runner. For Phase 0 of leaving the full
Supabase Docker stack, you can apply the same `supabase/migrations/` tree against a
plain `postgres:17` container. This is **additive**: `bun run supabase:start` remains
the default local path.

### Start vanilla Postgres

```bash
bun run postgres:vanilla:up
```

`docker-compose.yml` exposes Postgres on `127.0.0.1:5432`. Credentials are injected by
`scripts/vanilla-postgres-env.sh` (defaults: user/password `postgres`, database `capgo`).
Override with `VANILLA_POSTGRES_USER`, `VANILLA_POSTGRES_PASSWORD`, or `VANILLA_POSTGRES_DB`.

### Apply migrations

```bash
bun run postgres:vanilla:push
```

This runs `bunx supabase db push --db-url postgresql://postgres:postgres@127.0.0.1:5432/capgo?sslmode=disable`
and writes a timestamped log under `.context/vanilla-postgres/`.

Override the URL when needed:

```bash
VANILLA_POSTGRES_DATABASE_URL='postgresql://postgres:postgres@127.0.0.1:5432/capgo?sslmode=disable' bun run postgres:vanilla:push
```

### Create new migrations (unchanged)

```bash
bunx supabase migration new <feature_slug>
```

Edit the generated file under `supabase/migrations/`, test on the full Supabase stack
with `bun run supabase:db:reset`, and optionally re-run `bun run postgres:vanilla:push`
to see what still depends on Supabase-only pieces.

### Known gaps on vanilla Postgres

See [vanilla_postgres_inventory.md](./vanilla_postgres_inventory.md) for the categorized
inventory from the Phase 0 apply attempt (extensions, auth/storage schemas, roles, hooks,
queues). Expect the baseline squash migration to fail early without Supabase extensions
and platform schemas.
166 changes: 166 additions & 0 deletions supabase/vanilla_postgres_inventory.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
# Vanilla Postgres 17 migration inventory (Phase 0)

Phase 0 applies the existing `supabase/migrations/` tree to a plain `postgres:17`
container (`docker-compose.yml`) via the Supabase CLI:

```bash
bun run postgres:vanilla:up
bun run postgres:vanilla:push
```

This path is **additive**. `bun run supabase:start` remains the default local
development stack.

## Apply attempt (2026-09-08)

| Field | Value |
| --- | --- |
| Postgres image | `postgres:17-alpine` |
| CLI | `bunx supabase db push --db-url 'postgresql://postgres:postgres@127.0.0.1:5432/capgo?sslmode=disable'` |
| Migration files | 81 (`20260708000000_prod_baseline.sql` + 80 incrementals) |
| **First failure** | `20260708000000_prod_baseline.sql` |
| **First error** | `extension "pg_cron" is not available` |
| **Failed statement** | `CREATE EXTENSION IF NOT EXISTS "pg_cron" WITH SCHEMA "pg_catalog"` (statement 10) |
| Migrations applied before failure | 0 (baseline did not complete) |
| Log artifact | `.context/vanilla-postgres/db-push-phase0.log` (local; not committed) |

`sslmode=disable` is required for the compose Postgres URL; the Supabase CLI
defaults to TLS and fails with “The server does not support SSL connections”
otherwise.

## Categorized inventory

Static analysis of `supabase/migrations/*.sql` plus the failed apply. Items are
ordered roughly as they would block a vanilla apply after earlier blockers are
resolved.

### 1. Extensions (baseline `20260708000000_prod_baseline.sql`)

| Extension | Schema | Vanilla PG 17 | Notes |
| --- | --- | --- | --- |
| `pg_cron` | `pg_catalog` | **Blocks first** | Job scheduler; not in stock `postgres:17` image |
| `pg_net` | `extensions` | **Required** | Async HTTP from SQL; Supabase/platform image |
| `pgmq` | `pgmq` | **Required** | Queue extension; used across baseline + incrementals |
| `supabase_vault` | `vault` | **Required** | Secrets (`vault.decrypted_secrets`, `vault.secrets`) |
| `http` | `extensions` | Likely missing | Used with `net.http_post` patterns |
| `hypopg` | `extensions` | Optional dev | Hypothetical indexes |
| `index_advisor` | `extensions` | Optional dev | Query advisor |
| `moddatetime` | `extensions` | Often installable | `updated_at` triggers |
| `pg_stat_statements` | `extensions` | Usually available | May need `shared_preload_libraries` |
| `pg_tle` | default | Uncommon | Trusted Language Extensions |
| `plpgsql_check` | `extensions` | Optional dev | Linting |
| `pgcrypto` | `extensions` | **Usually OK** | Stock contrib module |

Dropped in baseline (harmless on vanilla): `pg_graphql`, `pg_stat_monitor`, `postgres_fdw`.

### 2. Auth schema and helpers (`auth.*`)

Not created by Capgo migrations; provided by GoTrue / Supabase Auth image.

| Dependency | Occurrences (approx.) | Example |
| --- | --- | --- |
| `auth.users` | baseline + incrementals | joins, deletes, signup hooks |
| `auth.mfa_factors` | baseline + MFA migrations | 2FA enforcement |
| `auth.uid()` | baseline-heavy | RLS, RBAC helpers |
| `auth.jwt()` | baseline-heavy | role / service_role checks |
| `auth.role()` | baseline | request role resolution |

**Incremental migrations** that assume `auth.*` without creating it include
`20260817175411_block_password_signup_sso.sql`,
`20260817175835_split_mfa_session_and_email_otp_checks.sql`, and many baseline
RLS policies.

### 3. Storage schema (`storage.*`)

Not created by Capgo migrations; provided by Supabase Storage.

| Object | Migrations | Notes |
| --- | --- | --- |
| `storage.objects` RLS policies | baseline, `20260723120547_fix_app_create_storage_rls.sql` | `images` / `apps` bucket paths |
| `storage.foldername()` | storage RLS policies | Supabase storage helper |

### 4. Roles and grants

Supabase platform roles expected but not created on vanilla Postgres:

| Role | Usage |
| --- | --- |
| `anon` | PostgREST / RLS policies, RPC `GRANT EXECUTE` |
| `authenticated` | JWT-authenticated RLS and RPC grants |
| `service_role` | privileged bypass in functions and grants |
| `supabase_admin` | internal admin bypass arrays |
| `supabase_auth_admin` | GoTrue hook execution (`hook_*` grants) |
| `supabase_storage_admin` | storage bypass in audit helpers |
| `supabase_realtime_admin` | listed in privileged session_user checks |

Baseline alone has **~170** `GRANT ... TO anon|authenticated|service_role` statements.
Incrementals add more (e.g. `20260715213729_app_preview_api_key_role.sql`).

Hook migrations grant to `supabase_auth_admin` only when the role exists
(`IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'supabase_auth_admin')`).

### 5. GoTrue auth hooks

| Function | Migration | Purpose |
| --- | --- | --- |
| `public.hook_before_user_created` | `20260817175411_block_password_signup_sso.sql` | Block password signup when SSO-only |
| `public.hook_send_email` | `20260820101459_auth_send_email_hook_queue.sql` | Enqueue auth emails via `pgmq` |

Both require GoTrue to call them and `supabase_auth_admin` execute grants.

### 6. Queues and cron (`pgmq`, `pg_cron`, `net`)

| Mechanism | Baseline | Incrementals (examples) |
| --- | --- | --- |
| `pgmq.create(...)` | yes | `global_stats_creates`, `send_email`, `on_user_org_access`, … |
| `pgmq.send(...)` | yes | webhooks, cron dispatch, auth email queue |
| `cron.schedule(...)` | yes | `20260715213729_app_preview_api_key_role.sql` |
| `net.http_post(...)` | yes | edge function / worker dispatch from SQL |

Queue names touched in migrations include (non-exhaustive): `admin_stats`,
`cron_email`, `send_email`, `global_stats_creates`, `on_user_org_access`,
`canceled_org_retention_alerts`, `cron_app_fame`, and cron-task-driven queues
registered in `cron_tasks`.

### 7. Vault secrets

| Pattern | Migration |
| --- | --- |
| `vault.decrypted_secrets` reads | baseline (`apikey`, `db_url`, runtime config) |
| `DELETE FROM vault.secrets` | `20260820090539_remove_rbac_global_flag.sql` |

Requires `supabase_vault` extension and seeded secrets (normally platform-managed).

### 8. Other Supabase-platform assumptions

- **`extensions` schema** — baseline installs multiple extensions into `extensions`.
- **PostgREST request GUCs** — `request.headers`, `capgkey` header helpers (via Supabase API layer).
- **Realtime / GraphQL** — `pg_graphql` dropped in baseline; realtime not migrated but admin roles referenced.

## What likely works without changes

After stubbing or replacing the blockers above, much of `public.*` (tables, RBAC,
business logic) is plain PostgreSQL. Phase 0 intentionally does **not** stub those
pieces; it documents dependencies before any auth cutover or PlanetScale work.

## Next phases (out of scope for Phase 0)

- Do not change auth or adopt better-auth in Phase 0.
- Do not modify PlanetScale / replica paths.
- Prefer additive compatibility shims or separate bootstrap SQL over editing the
squashed baseline until a deliberate migration strategy is chosen.

## Reproduce locally

```bash
bun run postgres:vanilla:up
bun run postgres:vanilla:push # expect failure at pg_cron on fresh DB
```

Fresh database:

```bash
docker compose -f docker-compose.yml down -v
bun run postgres:vanilla:up
bun run postgres:vanilla:push
```
Loading