> ## Documentation Index
> Fetch the complete documentation index at: https://docs.encoreos.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Supabase Multi-Environment Configuration

> Version: 2.2.1 Last Updated: 2026-04-24 Based on: Supabase Official Documentation

**Version:** 2.2.1
**Last Updated:** 2026-04-24
**Based on:** Supabase Official Documentation

This guide provides recommendations for configuring `supabase/config.toml` to support four environments:

* **Local** - Local CLI development (Docker)
* **Dev** - Persistent dev project (`zkgxozahyczcnzpwhbbf`); per-PR preview DBs created by Branching from PRs targeting GitHub `prod`
* **Staging (Persistent)** - UAT and pre-prod validation
* **Production** - Live application (`srcaoozjkrughebmbvfb`); per-PR preview DBs created by Branching only for release PRs targeting GitHub `production`

> **Authoritative branch ↔ Vercel ↔ Supabase mapping:** see
> [`VERCEL_SUPABASE_ENV_ALIGNMENT.md`](../VERCEL_SUPABASE_ENV_ALIGNMENT.md).
> When that document and this one disagree, the alignment doc wins.

> **2026-04 reconcile:** the legacy production project (`zkgxozahyczcnzpwhbbf`)
> was repurposed as Dev. Two new clean projects were created from a single
> optimized baseline migration. See
> SUPABASE\_RECONCILE\_2026-04.md for the
> cutover record.

**As of 2026-04-22 — source of truth and deploy path**

* **Dev** (`zkgxozahyczcnzpwhbbf`, “Encore OS”): Lovable + Supabase MCP apply migrations and deploy Edge Functions here first; this database is the live schema reference for day-to-day work.
* **Git repo** (`supabase/migrations/`, `supabase/functions/`): canonical history for review and for **production** rollout.
* **Production** (`srcaoozjkrughebmbvfb`, “encore\_os\_prod”): **Supabase Branching** (GitHub integration) replays migrations for release PRs and, with **Deploy to production** ON, applies them on merge to `production`. CI still runs **`supabase db push --yes`** from [`.github/workflows/supabase-deploy-prod.yml`](../../../.github/workflows/supabase-deploy-prod.yml) on the same push as a reconcile with the repo. **Break-glass:** manual `workflow_dispatch` on that workflow with **`skip_db_push`** when migrations were already fully applied elsewhere (document in your ops log).

Do **not** point ad-hoc `db push` at Dev for schema promotion; treat Dev as MCP-driven. Use GitHub `prod` → optional [`.github/workflows/supabase-deploy-dev.yml`](../../../.github/workflows/supabase-deploy-dev.yml) backstop, and `production` → prod.

### Bidirectional sync (Dev ↔ repo)

Two paths keep Dev and the repo aligned:

