Skip to content
Merged
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
6 changes: 4 additions & 2 deletions docs/declarative-row-security.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,8 +279,10 @@ this table-scoped work.
4. **Prove application behavior.** The local Supabase harness now checks real
PostgREST requests from two authenticated users and anonymous callers, allowed
and denied writes, changed visibility, and rollback after a cancelled apply.
Hosted connection and privilege validation remains a follow-up on a disposable
project; local results do not establish hosted support.
The [hosted suite](../integration/supabase/hosted/README.md) also exercised
preview, apply, convergence, tenant access, and atomic rollback as the owning
`postgres` role on a disposable project. Hosted non-owner privilege validation
remains a follow-up; owner-role results do not establish that boundary.

The [inspection tests](../pkg/schemadiff/row_security_integration_test.go),
[round-trip tests](../pkg/schemadiff/row_security_roundtrip_integration_test.go), and
Expand Down
60 changes: 36 additions & 24 deletions docs/supabase.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ Your Supabase app runs on PostgreSQL. pg-sprite helps you change its tables as
you build: add a column for a new feature, or add an index as your queries grow.

Local tests cover column additions and concurrent index builds alongside
Supabase's access policies, Data API, and Realtime subscriptions. Hosted projects
are the next validation step. Changes that need a replacement table are refused
today because copy-and-swap is not implemented yet.
Supabase's access policies, Data API, and Realtime subscriptions. An opt-in [hosted suite](../integration/supabase/hosted/README.md) also exercises
real Auth users and service endpoints. Its Realtime schema change cases initialize
one shared fixture and verify baseline delivery before applying changes, then
check that events and tenant isolation survive on the same connections. Changes
that need a replacement table are refused today because copy-and-swap is not
implemented yet.

