Best for
- User has a Supabase project (or wants one) and is wiring Prisma Next into it.
- User wants RLS policies on their tables (policyselect, @@rls, auth.uid()).
- User wants per-request role binding (asUser(jwt), asAnon(), asServiceRole()).
prisma/prisma/skills/prisma-next-supabase/SKILL.md
Use Prisma Next with a Supabase project via `@internal/extension-supabase` — wire `extensions: [supabasePack]`, declare cross-space FKs to `supabase:auth.AuthUser`, author RLS policies (`policy_select` / `policy_update` / `@@rls`, `auth.uid()` predicates), build `db.ts` with the `supabase()` factory, bind roles per request (`asUser(jwt)` / `asAnon()` / `asServiceRole()`), query `auth.*` / `storage.*` via the `db.asServiceRole().supabase` admin root, and validate JWTs (`jwksUrl` for current proje
Decision brief
Edit your data contract. Prisma handles the rest.
Compatibility matrix
| Platform | Status | Evidence | What to check |
|---|---|---|---|
| Codex | Not declared | No explicit evidence | Portability before use |
| Claude Code | Not declared | No explicit evidence | Portability before use |
| Cursor | Not declared | No explicit evidence | Portability before use |
| Gemini CLI | Not declared | No explicit evidence | Portability before use |
Installation
The source command is displayed only when detected. A safe inspection prompt is always available so your agent can explain every action before execution.
npx skills add https://github.com/prisma/prisma --skill "skills/prisma-next-supabase"Inspect the Agent Skill "prisma-next-supabase" from https://github.com/prisma/prisma/blob/689981aeab99ca2918610f1acbe9c335312b15e8/skills/prisma-next-supabase/SKILL.md at commit 689981aeab99ca2918610f1acbe9c335312b15e8. List every install step, command, network request, credential, file read/write, external action, and rollback step. Explain whether it fits my task. Do not install or execute anything until I approve.
Workflow
The concept: the pack registers the Supabase contract space so your contract can reference it and the planner/verifier know what Supabase owns. The extension has no /control subpath yet, so it can't go through the target façade's defineConfig({ extensions: [...] }) — it wires in…
The concept: your models live in your namespaces (public); Supabase's live in the pack's (auth, storage). A relation field typed supabase:auth.AuthUser is a cross-space FK — the planner emits REFERENCES "auth"."users"("id"), and the target table is verified, never migrated. RLS…
The concept: instead of the stock postgres() factory, a Supabase app builds its client with supabase() from the extension's /runtime subpath. The factory is async (it prepares JWT key material — including the one-time JWKS fetch when jwksUrl is set), and the result is role-first.
The concept: bind the role that should execute the request, then query through the returned RoleBoundDb — every query surface from prisma-next-queries works, RLS-filtered by Postgres.
The concept: Supabase-internal tables are not part of your contract, so they are not on your query surfaces. The servicerole binding carries a secondary root — db.asServiceRole().supabase — which is the pack's contract surface:
Permission review
The documentation asks the agent to read local files, directories, or repositories.
// anon may read every profile (a public directory listing).The documentation includes network, browsing, or remote request actions.
jwksUrl: process.env['SUPABASE_JWKS_URL'], // https://<project-ref>.supabase.co/auth/v1/.well-known/jwks.jsonEvidence record
| Signal | Value | Evidence type | Meaning |
|---|---|---|---|
| Quality score | 94/100 | Computed | Documentation, specificity, maintenance, and trust rules |
| Repository stars | 47,525 | Source | Repository attention, not individual Skill quality |
| Compatibility | 0 platforms | Source | Declared in the catalog source record |
| Usage guide | automated source guide | Editorial | Generated or reviewed according to the visible evidence level |
Pinned source
Edit your data contract. Prisma handles the rest.
This skill covers using Prisma Next against a Supabase project end-to-end: composing the Supabase extension pack, referencing Supabase-owned tables from your contract, authoring row-level-security (RLS) policies, and running role-bound queries through the supabase() runtime.
policy_select, @@rls, auth.uid()).asUser(jwt), asAnon(), asServiceRole()).auth.users (cross-space FK).auth.*, storage.*) as an admin.prisma-next-contract.db.ts wiring, middleware, teardown → prisma-next-runtime.prisma-next-queries — everything there applies to a role-bound db too.prisma-next-migrations.external contract space. @internal/extension-supabase/pack ships a complete, introspection-generated contract of everything Supabase owns — the auth and storage schemas, their native enum types, and the platform roles (anon, authenticated, service_role) — all with control policy external. Composed via extensions, it means: the migration planner emits no DDL for those objects (Supabase manages them), and db verify confirms they exist in the live database. Your own tables stay managed as usual.roles = [authenticated] identifiers resolve against the composed contract. Pointing the runtime at a non-Supabase Postgres fails verify with a not-found issue naming the missing role — the common "wrong database" misconfiguration surfaces before queries run.supabase() returns a SupabaseDb with no top-level query surface — there is no db.sql / db.orm until you bind a role. await db.asUser(jwt) / db.asAnon() / db.asServiceRole() each return a RoleBoundDb exposing .sql, .orm, .raw, .execute(plan), and .transaction(fn). This is deliberate: in a Supabase app there is no meaningful "no role" execution context, and defaulting to the connection's login role is a silent-RLS-bypass footgun.set_config('role', …) and set_config('request.jwt.claims', …) applied beneath the user-middleware chain, with RESET ALL on release. Postgres-side auth.uid() / auth.jwt() read those session vars — RLS enforcement is Postgres's job; the runtime's job is binding the context.GRANT controls table access. Prisma Next authors and migrates the policies; it does not author grants (see What Prisma Next doesn't do yet). A role with policies but no GRANT gets a permission error, not filtered rows. On Supabase your public tables already carry the platform-role grants via default privileges — the grant that is actually missing out of the box is service_role's on auth.* / storage.* (see Workflow — Grants).jwksUrl. asUser(jwt) verifies the token (via jose) before any connection is acquired: signature + expiry against jwksUrl (asymmetric signing keys — the default on current Supabase projects, which sign ES256) xor jwtSecret (the symmetric HS256 secret — legacy projects only). Both or neither → a structured error with code SUPABASE.CONFIG_INVALID. Bad tokens throw a structured error with code SUPABASE.JWT_INVALID and a typed meta.reason — including a mismatch between the token's algorithm and the configured key source (an ES256 token against a jwtSecret client names the problem and tells you to switch to jwksUrl). The Postgres role is derived from the token's role claim (defaults to authenticated). Note: supabase status still prints a JWT_SECRET even on projects that sign ES256 — its presence does not mean your project uses it.auth.* / storage.* is a secondary root on service_role only — and needs a one-time grant. db.asServiceRole().supabase exposes the pack's own contract (.sql, .orm, .nativeEnums, .execute). The root exists only on service_role by design, but a real Supabase project grants service_role no table privileges on auth.* / storage.* (only schema USAGE; only postgres holds table grants). Before the admin root can read a Supabase-internal table, run the narrow grant once (see Workflow — Grants). asUser / asAnon have no .supabase, and the primary asServiceRole().sql / .orm stay scoped to your contract.The concept: the pack registers the Supabase contract space so your contract can reference it and the planner/verifier know what Supabase owns. The extension has no /control subpath yet, so it can't go through the target façade's defineConfig({ extensions: [...] }) — it wires into the low-level config's extensions (see What Prisma Next doesn't do yet). The low-level imports below are a deliberate exception to the façade-only import rule, forced by that gap; the block mirrors examples/supabase/prisma-next.config.ts verbatim — copy it rather than composing your own:
// prisma-next.config.ts
import postgresAdapter from '@internal/adapter-postgres/control';
import { defineConfig } from '@internal/cli/config-types';
import postgresDriver from '@internal/driver-postgres/control';
import supabasePack from '@internal/extension-supabase/pack';
import sql from '@internal/family-sql/control';
import { prismaContract } from '@internal/sql-contract-psl/provider';
import postgres from '@internal/target-postgres/control';
import postgresPackRef from '@internal/target-postgres/pack';
import { postgresCreateNamespace } from '@internal/target-postgres/types';
export default defineConfig({
family: sql,
target: postgres,
adapter: postgresAdapter,
driver: postgresDriver,
extensions: [supabasePack],
contract: prismaContract('./src/contract.prisma', {
output: 'src/contract.json',
target: postgresPackRef,
createNamespace: postgresCreateNamespace,
}),
migrations: { dir: 'migrations' },
});
auth.users + RLS policiesThe concept: your models live in your namespaces (public); Supabase's live in the pack's (auth, storage). A relation field typed supabase:auth.AuthUser is a cross-space FK — the planner emits REFERENCES "auth"."users"("id"), and the target table is verified, never migrated. RLS policies are top-level policy_<operation> blocks in the same namespace as their target model, and the target model must opt in with @@rls. Mirror examples/supabase/src/contract.prisma:
namespace public {
model Profile {
id Uuid @id @default(uuid())
username String
userId Uuid @unique
user supabase:auth.AuthUser @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("profile")
@@rls
}
// authenticated may read only their own profile.
policy_select profile_owner_read {
target = Profile
roles = [authenticated]
using = "\"userId\"::uuid = auth.uid()"
}
// anon may read every profile (a public directory listing).
policy_select profile_public_read {
target = Profile
roles = [anon]
using = "true"
}
// authenticated may update only their own profile, and may not
// reassign it to another owner (WITH CHECK).
policy_update profile_owner_write {
target = Profile
roles = [authenticated]
using = "\"userId\"::uuid = auth.uid()"
withCheck = "\"userId\"::uuid = auth.uid()"
}
}
The Uuid constructor selects native UUID storage in type position. The legacy @db.Uuid spelling is removed; rewrite any String @db.Uuid alias or field as Uuid before emitting.
The pieces:
policy_select, policy_insert, policy_update, policy_delete, policy_all. Body is key = value: target (a model in this namespace), roles (resolve against the composed contract — the pack supplies anon / authenticated / service_role), using, and (for write operations) withCheck. Multiple permissive policies per (target, operation) are valid — Postgres ORs them. A block may also carry @@map("physical name") to adopt an existing live policy under its exact name (no wire-name hash; drift detection then byte-compares the body against Postgres's reprint, so keep the text as captured — hand-authoring it warns).@@rls is required on policy targets. A policy_* block whose target model lacks @@rls fails emit with PSL_EXTENSION_TARGET_MODEL_MISSING_ATTRIBUTE. A model with @@rls and no policies is also meaningful: RLS enabled, deny-all.\"userId\"), and cast where needed — auth.uid() returns uuid. Renames in your contract do not rewrite predicate bodies.@internal/postgres/contract-builder exports policySelect / policyInsert / policyUpdate / policyDelete / policyAll, rlsEnabled(Model), and role('anon') — mirroring the PSL lowering key-for-key (identical emitted wire names). PSL is the canonical path shown here.Emit + migrate as usual (prisma-next contract emit, then prisma-next-migrations). The plan creates your table, its FK, ENABLE ROW LEVEL SECURITY, and the CREATE POLICY statements — and no DDL for auth.*.
db.ts with the supabase() factoryThe concept: instead of the stock postgres() factory, a Supabase app builds its client with supabase() from the extension's /runtime subpath. The factory is async (it prepares JWT key material — including the one-time JWKS fetch when jwksUrl is set), and the result is role-first.
// src/prisma/db.ts
import { supabase } from '@internal/extension-supabase/runtime';
import type { Contract } from './contract.d';
import contractJson from './contract.json' with { type: 'json' };
export const db = await supabase<Contract>({
contractJson,
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.
});
Options beyond the basics: middleware (same composition as postgres() — see prisma-next-runtime; middleware never sees the role-binding set_config calls), poolOptions, pg (BYO pg.Pool / pg.Client instead of url). Teardown is await db.close() / await using exactly as in prisma-next-runtime — the same script-hang rules apply.
The concept: bind the role that should execute the request, then query through the returned RoleBoundDb — every query surface from prisma-next-queries works, RLS-filtered by Postgres.
// A signed-in user: rows are RLS-scoped to the JWT's auth.uid().
const userDb = await db.asUser(jwt); // async — rejects with code SUPABASE.JWT_INVALID on a bad/expired token
const mine = await userDb.orm.public.Profile.select('id', 'username').all();
// The anon role: sees what anon policies permit.
const listing = await db.asAnon().orm.public.Profile.select('id', 'username').all();
// service_role: BYPASSRLS — sees everything in YOUR contract.
const all = await db.asServiceRole().orm.public.Profile.select('id', 'username').all();
// Writes ride the same surfaces; RLS filters them too. An UPDATE against
// another owner's row affects 0 rows; a withCheck violation raises an error.
const updated = await userDb.orm.public.Profile
.where({ userId: me })
.updateAndCount({ username: 'new-name' });
Notes: asAnon() / asServiceRole() are sync; only asUser is async. Multi-namespace contracts address models by coordinate (orm.public.Profile, sql.public.profile) — see prisma-next-queries § Namespace-aware accessors. RoleBoundDb.transaction(fn) wraps work in a transaction on the role-bound session.
auth.* / storage.*The concept: Supabase-internal tables are not part of your contract, so they are not on your query surfaces. The service_role binding carries a secondary root — db.asServiceRole().supabase — which is the pack's contract surface:
const admin = db.asServiceRole();
// SQL builder over the pack contract:
const users = await admin.supabase
.execute(admin.supabase.sql.auth.users.select('id', 'email').build())
.toArray();
// ORM over the pack contract:
const sessions = await admin.supabase.orm.auth.AuthSession.select('id', 'aal').all();
// Native enum values (e.g. auth.aal_level) come typed:
type AalLevel = (typeof admin.supabase.nativeEnums.auth.AalLevel)['Value'];
The admin root needs a one-time grant. A real Supabase project gives service_role no table privileges on auth.* / storage.* — out of the box, the reads above fail with permission denied for table users (sqlState 42501). Grant exactly what you read, narrowly:
GRANT USAGE ON SCHEMA auth TO service_role;
GRANT SELECT ON TABLE auth.users TO service_role;
Other boundaries to respect: asUser / asAnon have no .supabase; the admin root has no .transaction (it is a separate contract-bound runtime sharing the pool — a transaction spanning both roots is out of scope); and for user management (creating users, password resets) prefer the GoTrue Admin API — Supabase-internal schemas can drift across platform upgrades; direct service_role SQL is for ad-hoc admin reads.
The concept: RLS policies are row filters on top of ordinary table privileges — a role with policies but no GRANT gets permission denied, not filtered rows. On Supabase the two directions are easy to get backwards:
public tables need nothing. Supabase ships ALTER DEFAULT PRIVILEGES on public, so tables created by prisma-next db init / migrate inherit full grants for anon / authenticated / service_role automatically — the same as dashboard-created tables. RLS policies are what actually protect the rows; do not add per-table grants, and do not narrow the defaults unless you have a reason.service_role has no table privileges on auth.* / storage.* (see Admin reads above for the narrow GRANT USAGE / GRANT SELECT pair).Run grants via the Supabase SQL editor or psql. Symptom of a missing grant: permission denied for table … (sqlState 42501) instead of an empty result.
The concept: the runtime needs a direct, session-capable Postgres connection — it binds roles with session-scoped set_config + RESET ALL.
aws-0-<region>.pooler.supabase.com:5432, username postgres.<project-ref>) — works everywhere, IPv4. The default choice.db.<project-ref>.supabase.co:5432) — works, but is IPv6-only on new projects; from IPv4-only environments it fails DNS/connect..env carries DATABASE_URL and the JWT key source. For current projects that is SUPABASE_JWKS_URL — https://<project-ref>.supabase.co/auth/v1/.well-known/jwks.json (local stack: http://127.0.0.1:54321/auth/v1/.well-known/jwks.json). Only legacy HS256 projects use SUPABASE_JWT_SECRET (Project Settings → API → JWT Secret) — and note supabase status prints a JWT_SECRET even on ES256 projects, so don't infer the mode from its presence; check the JWKS endpoint or a token's header alg.
jwtSecret because supabase status prints a JWT_SECRET. Current projects sign ES256; asUser then throws SUPABASE.JWT_INVALID explaining the token is ES256 and the client needs jwksUrl. Configure SUPABASE_JWKS_URL; reserve jwtSecret for legacy HS256 projects.public tables need no grants (Supabase's default privileges cover them; RLS protects the rows) — the grant you need is the narrow auth.* pair for service_role admin reads. permission denied (42501) means a missing grant, not a filtered result.db.sql / db.orm on the top-level db. The Supabase db is role-first; bind a role, query the RoleBoundDb.await — on the supabase() factory and on asUser(jwt). Both are async; asAnon() / asServiceRole() are not..supabase on asUser / asAnon. Admin access to auth.* is service_role-only by construction — and even service_role needs the one-time narrow grant first.policy_* block whose target lacks @@rls. Emit fails with PSL_EXTENSION_TARGET_MODEL_MISSING_ATTRIBUTE — add @@rls to the model."userId" needs quotes; compare uuid to auth.uid() with a ::uuid cast where the column isn't already uuid.jwksUrl and jwtSecret (or neither) — the supabase() promise rejects with SUPABASE.CONFIG_INVALID. It's an async factory, so the misconfiguration surfaces as a rejection (await / .catch), not a synchronous throw.UPDATE against a row the role can't see affects 0 rows (no exception); only withCheck violations raise./control subpath on the extension — it can't register through the target façade's defineConfig({ extensions: [...] }); wiring goes through the low-level config's extensions as shown above. File interest via prisma-next-feedback.GRANT authoring. Table privileges are not contract elements; the one grant a Supabase app needs (the service_role auth.* pair for admin reads) is run once by hand (SQL editor / psql). If you want grants managed by the contract, file via prisma-next-feedback..supabase admin root. The two roots are separate contract-bound runtimes sharing one pool; a cross-root transaction is not supported.auth.users trigger is authored as raw SQL against your database, not in the contract. auth.uid() etc. appear only inside opaque policy predicate strings.@supabase/supabase-js interop, edge runtimes. Out of scope for the extension — it speaks Postgres directly (Node.js / Bun).examples/supabase — the canonical runnable app: config, contract, db.ts, acceptance tests, README.packages/3-extensions/supabase/README.md — package-level reference (JWT modes, role-binding model, unsupported scope).packages/3-extensions/supabase/src/runtime/supabase.ts — the authoritative options/type surface (SupabaseOptions, RoleBoundDb, ServiceRoleDb).extensions: [supabasePack] in the low-level defineConfig (no /control subpath exists).supabase:auth.AuthUser with explicit fields / references (+ onDelete if wanted).@@rls; predicates quote camelCase columns and cast for auth.uid().db.ts uses await supabase<Contract>({ contractJson, url, jwksUrl | jwtSecret }) — exactly one JWT key source; jwksUrl for current projects, jwtSecret only for legacy HS256.RoleBoundDb from asUser / asAnon / asServiceRole; asUser is awaited.auth.* / storage.* reads go through asServiceRole().supabase only, after the one-time narrow grant (GRANT USAGE ON SCHEMA auth + GRANT SELECT on the tables you read).public tables — Supabase default privileges cover them; RLS does the protecting./control subpath, a top-level db.sql, .supabase on non-service roles, or grant authoring in the contract.Alternatives
alirezarezvani/claude-skills
App Store Optimization (ASO) toolkit for researching keywords, analyzing competitor rankings, generating metadata suggestions, and improving app visibility on Apple App Store and Google Play Store. Use when the user asks about ASO, app store rankings, app metadata, app titles and descriptions, app store listings, app visibility, or mobile app marketing on iOS or Android. Supports keyword research and scoring, competitor keyword analysis, metadata optimization, A/B test planning, launch checklist
wanshuiyin/Auto-claude-code-research-in-sleep
Use it for operations and research tasks; the detail page covers purpose, installation, and practical steps.
dotnet/skills
Migrates .NET test projects from VSTest to Microsoft.Testing.Platform (MTP). Use when user asks to "migrate to MTP", "switch from VSTest", "enable Microsoft.Testing.Platform", "use MTP runner", set OutputType=Exe only for test projects in Directory.Build.props, or mentions EnableMSTestRunner, EnableNUnitRunner, or UseMicrosoftTestingPlatformRunner. USE FOR: MTP behavioral differences vs VSTest (exit code 8, zero tests discovered, --ignore-exit-code, TESTINGPLATFORM_EXITCODE_IGNORE); centralizing
aaron-he-zhu/aaron-marketing-skills
Use when the user asks to "set up my founder social-selling routine", "build a daily engagement block for target accounts", or "turn funding / hiring signals into selling plays"; produces the founder/seller daily operating block — a time-boxed engagement-block spec (substantive value-add comments on target-account posts, never a pitch), warm-touch-before-ask cadence rules, trigger-response plays consuming the social-pulse-monitor B2B trigger watchlist (funding / hiring / launch signals), and a q