* **Repo → Dev (push):** Author routine DDL declaratively under [`supabase/schemas/`](../../../supabase/schemas/), then run `npm run db:schemas:diff -- -f <slug>` (wraps `supabase stop` + `supabase db diff -f <slug>`) to emit the migration in `supabase/migrations/`. Reserve `supabase migration new` for caveat migrations (DML, certain `ALTER POLICY` / view ownership / grant / comment cases — see [DECLARATIVE\_SCHEMA\_GUIDE.md](DECLARATIVE_SCHEMA_GUIDE.md) and [MIGRATION\_LANES.md](MIGRATION_LANES.md)). The migrations land on the Dev project when [`supabase-deploy-dev.yml`](../../../.github/workflows/supabase-deploy-dev.yml) runs on its branch trigger (see workflow table below).
* **Dev → repo (pull):** Migrations applied directly to Dev by Lovable / Supabase MCP are captured back into `supabase/migrations/` by [`supabase-sync-from-dev.yml`](../../../.github/workflows/supabase-sync-from-dev.yml). It runs daily at 06:00 UTC and on-demand via `workflow_dispatch`. The workflow runs `supabase db diff --linked -f <slug>` to produce a real timestamped migration, validates it with `db reset` + the RLS / FK / lane guards, refreshes the declarative tree under `supabase/schemas/` via `npm run db:schemas:export` when bootstrapped (so the repo's source of truth stays current), regenerates `src/integrations/supabase/types.ts`, and opens a review PR to `dev`. Developers can run the same flow locally with `npm run db:pull-from-dev -- --name <slug>`.

## Promotion path

```
Local (Docker)  ->  GitHub `prod` / Dev project  ->  GitHub `production` / Prod project
        |                      |                              |
        +-- migration tests    +-- Lovable + MCP (primary)    +-- Branching preview (required check) → merge
        |                      |                              +-- CI db push + functions deploy (reconcile; break-glass skip_db_push)
```

**Release PRs (`prod` → `production`):** Require green **Supabase Preview** on `encore_os_prod` before merge. Order of operations and branch protection are documented in [`VERCEL_SUPABASE_ENV_ALIGNMENT.md`](../VERCEL_SUPABASE_ENV_ALIGNMENT.md) section 2.3. CLI spike notes: [`BRANCHING_CI_SPIKE.md`](./BRANCHING_CI_SPIKE.md).

Optional persistent staging (separate Supabase project) can sit between Dev and Prod when provisioned; there is no shared staging ref in `config.toml` until you add one.

***

## Recommended Configuration Structure

### Option 1: Remote-Specific Configuration (RECOMMENDED)

This approach uses Supabase's `[remotes]` configuration blocks to define environment-specific settings. This is the **recommended approach** per Supabase documentation.

```toml theme={null}
# supabase/config.toml (illustrative — see repo file for the live copy)

# Default configuration (used for local development)
# project_id is implied for `supabase start`

[api]
enabled = true
port = 54321
schemas = ["public", "storage", "graphql_public"]

[db]
port = 54322

# Production remote — encore_os_prod
[remotes.production]
project_id = "srcaoozjkrughebmbvfb"

[remotes.production.api]
max_rows = 500

# Dev remote — Encore OS (Lovable / MCP target)
[remotes.dev]
project_id = "zkgxozahyczcnzpwhbbf"

[remotes.dev.api]
max_rows = 1000

# Optional: add [remotes.staging] when a dedicated UAT project exists.
```

### Option 2: Environment Variable-Based (Alternative)

If you prefer to switch environments via environment variables:

```toml theme={null}
# supabase/config.toml

# Use environment variable to determine project
project_id = "env(SUPABASE_PROJECT_ID)"

[api]
enabled = true
port = 54321
schemas = ["public", "storage", "graphql_public"]

[db]
port = 54322
pool_size = 10
```

Then use different `.env` files:

* `.env.local` → `SUPABASE_PROJECT_ID=local-dev`
* `.env.staging` → `SUPABASE_PROJECT_ID=<staging-ref>`
* `.env.production` → `SUPABASE_PROJECT_ID=srcaoozjkrughebmbvfb`

**Note:** Option 1 is preferred because it provides better separation and allows environment-specific configurations.

***

## Workflow by Environment

### Local Development

**Purpose:** Feature development, migration testing, rapid iteration

**Setup:**

```bash theme={null}
# Start local Supabase stack
supabase start

# This uses the default config (no remote specified)
# Local services available at:
# - API: http://localhost:54321
# - Studio: http://localhost:54323
# - DB: localhost:54322
```

**Commands:**

```bash theme={null}
# Create new migration
supabase migration new my_feature

# Apply migrations locally
supabase db reset  # Resets and applies all migrations
# OR
supabase migration up  # Applies pending migrations

# Test migrations
supabase db diff  # See what would change

# Generate types
supabase gen types typescript --local > src/integrations/supabase/types.ts
```

**Configuration:** Uses default `[api]`, `[db]` settings in `config.toml`

***

### Staging (Persistent UAT)

**Purpose:** User acceptance testing, integration testing, pre-production validation

**Setup:**

```bash theme={null}
# Link to staging project
supabase link --project-ref <STAGING_PROJECT_REF>

# Verify connection
supabase status
```

**Deploying to Staging:**

```bash theme={null}
# Push migrations to staging
supabase db push

# Deploy edge functions
supabase functions deploy

# Set secrets (if needed)
supabase secrets set --env-file .env.staging
```

**Configuration:** Uses `[remotes.staging]` block from `config.toml`

**Best Practices:**

* Keep staging schema in sync with production
* Use synthetic test data (no PHI/PII)
* Test all migrations in staging before production
* Use staging for UAT sign-offs

***

### Production

**Purpose:** Live application serving real users

**Canonical mapping:** GitHub branch `production`, project ref `srcaoozjkrughebmbvfb` — see [`VERCEL_SUPABASE_ENV_ALIGNMENT.md`](../VERCEL_SUPABASE_ENV_ALIGNMENT.md).

**Setup:**

```bash theme={null}
# Link to production project (encore_os_prod)
supabase link --project-ref srcaoozjkrughebmbvfb

# Verify connection (be careful!)
supabase status
```

**Deploying to Production:**

```bash theme={null}
# ⚠️ ALWAYS test in staging first!

# Push migrations to production
supabase db push

# Deploy edge functions
supabase functions deploy

# Set secrets (if needed)
supabase secrets set --env-file .env.production
```

**Configuration:** Uses `[remotes.production]` block from `config.toml`

**Best Practices:**

* **NEVER** deploy untested migrations to production
* Always deploy to staging first
* Use production for verified, tested changes only
* Monitor deployments closely
* Have rollback plan ready

***

## Environment Switching Workflow

### Recommended: Use Separate Git Branches

```bash theme={null}
# Local development (default)
git checkout feature/my-feature
supabase start  # Uses local config

# Staging deployment
git checkout staging
supabase link --project-ref <STAGING_PROJECT_REF>
supabase db push

# Production deployment (GitHub branch `production`, not `prod`)
git checkout production
supabase link --project-ref srcaoozjkrughebmbvfb
supabase db push
```

### Alternative: Manual Linking

```bash theme={null}
# Switch to staging
supabase link --project-ref <STAGING_PROJECT_REF>

# Switch to production (encore_os_prod)
supabase link --project-ref srcaoozjkrughebmbvfb

# Switch back to local
supabase unlink  # Then use supabase start for local
```

***

## Configuration Options Reference

### API Configuration

```toml theme={null}
[api]
enabled = true
port = 54321
schemas = ["public", "storage", "graphql_public"]
extra_search_path = ["public", "extensions"]
max_rows = 1000  # Limit rows returned per request
```

### Database Configuration

```toml theme={null}
[db]
port = 54322
pool_size = 10  # Connection pool size
major_version = 15  # Postgres version
```

### Remote-Specific Overrides

```toml theme={null}
[remotes.staging]
project_id = "<STAGING_PROJECT_REF>"

# Override API settings for staging
[remotes.staging.api]
max_rows = 2000  # Staging can handle more rows

# Override DB settings for staging
[remotes.staging.db]
pool_size = 20
```

***

## Secrets Management

### Local Development

Secrets are loaded from `.env` file in project root:

```bash theme={null}
# .env (gitignored)
SUPABASE_AUTH_GITHUB_CLIENT_ID="local-dev-id"
SUPABASE_AUTH_GITHUB_SECRET="local-dev-secret"
```

Reference in `config.toml`:

```toml theme={null}
[auth.external.github]
enabled = true
client_id = "env(SUPABASE_AUTH_GITHUB_CLIENT_ID)"
secret = "env(SUPABASE_AUTH_GITHUB_SECRET)"
```

### Remote Environments (Staging/Production)

Set secrets via CLI:

```bash theme={null}
# Set secrets for linked project
supabase secrets set GITHUB_CLIENT_ID=xxx GITHUB_SECRET=yyy

# Or from env file
supabase secrets set --env-file .env.staging
```

**Important:** Secrets are **project-specific**. You must set them separately for staging and production.

***

## Migration Workflow

### Recommended Flow

```
1. Local Development
   └─> Create migration locally
   └─> Test with `supabase db reset`
   └─> Commit to feature branch

2. Staging Deployment
   └─> Merge to staging branch
   └─> Link to staging: `supabase link --project-ref <STAGING_PROJECT_REF>`
   └─> Push migrations: `supabase db push`
   └─> Verify in staging dashboard

3. Production Deployment
   └─> Merge to `production` branch (release PR from `prod`)
   └─> Link to production: `supabase link --project-ref srcaoozjkrughebmbvfb`
   └─> Push migrations: `supabase db push` (prefer CI: [`.github/workflows/supabase-deploy-prod.yml`](../../../.github/workflows/supabase-deploy-prod.yml))
   └─> Monitor production dashboard
```

### CI/CD Integration

For automated deployments, use GitHub Actions with environment-specific secrets:

```yaml theme={null}
# .github/workflows/deploy-staging.yml
name: Deploy to Staging

on:
  push:
    branches: [staging]

jobs:
  deploy:
    runs-on: ubuntu-latest
    env:
      SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
      SUPABASE_PROJECT_ID: ${{ secrets.STAGING_PROJECT_ID }}
      SUPABASE_DB_PASSWORD: ${{ secrets.STAGING_DB_PASSWORD }}
    
    steps:
      - uses: actions/checkout@v4
      - uses: supabase/setup-cli@v1
      - run: supabase link --project-ref $SUPABASE_PROJECT_ID
      - run: supabase db push
```

***

## Best Practices

### 1. Always Test Locally First

* Use `supabase db reset` to test migrations from scratch
* Verify migrations are idempotent (can be run multiple times safely)

### 2. Staging Before Production

* **NEVER** deploy directly to production
* Always validate in staging first
* Use staging for UAT sign-offs

### 3. Environment Isolation

* Keep production and staging completely separate
* Never use production data in staging
* Use synthetic test data in staging

### 4. Configuration as Code

* Keep `config.toml` in version control
* Use `[remotes]` blocks for environment-specific settings
* Document any manual configuration changes

### 5. Secrets Management

* Never commit secrets to git
* Use `.env` files for local development (gitignored)
* Use `supabase secrets set` for remote environments
* Rotate secrets regularly

### 6. Migration Safety

* Always review migrations before deploying
* Test rollback procedures
* Use transactions where possible
* Avoid breaking changes when possible

***

## Troubleshooting

### Issue: Wrong Project Linked

**Symptom:** `supabase db push` deploys to wrong environment

**Solution:**

```bash theme={null}
# Check current link
supabase status

# Unlink and re-link
supabase unlink
supabase link --project-ref <correct-project-id>
```

### Issue: Config Not Applied

**Symptom:** Remote-specific config not taking effect

**Solution:**

* Verify `project_id` in `[remotes.*]` matches actual project ID
* Use `supabase config push` to sync config to remote
* Check that you're linked to correct project

### Issue: Secrets Not Available

**Symptom:** Edge functions can't access secrets

**Solution:**

```bash theme={null}
# List secrets for current project
supabase secrets list

# Set missing secrets
supabase secrets set KEY=value

# Verify secrets are set
supabase secrets list
```

***

## Migration from Current Setup

### Current State

* `config.toml` has single `project_id = "<STAGING_PROJECT_REF>"` (staging)
* Manual switching between environments

### Recommended Migration Steps

1. **Backup current config:**
   ```bash theme={null}
   cp supabase/config.toml supabase/config.toml.backup
   ```

2. **Update config.toml:**
   * Add default local configuration
   * Add `[remotes.production]` block
   * Add `[remotes.staging]` block
   * Keep current staging project\_id

3. **Test local development:**
   ```bash theme={null}
   supabase start
   # Verify local stack works
   ```

4. **Test staging link:**
   ```bash theme={null}
   supabase link --project-ref <STAGING_PROJECT_REF>
   supabase status
   ```

5. **Test production link:**
   ```bash theme={null}
   supabase link --project-ref srcaoozjkrughebmbvfb
   supabase status
   ```

6. **Update documentation:**
   * Update `docs/development/ENVIRONMENT_CONFIG.md` with new workflow
   * Document switching process for team

***

## Database connections for scripts (pooler, not direct)

Supabase’s **direct** Postgres hostname (`db.<project-ref>.supabase.co`) is often **IPv6-only**. Many local networks and CI runners only resolve IPv4, which surfaces as `ENOTFOUND` or connection timeouts when using `pg`, `psql`, or `pg_dump`.

**Use the Session pooler** for any script that opens a raw Postgres connection from a laptop or CI:

* Dashboard → **Project Settings** → **Database** → **Connection string** → **Session mode** (Supavisor, port **5432**).
* URI shape: `postgresql://postgres.<PROJECT_REF>:<PASSWORD>@aws-0-<REGION>.pooler.supabase.com:5432/postgres`

This repo’s seed tooling reads:

* `SUPABASE_DEV_DB_URL` — dev (`zkgxozahyczcnzpwhbbf`), pooler URI.
* `SUPABASE_PROD_DB_URL` — prod (`srcaoozjkrughebmbvfb`), pooler URI.

Keep these in **`.env.local`** only (gitignored). See `.env.local.example`.

### Applying the global system-defaults bundle to prod

1. Regenerate `supabase/seeds/system-defaults/00_global_system_defaults.sql` (`npm run seed:extract-system-defaults` or `--source rest`).
2. Prefer recording the load in migration history: in Cursor, use the Supabase MCP **`apply_migration`** tool against **`srcaoozjkrughebmbvfb`** with migration name `seed_global_system_defaults_bundle` and `query` set to the file contents (same SQL as the SQL Editor path).
3. Fallback: paste the file into the prod project **SQL Editor** and run.
4. **Fresh production project (empty / first-time):** run the manual GitHub Actions workflow **Bootstrap Supabase (production)** (see workflow table below). It runs `supabase db push --include-all`, then `supabase db query --linked --file` on `00_global_system_defaults.sql`, then optional `npm run check:prod-seed-parity` when `SUPABASE_PROD_DB_URL` is configured as a secret.

### Migration lanes (schema vs system data)

New migrations must follow the lane rules in [MIGRATION\_LANES.md](./MIGRATION_LANES.md) so CI can block dev-only URLs, JWTs in SQL, and non-idempotent catalog inserts after the strict cutoff timestamp.

### Ongoing parity check

`npm run check:prod-seed-parity` compares prod catalog row counts to dev (if `SUPABASE_DEV_DB_URL` is set) or to documented minimum baselines. It **skips** when `SUPABASE_PROD_DB_URL` is unset (e.g. CI without secrets). Set that variable in CI to enforce the gate in automation.

## GitHub Actions (Supabase deploy + drift)

Workflows live under [`.github/workflows/`](../../../.github/workflows/):

| Workflow                      | Trigger                                                                                                        | Action                                                                                                                                                                                            |
| ----------------------------- | -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `supabase-deploy-prod.yml`    | Push to **`production`** touching `supabase/migrations/**`, `supabase/functions/**`, or `supabase/config.toml` | `supabase link` → `srcaoozjkrughebmbvfb` → post-link migration audit → `db push --yes` → deploy **changed** Edge Functions only (or all if `_shared` / config / unknown `before` SHA)             |
| `supabase-bootstrap-prod.yml` | **Manual** `workflow_dispatch` with confirm `bootstrap-new-prod`                                               | `db push --include-all` → `db query --linked` system-defaults bundle → optional seed parity                                                                                                       |
| `supabase-deploy-dev.yml`     | Push to **`prod`** with same path filters (or manual)                                                          | `supabase link` → **soft drift check** (auto-dispatches `supabase-sync-from-dev.yml` if drift exists; bypass via `force_deploy_dev`) → `db push --yes` → selective function deploy                |
| `supabase-sync-from-dev.yml`  | **Daily 06:00 UTC** + manual `workflow_dispatch` with `migration_name`                                         | `supabase db diff --linked -f <slug>` → validates with `db reset` + RLS / FK / lane guards → regenerates `src/integrations/supabase/types.ts` → opens PR to **`dev`** with the captured migration |
| `supabase-promote-pr.yml`     | PR → **`production`** when migrations (or lane script) change                                                  | Migration lane report + sticky PR comment                                                                                                                                                         |
| `db-migration-guard.yml`      | PR → **`main`** or **`production`** when migrations change                                                     | RLS init-plan + FK index checks; migration lane on **`main`** PRs only                                                                                                                            |
| `build.yml`                   | PR → **`main`**, **`production`**, or `feature/**`                                                             | Full app CI gate                                                                                                                                                                                  |

**Repository secrets (Actions → Secrets and variables → Actions):**

| Secret                            | Used by                                                                                |
| --------------------------------- | -------------------------------------------------------------------------------------- |
| `SUPABASE_ACCESS_TOKEN`           | Supabase CLI auth (deploy, drift, bootstrap)                                           |
| `SUPABASE_PROD_DB_PASSWORD`       | `supabase-deploy-prod.yml`, `supabase-bootstrap-prod.yml` (`supabase link --password`) |
| `SUPABASE_DEV_DB_PASSWORD`        | `supabase-deploy-dev.yml`, `supabase-drift-check.yml`                                  |
| `SUPABASE_PROD_DB_URL` (optional) | `supabase-bootstrap-prod.yml` seed parity step                                         |

`db diff --linked` requires Docker on the runner; if the drift job logs a CLI/Docker failure, fix runner setup or run the diff locally.

## References

* [Supabase Configuration Documentation](https://supabase.com/docs/guides/deployment/branching/configuration)
* [Managing Environments Guide](https://supabase.com/docs/guides/deployment/managing-environments)
* [Local Development Guide](https://supabase.com/docs/guides/local-development)
* [CLI Config Reference](https://supabase.com/docs/reference/cli/config)

***

## Summary

**Recommended Approach:** Use `[remotes]` configuration blocks in `config.toml` to define environment-specific settings. This provides:

✅ Clear separation between environments\
✅ Environment-specific configuration overrides\
✅ Version-controlled configuration\
✅ Easy switching via `supabase link`\
✅ Support for different settings per environment

**Workflow:**

1. **Local:** `supabase start` (uses default config), then `supabase db reset` when testing migrations.
2. **Dev (Encore OS):** `zkgxozahyczcnzpwhbbf` — primary schema changes via Lovable + MCP; optional GitHub `prod` branch workflow mirrors repo → Dev.
3. **Production (`encore_os_prod`):** `srcaoozjkrughebmbvfb` — promote via GitHub `production` branch (CI) or manual `supabase link` + `db push` + `functions deploy` following SUPABASE\_RECONCILE\_2026-04.md.

This approach aligns with Supabase best practices and provides a scalable, maintainable multi-environment setup.