Start with [your first change](#make-your-first-change). The [test results](#what-works-today)
and [roadmap](#where-we-go-next) show how far the current coverage goes.
Expand All @@ -32,7 +35,7 @@ and the [Data API](https://supabase.com/docs/guides/api) for more detail.

This walkthrough adds a nullable `title` column to an existing `public.documents`
table. Substitute your own app table and column. Start on a development project;
the compatibility results below come from local Supabase services.
the capability matrix below describes the pinned local Supabase services.

### Install pg-sprite

Expand All @@ -57,7 +60,7 @@ See [engine-role.md](engine-role.md) for the operation-specific grants.
Supavisor offers two pooling modes:

- **Session mode** keeps the same PostgreSQL connection for the client's session.
The tested session endpoint works with pg-sprite and can help on IPv4-only networks
The locally tested session endpoint works with pg-sprite and can help on IPv4-only networks
- **Transaction mode** can assign a different PostgreSQL connection after each
transaction. Do not use it for pg-sprite: execution limits need a stable session

Expand All @@ -74,18 +77,20 @@ Replace the example value with your connection string. This sets an environment
variable without opening a connection. Keep credentials out of source control;
your secret manager can also set this variable for you or your agent.

For hosted connections, use `sslmode=verify-full` in the URL to verify the server's
certificate and hostname. If you need to supply a CA certificate separately,
save the certificate for your project and point pg-sprite at it:
For certificate and hostname verification, download the CA from **Database
Settings → SSL Configuration** and follow [Supabase's verification instructions](https://supabase.com/docs/guides/platform/ssl-enforcement#a-note-about-postgres-ssl-modes).
Use `sslmode=verify-full` with `sslrootcert` in the URL, or point pg-sprite at the CA:

```sh
export PGSPRITE_CA_CERT='/absolute/path/to/project-ca.crt'
```

That variable sets the certificate file used by the commands below. A certificate
error should be fixed by checking the hostname and trusted certificate, rather
than disabling verification. Hosted certificate handling remains a validation
milestone for this guide.
than disabling verification. Hosted checks passed with the default URI,
`sslmode=require`, and `verify-full` with the downloaded CA. The default connection
used TLS, which alone does not establish server identity verification. An unrelated
CA and a mismatched expected hostname were rejected in the [hosted TLS tests](../integration/supabase/hosted/tls_test.go).

### Preview the change

Expand Down Expand Up @@ -189,7 +194,9 @@ The result lists the statements committed in one transaction. Applying it again
reports that row security already matches. A mixed column/index and RLS edit
refuses without committing either part. See [output and failure handling](declarative-row-security.md#apply-the-declaration).
This CLI route is covered by PostgreSQL integration tests; local Supabase API
coverage uses the same executor. Hosted validation remains a separate step.
coverage uses the same executor. The [hosted suite](../integration/supabase/hosted/README.md)
also exercised preview, apply, convergence, and atomic rollback as the owning
`postgres` role. Hosted non-owner privilege validation remains a follow-up.

## What works today

Expand Down Expand Up @@ -257,26 +264,31 @@ outcome; they do not assume every unsuccessful change rolls back completely.
- Realtime coverage is limited to INSERT/UPDATE subscriptions during the tested
native changes. Deletes, reconnect recovery, column removal, and table
replacement need separate validation
- PostgREST cache refresh was exercised with the image's schema-change event triggers;
- PostgREST cache refresh was exercised with the image's schema change event triggers;
a deployment without those triggers needs its own reload workflow

Hosted role configuration and TLS remain unverified. The Auth
service runs its schema initialization; JWTs are signed by the test fixture,
so this does not test signup or login. These local results are not an
unrestricted Supabase support claim.
Hosted checks have passed as the owning `postgres` role on PostgreSQL 17.6,
including CA-based TLS verification, the CLI schema lifecycle, real Auth defaults
and foreign keys, and atomic RLS rollback. Hosted poolers remain untested and
Realtime checks cover continuity after fixture initialization; see the
[hosted suite](../integration/supabase/hosted/README.md) for cases and limits.
In the local suite, Auth runs its schema initialization and the fixture signs JWTs,
so those cases do not test login. Hosted cases create
confirmed test users through the Auth admin API and sign in with their passwords;
they do not test public signup, email delivery, or OAuth. Neither suite establishes
unrestricted Supabase support.

## Where we go next

The next milestones build on the local tests. Each needs repeatable evidence
The next milestones build on the local and scoped hosted tests. Each needs repeatable evidence
before we expand the support claim:

1. **Validate hosted projects.** Run the same checks on a disposable Supabase
project, including certificate verification, network access, and hosted roles
2. **Validate declarative schema workflows.** Export an existing Supabase schema,
edit the desired SQL files, preview the diff, and apply supported changes.
Verify that the live schema matches the files and a second diff is empty,
while access policies and Realtime subscriptions still work. Make clear which
objects the files describe and which remain managed separately
1. **Finish hosted validation.** Core direct-connection and TLS cases have passed;
validate the hosted pooler endpoints
2. **Publish a reproducible declarative workflow.** The hosted CLI cases cover
create, export, edit, apply, RLS, and convergence. Complete a fresh-project
walkthrough from the published guide, with clear boundaries for managed objects
and Realtime behavior
3. **Cover more app workflows.** Exercise deletes and reconnects in Realtime,
Realtime payloads after column renames and removals, and real signup/login
flows. Make the limits of desired schema files and access-policy handling
Expand Down
6 changes: 6 additions & 0 deletions integration/supabase/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,9 @@ Handle unrelated upgrades in a separate follow-up. When making an upgrade:

This check is triggered by agent work, not a scheduled update bot. Fresh CI
runners pull the pinned images on each run, which also exposes unavailable pins.

## Hosted projects

The separate [hosted suite](hosted/README.md) uses real Auth users and managed
endpoints with an explicit opt-in. Do not redirect this local fixture at a hosted
project: its fixed names, JWT signer, and Docker lifecycle are local-only.
149 changes: 149 additions & 0 deletions integration/supabase/hosted/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# Hosted Supabase checks

Use this suite on a **disposable hosted project**. It creates real Auth users,
application tables, policies, and publication entries, then removes its fixtures.
It leaves project settings alone. No Docker services or locally signed JWTs are used.

This is an opt-in complement to the [local suite](../README.md), not a replacement
for its larger DDL matrix or a required hosted CI job. Realtime checks establish
working delivery before applying schema changes, then verify continuity on the
same table and connections. Setup failures fail the test; writes are never retried.

## Run it

Create a disposable Supabase project and copy its **Direct connection** details
from **Connect**. Use a private PostgreSQL password file or your secret manager;
never check credentials into the repository. Keep the creation defaults if you
want to test the normal new-project experience.

In **Settings → API Keys**, copy the publishable and secret keys into a local JSON
file with mode `0600`. The secret key is used only to create and delete test users;
application requests use the publishable key and real user access tokens.

```json
{
"publishable_key": "YOUR_PUBLISHABLE_KEY",
"secret_key": "YOUR_SECRET_KEY"
}
```

Set the paths and project details, then run the suite from the repository root:

```sh
export SUPABASE_HOSTED_TEST=1
export SUPABASE_HOSTED_URL='https://YOUR_PROJECT_REF.supabase.co'
export SUPABASE_HOSTED_CREDENTIALS='/absolute/path/to/credentials.json'
export PGSPRITE_URL='postgresql://postgres@db.YOUR_PROJECT_REF.supabase.co:5432/postgres'
export PGPASSFILE='/absolute/path/to/pgpass'
go build -o ./bin/pg-sprite ./cmd/pg-sprite
export SUPABASE_HOSTED_BIN="$PWD/bin/pg-sprite"
go test -count=1 -timeout=10m -v ./integration/supabase/hosted
```

A successful case prints `--- PASS: TestHostedDeclarativeRLS`. With the opt-in
unset, cases print a skip reason and make no database or API requests. The baseline
requires a direct hostname matching the API project. Do not run multiple copies
against the same project: publication changes affect its shared Realtime service.

To test the session pooler, set `SUPABASE_HOSTED_SESSION_URL` to its URL from
**Connect** and add a password-file entry for that exact endpoint. Its host,
port, and username differ from the direct endpoint. A missing URL produces an
explicit skip.

Transaction-pooler refusal stays in the [controlled local suite](../../../pkg/dbconn/supabase_integration_test.go).
An idle hosted transaction pooler can reuse one backend and pass the session
probe, so a hosted assertion cannot reliably prove refusal. A passing probe
does not make transaction pooling supported; use direct or session connections.

To run the CLI lifecycle independently of Realtime and pooler tests:

```sh
go test -count=1 -timeout=5m -v ./integration/supabase/hosted -run '^TestHostedCLI'
```

These cases do not change Realtime publication membership. Passing them establishes
only the capabilities below; it does not clear failures in the separate Realtime
cases. For example, a successful column case prints
`--- PASS: TestHostedCLIAddColumn`.

## TLS checks without API keys

TLS tests use only `PGSPRITE_URL`, `PGPASSFILE`, and the opt-in. Supply the direct
URL with `sslmode=verify-full` and `sslrootcert` pointing at the CA downloaded from
**Database Settings → SSL Configuration**. Then run:

```sh
go test -count=1 -timeout=2m -v ./integration/supabase/hosted -run '^TestHostedTLS'
```

Expected cases are `TestHostedTLSDefault`, `TestHostedTLSRequire`,
`TestHostedTLSVerifyFull`, `TestHostedTLSRejectUntrustedCA`, and
`TestHostedTLSRejectWrongHostname`. Successful connections assert encryption using
`pg_stat_ssl`; negative cases require the specific certificate-trust or hostname
error, not just any failed connection. Verification cases skip explicitly when
`sslrootcert` is absent. The hostname negative case changes the expected TLS name
while still dialing the real database; it uses the underlying pgx driver to inject
that mismatch. These tests make no schema changes and do not test server-side
rejection of plaintext connections.

## What the cases establish

| Case | Passing result |
| --- | --- |
| `TestHostedTableCleanup` | Cleanup handles absent tables and tables committed before a later error |
| `TestHostedCLIAuthDefault` | A desired `auth.uid()` default uses the real HTTP caller; spoofing another owner is denied |
| `TestHostedCLIAuthForeignKey` | A validated reference to real Auth users rejects orphan writes |
| `TestHostedCLIUniqueIndexFailure` | Duplicate data causes a typed failure and a reported invalid index; rows and access survive |
| `TestHostedRLSCancellationPreservesAccess` | Cancellation after a live policy drop rolls back the transaction; read and write isolation survive |
| `TestHostedCLICreateExportAndRLS` | Desired table creation, export/diff convergence, explicit RLS preview/apply, real tenant API isolation, and an unchanged second apply |
| `TestHostedCLIAddColumn` | A defaulted column is applied without losing existing data or RLS access controls |
| `TestHostedCLIAddIndex` | The new index is valid; table identity, rows, policies, and grants remain unchanged |
| `TestHostedCLIFailedNotNull` | Failed validation preserves NULL data and tenant access, leaves the column nullable, and reports the retained unvalidated helper constraint |
| `TestHostedCLIRefuseDropColumn` | Destructive preview and apply are refused without dropping a column |
| `TestHostedCLIRefuseCopySwap` | The unavailable copy-and-swap plan is refused before its safe prefix can run |
| `TestHostedCLIRLSLockFailure` | A lock-budget failure leaves existing policies and tenant access intact |
| `TestHostedRealtimeContinuity/initialize` | An active reader and a delivered baseline for both tenants precede all pg-sprite DDL |
| `TestHostedRealtimeContinuity/no_DDL_control` | Both tenants receive updates before schema changes |
| `TestHostedRealtimeContinuity/refuse_volatile_UUID` | Imperative and desired UUID-default changes are refused without applying their safe prefix; data and events survive |
| `TestHostedRealtimeContinuity/refuse_volatile_timestamp` | The same refusal checks hold for `clock_timestamp()` |
| `TestHostedRealtimeContinuity/add_column` | A nullable column preserves events and API isolation on the same table and sockets |
| `TestHostedRealtimeContinuity/add_index` | A concurrent index preserves events, API isolation, and table identity |
| `TestHostedDeclarativeRLS` | Policy changes alter real users' HTTP access; removing the last policy denies reads while retaining the data |
| `TestHostedSessionPooler` | A column change succeeds through the supplied session endpoint |

A passing refusal case means **safe rejection**, not support for executing that
DDL. None of these tests establishes support for every Supabase feature or plan.

Each test uses random fixture names and fails on a collision instead of deleting
an existing table. Cleanup has its own bounded context and deletes only objects
created by that case. If the process is forcibly terminated, inspect the test's
`pgsprite_hosted_…` tables and `pgsprite-…@example.com` users before removing any
leftovers. Do not reset `public` or delete unrelated Auth users.

## Realtime fixture lifecycle

`TestHostedRealtimeContinuity` keeps one table, publication membership, two users,
and two sockets for all cases. Setup waits for an active wal2json reader, verifies
the authenticated subscriptions, then sends one baseline update and requires
delivery to both users. Initialization failures fail the test before DDL runs.

The publication-startup budget is 75 seconds; each event must arrive within 30
seconds. Protocol heartbeats keep sockets alive during setup. There are no fixed
readiness sleeps, retried writes, or reconnects. This setup predicate is specific
to the fixture, not a public Supabase readiness API. The suite tests whether
pg-sprite preserves established delivery, not Supabase's cold-start guarantees.

To validate a fixture fix, export the credentials and `SUPABASE_HOSTED_TEST=1`
as shown above, then use the repository's fail-fast, race-enabled procedure:

```sh
: "${SUPABASE_HOSTED_TEST:?Export SUPABASE_HOSTED_TEST=1 first}"
test "$SUPABASE_HOSTED_TEST" = 1 || exit 1
scripts/test-flaky.sh TestHostedRealtimeContinuity 10 ./integration/supabase/hosted
```

The script stops at the first failure. A successful validation prints
`PASSED all 10 iterations`; it never retries a failed run to obtain a pass.

A success line is evidence only when the hosted case ran. Without the opt-in,
Go skips it and the flake script can still report success.
Loading
Loading