TML-3035: a first Supabase project works first-try — RLS migrations, verify, migrate, JWKS, grants#997
Conversation
The Postgres migration renderer emitted RLS ops as free-function calls (createRlsPolicy(...), enableRowLevelSecurity(...)) importing from @prisma-next/postgres/migration. Those symbols were never exported, so a generated migration.ts for any RLS contract failed to import — and exporting them cannot fix it, because the underlying op factories take an ExecuteRequestLowerer argument the rendered call site has no way to supply. db init masked this by applying ops.json without importing the module. Render RLS ops the way every other lowerer-requiring op renders: as this.-method calls on the migration base class, which lowers through the migration's stored control adapter. Adds the five RLS methods to PostgresMigration (createRlsPolicy, dropRlsPolicy, renameRlsPolicy, enableRowLevelSecurity, disableRowLevelSecurity), exports the five RLS call classes from the op-factory-call facade, and deletes the importRequirements() overrides on the two planner-internal NOT NULL calls that render rawSql(...) but declared no import (same defect class). Regression tests: a structural test renders one call of every op-factory-call class (a new class fails the test until covered) and asserts every symbol the rendered module imports from the facade is actually exported by it; a second test locks the this.-method render shape for all five RLS ops. Relates to TML-3035. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Extends the tsx-execution roundtrip with all five RLS op kinds (enable, createPolicy, renamePolicy, dropPolicy, disable) so the rendered migration.ts for RLS contracts is proven to parse, typecheck via tsx, and lower back to the exact renderOps(calls) operations. This is the test that exposed the free-function RLS rendering defect. Relates to TML-3035. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
The ./test/utils subpath (bootstrapSupabaseShim) has never been runnable from npm: it reads test/fixtures/supabase-reference/*.sql at runtime, and the package files list ships only dist and src, so every external call fails with ENOENT before touching a database. No deprecation is needed — no consumer outside the monorepo can ever have run it. Drop the export and its tsdown build entry. The shim stays in-repo test tooling: the extension's own tests already import it by relative source path, and examples/supabase now does the same instead of going through the published subpath. Relates to TML-3035. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…formation_schema db verify against a real Supabase instance reported the pack's own constraints (auth.schema_migrations PK, storage.migrations PK + unique, storage.buckets_vectors PK, storage.vector_indexes PK + FK) as not-found while they provably exist. Mechanism: information_schema.table_constraints hides constraints on tables where the connecting role's only privilege is SELECT — its visibility filter grants INSERT/UPDATE/DELETE/TRUNCATE/REFERENCES/TRIGGER but not SELECT, while information_schema.tables and .columns do include SELECT. On real Supabase the connecting role can only SELECT the platform's internal tables, so introspection returned them with all columns but zero constraints and verify flagged every declared PK/unique/FK missing. Rewrite the PK, unique, and FK introspection queries against pg_catalog.pg_constraint (not privilege-filtered), pairing columns via unnest(conkey) WITH ORDINALITY and mapping confdeltype/confupdtype to the same rule strings information_schema reported. The constraint-backed-index exclusion in the index query moves from a name-keyed information_schema lookup to pg_constraint.conindid, which also fixes the long-standing P2-1 defect where two tables sharing a constraint name merged their FK columns — the it.fails marker on "drops the correct FK when two tables share a constraint name" now passes and is flipped to a regular test. Regression test reproduces the Supabase failure shape exactly: a table owned by another role with SELECT-only granted to the connecting role must introspect with its PK, unique, FK, and index, and verify must report no issues. Relates to TML-3035. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…en RLS render typing Both 0.15-to-0.16 upgrade recipes gain a change entry for the removed @prisma-next/extension-supabase ./test/utils export: the import typechecked but never ran from the published package (unpublished fixtures), so the migration is deleting the import; the testing story is Supabase's own tooling (supabase test db / pgTAP). CreatePostgresRlsPolicyCall.renderTypeScript() annotates its hand-built literal as PostgresRlsPolicyInput so drift from the input type fails to compile, and the migration roundtrip policy gains a withCheck predicate so the ifDefined serialization path executes end-to-end. Relates to TML-3035. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…m index introspection For FK constraints, pg_constraint.conindid points at the unique index on the REFERENCED table, and Postgres accepts FKs that reference a plain CREATE UNIQUE INDEX with no constraint object. The constraint-backed-index exclusion introduced with the pg_catalog rewrite matched those rows too, hiding the referenced index from introspection and making verify report a declared index as missing. Restrict the exclusion to constraints that own their index (contype p/u/x); no conrelid guard, so self-referential FKs stay correct. Regression test: an FK referencing a plain unique index — the index must keep introspecting on the referenced table while constraint-backed indexes stay excluded. Confirmed red with the filter removed. Gate note: the prior test:packages run failed only on a 100ms-timeout scheduling flake in @prisma-next/family-mongo (untouched package); the suite passes in isolation (14 files / 170 tests green). Relates to TML-3035. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…tively
A Supabase-extension project has a contract space that pins a head ref
but ships zero migration packages by design (the pack's auth/storage
tables are external; it emits no DDL). migrate treated every
empty-graph space not already at its head as "never planned" and failed
PN-RUN-3000 — and its remediation was circular: it prescribed
`migration plan --to <hash>`, but full-hash contract refs only resolve
against migration-graph nodes, so the empty graph rejected the very
hash the error supplied. db init was the only path.
The model already decides this case elsewhere, in the same words: the
db-init aggregate planner resolves empty-graph extension spaces via a
declared-state zero-op plan ("every real case is an all-external
extension space (e.g. Supabase's auth/storage)"), and check-integrity
codifies "when a space ships no migrations, the planner emits no DDL
for it — the database is treated as already at the declared state".
migrate now mirrors that: an empty-graph EXTENSION space resolves
declaratively (marker advances to its head ref, zero operations, inside
the per-space transaction), with the same invariant guard db init
applies; the never-planned failure remains for the APP space, where an
authored graph is genuinely required. The "Already up to date"
short-circuit now counts a marker advance as pending work so a
declared-state resolution actually executes.
The remaining never-planned remediation is now runnable verbatim: the
fix prescribes `prisma-next migration plan --name <slug>` (the bare
form targets the working contract — the same contract the app head ref
is synthesized from) instead of a `--to <hash>` the empty graph cannot
resolve. The e2e test executes the printed commands literally and
migrate succeeds afterwards.
Relates to TML-3035.
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
… in a typed error
Current Supabase projects sign ES256 via JWKS by default, while
`supabase status` still prints a JWT_SECRET that only fits legacy HS256
projects. A user wiring that secret in got a raw jose internal at
asUser ("Key for the ES256 algorithm must be one of type CryptoKey...
Received an instance of Uint8Array") that never names the problem.
verifyJwt now decodes the token header before verification and raises
InvalidJwtError (the error asUser callers already catch) when the
algorithm cannot match the configured key material, in both directions:
an asymmetric token (ES*/RS*/PS*/EdDSA) against jwtSecret says to
configure jwksUrl (https://<project>/auth/v1/.well-known/jwks.json);
an HS* token against jwksUrl says a legacy project needs jwtSecret.
The jwks-side check fires before any JWKS fetch.
Relates to TML-3035.
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…es; ES256/JWKS coverage bootstrapSupabaseShim granted service_role ALL on auth.*/storage.* and anon/authenticated SELECT — privileges a real Supabase withholds. The reference fixture already carries the real GRANT / ALTER DEFAULT PRIVILEGES statements verbatim (including default privileges for the postgres role, which tests connect as), so the shim now restores the fixture and adds nothing: its job is fidelity to a fresh `supabase db reset`. A shim more permissive than production let tests pass that fail on a real project (findings sections 6 and 8). Fallout lands where the docs point users: tests exercising the `.supabase` admin root issue the narrow grant in their own setup (GRANT USAGE ON SCHEMA auth TO service_role; GRANT SELECT ON TABLE auth.users TO service_role — refresh_tokens/sessions for the tests reading those tables). The skeleton cascade test gains a RESET ROLE before its platform-admin delete: the shared PGlite session keeps the session-level role GUC a role-bound runtime set, and the over-grant had been masking that leak. The real-Supabase acceptance harness does not touch the admin root, so it needs no grant. ES256/JWKS coverage: the RLS role-binding suite gains a hermetic test that generates a P-256 keypair, serves a JWKS from a local HTTP server, and proves the full asUser path (ES256 verify via jwksUrl, then role binding and RLS-scoped reads) — the default configuration of a current Supabase project. Relates to TML-3035. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
The prisma-next-supabase skill and package README described a Supabase that no longer exists: they led with jwtSecret (current projects sign ES256 via JWKS), claimed service_role holds grants on auth./storage. (a real project grants it nothing there), told users to grant roles access to their own public tables (Supabase default privileges already cover them), and documented the deleted test/utils shim as the testing path. Rewritten: jwksUrl-first JWT guidance with the legacy-HS256 demotion, the narrow one-time service_role grant for admin reads, inverted grants guidance, and an RLS testing story built on supabase start + supabase test db (pgTAP). Refs: TML-3035 Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
The fidelity class of bug (TML-3035 findings sections 5/6/8) shipped because nothing exercised the extension against real Supabase defaults: the acceptance harness was env-guarded and never ran in CI. New supabase-acceptance job in ci.yml: supabase start in examples/supabase (minimal stock supabase/config.toml added), credentials exported from supabase status, then the acceptance suite. Follows the sibling jobs exactly — plain pull_request/merge_group triggers, per-step inert-diff skip (any packages/** or examples/** change is non-inert, so extension-supabase, examples/supabase, and postgres target/adapter changes always run it), setup composite + Turbo cache, supabase/setup-cli pinned by SHA. The harness itself now exercises the stack real signing config: a new test signs up a user through GoTrue and verifies its issued token (ES256 on a current stack) through the project /auth/v1/.well-known/jwks.json endpoint, gated on SUPABASE_URL/SUPABASE_ANON_KEY. The three existing flows keep self-minted HS256 tokens using the JWT secret the stack provides. The manual per-table public.profile grants are gone — a stock project covers app tables via default privileges (findings section 7) — along with the stale comment claiming the shim fabricates grants. README updated to the CI-run story. Branch protection note: the job is in the required workflow (CI (PR)) following the sibling pattern; the ruleset that marks jobs required is managed outside the repo and needs "Supabase Acceptance" added. Relates to TML-3035. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…e RLS fixture The restored Supabase reference fixture carries the platform default privileges for the postgres role, so tables dbInit creates in public already grant anon/authenticated/service_role — the per-table GRANTs in applyGrantsFixture duplicated that and modelled the inverted guidance findings section 7 corrected. Only the PGlite WAL-schema accommodation remains. Relates to TML-3035. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…ed-index test tsc-based typecheck flagged the untyped introspect() result (the descriptor-typed adapter returns the wide schema-IR node); cast to PostgresDatabaseSchemaNode like the sibling introspection tests. Relates to TML-3035. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
The owner-scoped assertion was count(*) >= 0 — vacuously true. Seed an auth user + profile and assert exact counts for both roles. Refs: TML-3035 Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…restoreSupabaseReference bootstrapSupabaseShim had become a thin wrapper over restoreSupabaseReference, and the READMEs already describe the substrate by the inner name. Delete the wrapper (no alias kept): the fixture module test/fixtures/supabase-reference/restore.ts is THE entrypoint, its header absorbs the wrapper docs (fidelity contract, narrow-grant guidance), and every in-repo caller imports restoreSupabaseReference — extension tests directly, examples/supabase through its renamed test/supabase-reference.ts bridge. The historical mentions of bootstrapSupabaseShim in the 0.14-to-0.15 and 0.15-to-0.16 upgrade instructions stay: they describe the removed 0.15 export as it appears in user code. Relates to TML-3035. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Prisma Next 0.15.0 + Supabase — findings from building an app
Seven defects found while building a working notes app on The app itself works and is in this repo: contract → One theme worth reading first. The extension's testing story rests entirely on
So the suite passes green against a database that is both more permissive and differently configured than any real project, and a user following the skill hits §5 and §6 on first contact. Raising the shim's fidelity — or adding one acceptance test against a real Supabase — would have caught both, and is worth more than patching the individual symptoms. Environment
Reproduction repo: the external agent's directory. 1.
|
| # | Finding | Severity | Shape of fix |
|---|---|---|---|
| 2 | Generated migration.ts can't import → migrate broken for all RLS contracts |
High | Export the symbols, or fix the renderer; typecheck generated output in CI |
| 5 | Skill leads with jwtSecret; current Supabase is ES256/JWKS |
High | Skill rewrite + actionable runtime error + ES256 test coverage |
| 6 | service_role lacks auth.* grants; .supabase root fails |
High | Correct the skill; align shim with real defaults |
| 1 | Shim's fixtures unpublished → hermetic tests impossible from npm | High | Add test/fixtures to files; test against packed tarball |
| 3 | migrate's remediation is circular for external spaces |
Medium | Skip external spaces, or provide a command that plans the edge |
| 4 | db verify false negatives on pack tables |
Medium | Fix constraint introspection/matching |
| 7 | Grants guidance inverted | Low | Skill edit |
What worked
Worth recording, since the above is all negative:
contract emitaccepted the cross-space FK,@@rls, and all sevenpolicy_*blocks first try, with no iteration.- The planner emitted
REFERENCES "auth"."users"("id")and zero DDL againstauth/storage— exactly as documented. db initapplied tables, FK,ENABLE ROW LEVEL SECURITY, and every policy correctly, and the emitted SQL matched the contract's intent verbatim.- Role binding works, and RLS enforcement is genuinely correct:
anonsees only published rows, owners see their own,service_rolebypasses, cross-ownerUPDATEmatches 0 rows,WITH CHECKrejects forged ownership. - The role-first design (no
db.ormuntil a role is bound) is a good call and reads well at the call site. - Error envelopes are excellent where correct —
PN-RUN-3000withwhy/fix/metamade §3 diagnosable in one shot. The problem there was the content offix, not the format.
Addendum (operator decision, 2026-07-16)
Supersedes §1's suggested fix and sharpens the theme:
- §1 becomes "delete", not "fix".
bootstrapSupabaseShimhas been exported-but-broken for at least three minor versions; no external user can ever have run it, and nobody reported it. There is nothing to deprecate: drop./test/utilsfromexports, keep the shim internal for the extension's own tests. Do not publish the fixtures. - The user-facing testing story is Supabase's own tooling.
supabase test dbruns pgTAP against the local containers, alongsidesupabase start/db reset. pgTAP fits exactly what users would test ("assertanoncannot see this row"), and users already run the CLI for local dev. Rewrite the skill's "Testing without a Supabase project" section accordingly. - The disqualifying argument is the skill's own text: "RLS is enforced by policies and grants." The shim's grants differ from real Supabase in both directions, so it cannot faithfully test RLS — the only reason a user would reach for it. Green-locally-broken-in-prod is worse than no test. The shim's world also got written down as guidance (the skill's grant advice mirrors the shim's
ALTER DEFAULT PRIVILEGESline-for-line), so the shim actively produced §6/§7. - §8 (new finding): the fidelity blind spot is PN's, not just the user's. Un-exporting the shim fixes the user-facing story but not the pipeline that shipped §5/§6: internal tests still run against unfaithful grants and HS256-only tokens, and the real-Supabase acceptance harness (
examples/supabase/test/real-supabase.acceptance.test.ts) is env-guarded and does not run in CI. Making that acceptance run non-optional is what actually catches this class of bug.
📝 WalkthroughWalkthroughThis PR adds live Supabase acceptance CI, migrates Supabase tests to internal fixtures, improves JWT mismatch errors, updates empty-space migration behavior, adds PostgreSQL catalog introspection, and changes PostgreSQL RLS migration rendering to object-based instance calls. ChangesSupabase runtime and acceptance
Migration planning
PostgreSQL introspection and RLS rendering
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant CI
participant SupabaseCLI
participant AcceptanceTest
participant SupabaseRuntime
CI->>SupabaseCLI: Start local Supabase stack
SupabaseCLI-->>CI: Return database and JWT credentials
CI->>AcceptanceTest: Run real-Supabase acceptance suite
AcceptanceTest->>SupabaseRuntime: Configure JWKS and validate user JWT
SupabaseRuntime-->>AcceptanceTest: Apply RLS-scoped user query
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
@prisma-next/extension-author-tools
@prisma-next/mongo-runtime
@prisma-next/family-mongo
@prisma-next/sql-runtime
@prisma-next/family-sql
@prisma-next/extension-arktype-json
@prisma-next/middleware-cache
@prisma-next/mongo
@prisma-next/extension-paradedb
@prisma-next/extension-pgvector
@prisma-next/extension-postgis
@prisma-next/postgres
@prisma-next/sql-orm-client
@prisma-next/sqlite
@prisma-next/extension-supabase
@prisma-next/target-mongo
@prisma-next/adapter-mongo
@prisma-next/driver-mongo
@prisma-next/contract
@prisma-next/utils
@prisma-next/config
@prisma-next/errors
@prisma-next/framework-components
@prisma-next/operations
@prisma-next/ts-render
@prisma-next/contract-authoring
@prisma-next/ids
@prisma-next/psl-parser
@prisma-next/psl-printer
@prisma-next/cli
@prisma-next/cli-telemetry
@prisma-next/config-loader
@prisma-next/emitter
@prisma-next/language-server
@prisma-next/migration-tools
prisma-next
@prisma-next/vite-plugin-contract-emit
@prisma-next/mongo-codec
@prisma-next/mongo-contract
@prisma-next/mongo-value
@prisma-next/mongo-contract-psl
@prisma-next/mongo-contract-ts
@prisma-next/mongo-emitter
@prisma-next/mongo-schema-ir
@prisma-next/mongo-query-ast
@prisma-next/mongo-orm
@prisma-next/mongo-query-builder
@prisma-next/mongo-lowering
@prisma-next/mongo-wire
@prisma-next/sql-contract
@prisma-next/sql-errors
@prisma-next/sql-operations
@prisma-next/sql-schema-ir
@prisma-next/sql-contract-psl
@prisma-next/sql-contract-ts
@prisma-next/sql-contract-emitter
@prisma-next/sql-lane-query-builder
@prisma-next/sql-relational-core
@prisma-next/sql-builder
@prisma-next/target-postgres
@prisma-next/target-sqlite
@prisma-next/adapter-postgres
@prisma-next/adapter-sqlite
@prisma-next/driver-postgres
@prisma-next/driver-sqlite
commit: |
size-limit report 📦
|
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (2)
skills/prisma-next-supabase/SKILL.md (1)
134-136: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueEnsure strict TypeScript safety in configuration snippet.
In TypeScript strict mode,
process.env['XYZ']is inferred asstring | undefined, buturlandjwksUrlrequire astring. Consider appending the non-null assertion operator (!) to match the exact snippet pattern used inpackages/3-extensions/supabase/README.md.💡 Proposed consistency fix
- url: process.env['DATABASE_URL'], // direct Postgres connection — see pitfalls - jwksUrl: process.env['SUPABASE_JWKS_URL'], // https://<project-ref>.supabase.co/auth/v1/.well-known/jwks.json + url: process.env['DATABASE_URL']!, // direct Postgres connection — see pitfalls + jwksUrl: process.env['SUPABASE_JWKS_URL']!, // https://<project-ref>.supabase.co/auth/v1/.well-known/jwks.json // Legacy HS256 projects use jwtSecret: process.env['SUPABASE_JWT_SECRET'] instead — exactly one of the two.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@skills/prisma-next-supabase/SKILL.md` around lines 134 - 136, Update the configuration snippet’s process.env values for url and jwksUrl to use non-null assertions, matching the established pattern in the Supabase README and satisfying strict TypeScript string requirements.examples/supabase/test/supabase-reference.ts (1)
1-2: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAvoid re-exporting files.
As per coding guidelines, do not re-export from one file in another unless it is located within an
exports/folder. Consider importingrestoreSupabaseReferencedirectly in the consumer test files and removing this intermediate module.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@examples/supabase/test/supabase-reference.ts` around lines 1 - 2, Remove the intermediate re-export of restoreSupabaseReference from supabase-reference.ts, and update consumer test files to import restoreSupabaseReference directly from its fixture module. Keep re-exports limited to files within an exports/ folder.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@examples/supabase/README.md`:
- Around line 49-53: Update the environment-variable setup in the README example
to export DATABASE_URL, SUPABASE_JWT_SECRET, SUPABASE_URL, and SUPABASE_ANON_KEY
before invoking pnpm test, ensuring the acceptance tests receive the credentials
and do not silently skip.
In `@examples/supabase/test/rls-role-binding.integration.test.ts`:
- Around line 365-387: Move the supabase<Contract>() client creation into an
outer try/finally that owns JWKS server cleanup, so failures during client
initialization still execute jwksServer.close(). Preserve the existing
db.close() cleanup for successfully created clients, while ensuring the server
is closed exactly once across both success and failure paths.
In `@packages/1-framework/3-tooling/cli/src/control-api/operations/migrate.ts`:
- Around line 408-436: The non-app zero-operation path must only apply to
extensions whose contract is explicitly all-external. In migrate.ts, update the
extension handling around the existing isAppSpace check and
buildAtHeadResolution call to validate the extension contract first, returning
the existing missing-graph unsatisfiable result for managed extensions while
preserving declarative advancement for all-external ones. In
packages/1-framework/3-tooling/cli/test/control-api/migrate-plan-space-path.test.ts
lines 44-77, change the fixture to explicitly represent an all-external
extension and add coverage confirming a managed extension is rejected.
In `@test/integration/test/cli.migrate-external-space.e2e.test.ts`:
- Around line 199-228: Strengthen the remediation assertions around
errorJson.fix by validating the complete expected remediation text, including
all commands, flags, and configuration. Parse or extract the suggested plan and
migrate commands from fix, substitute the slug, and pass those derived arguments
to runMigrationPlan and runMigrate instead of hardcoding them, while preserving
the existing success assertions.
---
Nitpick comments:
In `@examples/supabase/test/supabase-reference.ts`:
- Around line 1-2: Remove the intermediate re-export of restoreSupabaseReference
from supabase-reference.ts, and update consumer test files to import
restoreSupabaseReference directly from its fixture module. Keep re-exports
limited to files within an exports/ folder.
In `@skills/prisma-next-supabase/SKILL.md`:
- Around line 134-136: Update the configuration snippet’s process.env values for
url and jwksUrl to use non-null assertions, matching the established pattern in
the Supabase README and satisfying strict TypeScript string requirements.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: f85e3d1a-8058-453b-9f1c-05504131433a
📒 Files selected for processing (49)
.github/workflows/ci.ymlexamples/supabase/README.mdexamples/supabase/supabase/.gitignoreexamples/supabase/supabase/config.tomlexamples/supabase/test/explicit-namespace-query.integration.test.tsexamples/supabase/test/native-enum-session.integration.test.tsexamples/supabase/test/real-supabase.acceptance.test.tsexamples/supabase/test/rls-role-binding.integration.test.tsexamples/supabase/test/skeleton.integration.test.tsexamples/supabase/test/supabase-bootstrap.tsexamples/supabase/test/supabase-reference.tspackages/1-framework/3-tooling/cli/src/control-api/operations/migrate.tspackages/1-framework/3-tooling/cli/src/utils/cli-errors.tspackages/1-framework/3-tooling/cli/test/cli-errors.test.tspackages/1-framework/3-tooling/cli/test/control-api/migrate-plan-space-path.test.tspackages/3-extensions/supabase/README.mdpackages/3-extensions/supabase/package.jsonpackages/3-extensions/supabase/src/runtime/supabase.tspackages/3-extensions/supabase/test/brownfield-infer.integration.test.tspackages/3-extensions/supabase/test/classification.e2e.test.tspackages/3-extensions/supabase/test/cross-contract-fk.integration.test.tspackages/3-extensions/supabase/test/fixtures/supabase-reference/restore.tspackages/3-extensions/supabase/test/infer-cross-space-fk.integration.test.tspackages/3-extensions/supabase/test/roles-verify.integration.test.tspackages/3-extensions/supabase/test/service-role-refresh-tokens.integration.test.tspackages/3-extensions/supabase/test/supabase-bootstrap.tspackages/3-extensions/supabase/test/supabase-facade.test.tspackages/3-extensions/supabase/tsdown.config.tspackages/3-targets/3-targets/postgres/src/core/migrations/op-factory-call.tspackages/3-targets/3-targets/postgres/src/core/migrations/postgres-migration.tspackages/3-targets/3-targets/postgres/src/exports/op-factory-call.tspackages/3-targets/3-targets/postgres/test/migrations/op-factory-call.lowering.test.tspackages/3-targets/3-targets/postgres/test/migrations/render-typescript.test.tspackages/3-targets/3-targets/postgres/test/migrations/rls-disable-rename-ops.test.tspackages/3-targets/6-adapters/postgres/src/core/control-adapter.tspackages/3-targets/6-adapters/postgres/test/control-adapter.defaults.test.tspackages/3-targets/6-adapters/postgres/test/control-adapter.test.tspackages/3-targets/6-adapters/postgres/test/migrations/introspect-fk-referenced-unique-index.integration.test.tspackages/3-targets/6-adapters/postgres/test/migrations/introspect-select-only-privileges.integration.test.tspackages/3-targets/6-adapters/postgres/test/migrations/planner.reconciliation.integration.test.tspackages/3-targets/6-adapters/postgres/test/migrations/render-typescript.roundtrip.test.tsskills/extension-author/prisma-next-extension-upgrade/upgrades/0.15-to-0.16/instructions.mdskills/prisma-next-contract/SKILL.mdskills/prisma-next-supabase/SKILL.mdskills/upgrade/prisma-next-upgrade/upgrades/0.15-to-0.16/instructions.mdtest/integration/test/cli.migrate-external-space.e2e.test.tstest/integration/test/contract-space-fixture/external-space.tstest/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/migrate-external-space/contract.tstest/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/migrate-external-space/prisma-next.config.with-db.ts
💤 Files with no reviewable changes (4)
- examples/supabase/test/supabase-bootstrap.ts
- packages/3-extensions/supabase/test/supabase-bootstrap.ts
- packages/3-extensions/supabase/package.json
- packages/3-extensions/supabase/tsdown.config.ts
…li isn't org-allowlisted The org restricts workflow actions to an allowlist and supabase/setup-cli is not on it, so the CI (PR) workflow hit startup_failure and ran no jobs at all. Replace the action step with a direct install: download the pinned v2.95.4 linux_amd64 release tarball, verify its sha256 against the release's published checksums file (hard-coded so the pin is real), and put the binary on PATH. Bumping the CLI is a two-line change (version + sha). Everything else in the job is unchanged. Relates to TML-3035. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…tension — examples model only public API examples/ are user-facing, and the example hermetic lane imported the monorepo-internal test substrate through a relative path into the extension package — unresolvable for anyone who copies the example out of the repo. The four hermetic suites (rls-role-binding, skeleton, explicit-namespace-query, native-enum-session) plus the no-policy / renamed-policy variant fixtures move into packages/3-extensions/supabase/test/, alongside the eight sibling suites that already use the substrate directly. They exercise the same contract via a new test fixture app (test/fixtures/example-app: the example contract shape plus db/session-queries helpers); test names and assertions are unchanged. The fixture contracts stay pipeline-emitted: the package gains an emit script wired into the root fixtures:emit (regen verified drift-free), and the extension vitest config takes over the PGlite crash mitigations (execArgv + CI retry). examples/supabase now ships exactly what a user can run: the real-Supabase acceptance suite (skip-guarded without a stack) and the compile-only type test. Its README run snippet also becomes a single command with inline env vars — the previous shape set the variables as standalone assignments that never reached pnpm test, so the suite silently skipped. The JWKS test (at its new home) closes its local HTTP server even when client creation rejects, so a failed supabase() call cannot leak the listener. Relates to TML-3035. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
… migrations The declared-state resolution (advance a space marker to its head with zero operations) keyed on graph emptiness — plus extension-ness on the migrate path. The real precondition is the property those checks assumed: every element the space declares is externally managed (control policy external, ADR 224). Only then can the storage structure be correct without a migration having produced it; an extension that declares a managed element but ships no migration graph is an authoring bug and must fail loudly, not silently advance. New shared predicate allStorageElementsExternal (migration-tools aggregate) walks elementCoordinates over the composed contract storage and resolves each element via effectiveControlPolicy (element control, then the contract defaultControlPolicy). Both consumers now guard on it: the db-init aggregate planner (declared-state branch fails extensionPathUnreachable) and the CLI migrate planSpacePath (falls through to never-planned, whose wording now names the enforced rule). Comments that stated "every real case is an all-external space" as an assumption now describe the check. Red-first at both layers: an empty-graph space with one managed element fails loudly (db-init: extensionPathUnreachable; migrate: never-planned), while the all-external shape (the Supabase case, now declared via defaultControlPolicy on the e2e fixture) still advances exactly as before. Relates to TML-3035. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
The "remediation runs verbatim" e2e asserted two substrings of the fix and then executed independently hard-coded commands, so a drifted remediation could keep passing. It now asserts the exact numbered command list and derives each executed argv from the printed text itself (substituting the <slug> placeholder; harness plumbing like --config/--json appended), tolerating only incidental whitespace. Relates to TML-3035. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…aseMockSchema restoreSupabaseReference was opaque about what callers get: a test database set up with the mock Supabase schema (the reference fixture: schemas, tables, roles, real default privileges). Rename the function and its module (test/fixtures/supabase-reference/set-up-mock-schema.ts) and update every caller and the README bullet; no alias kept. The fixture directory name is unchanged. Also serializes the extension vitest run (pool forks, one worker, per-file isolation kept for the pg-mocking facade test): the suite now hosts the moved reference-restoring integration tests, and parallel PGlite databases made the heaviest WASM restore crash-prone locally. Relates to TML-3035. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/3-extensions/supabase/test/fixtures/supabase-reference/set-up-mock-schema.ts (1)
64-77: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winGuarantee session cleanup when fixture replay fails.
If either SQL query rejects after changing
row_securityorsearch_path,RESET ALLis skipped and the caller-owned client remains contaminated.Proposed fix
- await client.query(rolesSql); - await client.query(schemaSql); + try { + await client.query(rolesSql); + await client.query(schemaSql); + } finally { + await client.query('RESET ALL'); + } // pg_dump's preamble includes session-local `SET`s ... - await client.query('RESET ALL');🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/3-extensions/supabase/test/fixtures/supabase-reference/set-up-mock-schema.ts` around lines 64 - 77, Ensure the fixture setup around rolesSql and schemaSql always executes RESET ALL even when either client.query call rejects. Wrap the replay queries in cleanup-safe control flow, preserving the original query error while guaranteeing session settings are reset on every exit path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In
`@packages/3-extensions/supabase/test/fixtures/supabase-reference/set-up-mock-schema.ts`:
- Around line 64-77: Ensure the fixture setup around rolesSql and schemaSql
always executes RESET ALL even when either client.query call rejects. Wrap the
replay queries in cleanup-safe control flow, preserving the original query error
while guaranteeing session settings are reset on every exit path.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: c0e9b8c7-26f5-4042-a958-1fc1e1ef356b
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (44)
.github/workflows/ci.ymlexamples/supabase/README.mdexamples/supabase/package.jsonexamples/supabase/test/real-supabase.acceptance.test.tsexamples/supabase/vitest.config.tspackage.jsonpackages/1-framework/3-tooling/cli/src/control-api/operations/migrate.tspackages/1-framework/3-tooling/cli/test/control-api/migrate-plan-space-path.test.tspackages/1-framework/3-tooling/migration/src/aggregate/all-external.tspackages/1-framework/3-tooling/migration/src/aggregate/planner.tspackages/1-framework/3-tooling/migration/src/exports/aggregate.tspackages/1-framework/3-tooling/migration/test/aggregate/planner.test.tspackages/3-extensions/supabase/README.mdpackages/3-extensions/supabase/package.jsonpackages/3-extensions/supabase/scripts/generate-contract.tspackages/3-extensions/supabase/test/brownfield-infer.integration.test.tspackages/3-extensions/supabase/test/classification.e2e.test.tspackages/3-extensions/supabase/test/cross-contract-fk.integration.test.tspackages/3-extensions/supabase/test/explicit-namespace-query.integration.test.tspackages/3-extensions/supabase/test/fixtures/example-app.config.tspackages/3-extensions/supabase/test/fixtures/example-app/contract.d.tspackages/3-extensions/supabase/test/fixtures/example-app/contract.jsonpackages/3-extensions/supabase/test/fixtures/example-app/contract.prismapackages/3-extensions/supabase/test/fixtures/example-app/db.tspackages/3-extensions/supabase/test/fixtures/example-app/session-queries.tspackages/3-extensions/supabase/test/fixtures/no-policy.config.tspackages/3-extensions/supabase/test/fixtures/no-policy/contract.d.tspackages/3-extensions/supabase/test/fixtures/no-policy/contract.jsonpackages/3-extensions/supabase/test/fixtures/no-policy/contract.prismapackages/3-extensions/supabase/test/fixtures/renamed-policy.config.tspackages/3-extensions/supabase/test/fixtures/renamed-policy/contract.d.tspackages/3-extensions/supabase/test/fixtures/renamed-policy/contract.jsonpackages/3-extensions/supabase/test/fixtures/renamed-policy/contract.prismapackages/3-extensions/supabase/test/fixtures/supabase-reference/set-up-mock-schema.tspackages/3-extensions/supabase/test/infer-cross-space-fk.integration.test.tspackages/3-extensions/supabase/test/native-enum-session.integration.test.tspackages/3-extensions/supabase/test/reference-fixture-verify.integration.test.tspackages/3-extensions/supabase/test/rls-role-binding.integration.test.tspackages/3-extensions/supabase/test/roles-verify.integration.test.tspackages/3-extensions/supabase/test/service-role-refresh-tokens.integration.test.tspackages/3-extensions/supabase/test/skeleton.integration.test.tspackages/3-extensions/supabase/vitest.config.tstest/integration/test/cli.migrate-external-space.e2e.test.tstest/integration/test/contract-space-fixture/external-space.ts
💤 Files with no reviewable changes (1)
- examples/supabase/vitest.config.ts
🚧 Files skipped from review as they are similar to previous changes (7)
- packages/3-extensions/supabase/test/brownfield-infer.integration.test.ts
- packages/1-framework/3-tooling/cli/test/control-api/migrate-plan-space-path.test.ts
- .github/workflows/ci.yml
- test/integration/test/cli.migrate-external-space.e2e.test.ts
- test/integration/test/contract-space-fixture/external-space.ts
- packages/1-framework/3-tooling/cli/src/control-api/operations/migrate.ts
- examples/supabase/test/real-supabase.acceptance.test.ts
…c lane The hermetic PGlite suites moved to the extension package; the example tests only against a live supabase start stack. Refs: TML-3035 Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Here is what a first-time user of
@prisma-next/extension-supabase0.15.0 hits, installing from npm and following our own skill against a stock Supabase project — five steps, five failures:This PR makes all five work (or fail with a remediation that works when followed verbatim), and adds the check that would have caught most of them before release: the real-Supabase acceptance suite now runs against a live
supabase startstack on every PR, as a required job.The findings come from an external agent run that built a notes app on 0.15.0; its full report is the first comment on this PR. The app's core worked — contract emit, cross-space FKs, RLS enforcement itself were all correct. Everything broken was in the surrounding surface, and none of it was visible to our own test suite. The common cause: our hermetic test shim provisioned a Supabase that no real project matches — it granted
service_roleprivileges real Supabase withholds, and minted only HS256 tokens where current Supabase signs ES256 via JWKS. Green tests, against a database no user has.1. Generated migrations now import, typecheck, and run (RLS contracts)
The renderer emitted RLS operations as free-function calls —
enableRowLevelSecurity("public", "profile")— importing from@prisma-next/postgres/migration. Those names were never exported, and exporting them can't fix it: the underlying factories take anExecuteRequestLowererargument that a rendered call site has no way to supply.db initmasked all of this by applyingops.jsonwithout ever importing the module.RLS operations now render as methods on the migration base class —
this.createRlsPolicy({...})— the same pattern ascreateTable, lowering through the migration's stored control adapter. Two structural guards keep the whole class fixed, not just these five ops:The sweep also caught a latent same-class bug:
addNotNullColumn's two op classes renderrawSql(...)but declared no import requirement.2.
db verifyno longer false-negatives on Supabase's own tablesAgainst a clean Supabase database, verify reported six constraints as
not-found— all on platform-internal tables (auth.schema_migrations,storage.migrations, …) — whilepsqlshowed them present with the exact expected names and columns.Root cause: introspection read
information_schema.table_constraints, whose visibility filter deliberately omits SELECT (unlikeinformation_schema.tables/columns, which include it). On Supabase, the connectingpostgresrole has full privileges on most pack tables but only SELECT on the platform's internal ones — so those tables introspected as present, with columns, and zero constraints. Introspection now readspg_catalog.pg_constraint(not privilege-filtered), keyed by table oid.Keying by oid instead of by constraint name also fixed a known marked bug: two tables in one schema sharing an FK name used to get their columns merged (the
it.failstest for it now passes and is un-marked). And a review round caught a regression the rewrite would have introduced — an FK referencing a plainCREATE UNIQUE INDEXmade that index vanish from introspection — fixed with acontypefilter and its own regression test.3.
migratehandles extension-pack contract spacesThe Supabase pack is an external contract space: it declares what Supabase owns so the planner emits no DDL for it, and it ships no migrations — by design.
migratedidn't know that: it required an on-disk migration graph for every space, and for the pack space it printed a remediation (step 2 of the console session above) thatmigration planitself rejects, because the pack's hash is a head ref, not a graph node. The user's only path forward wasdb init.The model already existed:
db init's aggregate planner resolves such spaces to a declared-state zero-op plan targeting the head ref.migratenow does exactly that — the space's marker advances to head inside the ordinary per-space transaction, and "Already up to date" means what it says.The precondition is enforced, not assumed: advancing a storage hash without a migration is valid exclusively for a space whose every element is externally managed — that's the only way storage structure can be correct without a migration having changed it. A shared predicate (
allStorageElementsExternal, walking the space's elements' effective control policy) guards bothdb initandmigrate; a no-migrations extension space that declares even one managed element fails loudly instead of advancing. App spaces still require an authored graph, and their never-planned error now prints a remediation the e2e test parses and executes verbatim — a driftedfixstring fails the test.4. JWT misconfiguration gets a real error, and both signing modes are tested
Current Supabase projects sign ES256 with asymmetric keys and publish a JWKS endpoint;
supabase statusstill prints aJWT_SECRET, so wiringjwtSecretis everyone's natural first move — and it failed with the rawjoseinternal in step 4 above.asUsernow detects the mismatch before verification and throwsInvalidJwtErrorwith the actual problem and fix:The reverse direction (HS256 token,
jwksUrlclient) gets the same treatment, and fires before any JWKS fetch. Hermetic coverage now includes the full ES256/JWKS path: an in-test P-256 keypair, a local JWKS server, and the complete asUser → role-binding → RLS-scoped-read flow.5. The grants story is now told in the right direction
Two inversions, both traceable to the shim:
service_rolenothing onauth.*/storage.*(only schemaUSAGE; onlypostgresholds table privileges). The documented admin root (db.asServiceRole().supabase) therefore fails out of the box — step 5 above. Our shim ranGRANT ALL ON ALL TABLES IN SCHEMA auth TO service_role, so every test passed. The shim now restores the reference fixture's real default privileges and nothing more — removing the over-grant reproduced the user's exact42501hermetically — and the docs state the narrow one-time grant the admin root needs:GRANT USAGE ON SCHEMA auth TO service_role; GRANT SELECT ON TABLE auth.users TO service_role;publictables need no grants at all. Supabase shipsALTER DEFAULT PRIVILEGESonpublic, so tables created bydb init/migrateinherit platform-role access automatically; RLS is what protects the rows. The skill told users to run per-table grants (up toGRANT ALL) — guidance that mirrored the shim's grant block line-for-line. Inverted in the skill, and the example app no longer issues the redundant grants either.Dropping the over-grant also unmasked a test that believed it was querying as an admin but was actually running as
service_rolethrough a leaked session variable.6. The broken
./test/utilsexport is deleted, not fixedbootstrapSupabaseShim— the skill's documented hermetic-testing entrypoint — has been exported-but-broken from npm for at least three minor versions: its SQL fixtures were never in the package'sfiles, so every consumer install fails with ENOENT before touching a database. Nobody ever reported it, so nobody ever ran it; there is nothing to deprecate.The subpath is removed (a breaking change, recorded in both the app-user and extension-author 0.15→0.16 upgrade instructions — the import typechecked fine before, so user code plausibly contains it). The substrate stays in-repo as
setUpSupabaseMockSchemafor the extension's own tests. The user-facing testing story in the skill is now Supabase's own tooling —supabase start+supabase test db(pgTAP), which tests policies and grants against the real platform defaults.The same boundary is applied to the example app: its hermetic PGlite suites imported the internal substrate by relative path — a testing setup no user copying the example could run. Those suites moved into the extension package (against a checked-in fixture app), and
examples/supabase's own test suite is now exactly what a user gets: the acceptance run against their ownsupabase start.7. The class of bug is now structurally caught: a required live-Supabase CI job
Everything above shipped because the only tests were hermetic and the hermetic environment was unfaithful. The env-guarded acceptance suite (
examples/supabase/test/real-supabase.acceptance.test.ts) now runs on every PR: a newsupabase-acceptancejob boots a stocksupabase startinexamples/supabaseand runs the suite with the stack's real credentials — including a new flow that signs up a user through GoTrue and verifies its issued ES256 token through the project's live JWKS endpoint. It passed its first execution on this PR.Ops follow-up: the branch ruleset (managed outside the repo) needs "Supabase Acceptance" added to its required checks.
Alternatives considered
SupabaseConfigErrorfor the algorithm mismatch (the report's suggestion). The mismatch is only observable per-token atasUser, where callers already catchInvalidJwtError(with its typedreason); config errors stay factory-time. Better ergonomics, same information.supabase/setup-cliin org settings. The org restricts Actions to an allowlist, and the action's absence failed the whole CI workflow at startup (producing zero check runs — deceptively green ingh pr checks). The job instead installs the pinned CLI release binary (v2.95.4, sha256-verified against the release's checksums file), which keeps the pin real and avoids a standing org-settings dependency.Refs: TML-3035
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Documentation