← library
skill🗄️ Databasesv1 · updated 2026-09-10

Supabase Conventions

Applies house Supabase conventions — the new sb_publishable_/sb_secret_ API keys, RLS-first multi-tenant schema design, migration discipline, and pre-ship advisor checks. Use when creating or changing schema, auth, API keys, or client wiring on any Supabase project.

Run it as a prompt

Paste this into any AI agent, or fetch it: curl -s https://uplift.page/api/v1/prompts/supabase-conventions/raw

prompt.md
# Supabase Conventions

Every client project runs on Supabase; these are the defaults that keep them safe and
consistent. When a project's own docs conflict, the project wins — but say so.

## When to use

- Creating or altering tables, policies, functions, or triggers on a Supabase project
- Wiring a client app or server to Supabase (keys, environment, SSR)
- Reviewing a Supabase project before ship or handoff

## API keys

1. New code uses the current key family: `sb_publishable_…` in clients,
   `sb_secret_…` on servers only. Do not introduce the legacy `anon` /
   `service_role` JWT keys into new code — they are deprecated with removal
   planned for late 2026 (supabase.com/docs → migrating-to-new-api-keys).
2. Touching a project still on legacy keys? Flag the migration as its own task;
   don't mix key families within one codebase.
3. Secret keys never reach a browser, a client bundle, or a repo. Publishable
   keys are fine to expose — RLS is the security boundary, not key secrecy.

## Schema & RLS

1. RLS on from the first migration, deny-by-default; every policy names its table's
   tenant predicate explicitly (e.g. `org_id = (select auth.jwt() ->> 'org_id')::uuid`
   or a shared `is_org_member(org_id)` helper — one pattern per project, reused).
2. Multi-tenant tables carry `org_id` (or the project's tenant column) NOT NULL with a
   foreign key; server-side code derives the tenant from the credential, never from the
   request body.
3. `security definer` functions pin `search_path` and get an explicit
   `revoke execute … from public` decision, stated in the migration.

## Migrations

1. Schema changes are migration files in the repo (CLI-generated), applied in order —
   never hand-run statements that bypass the migration history.
2. Don't pin extension versions in `create extension` — version clauses are ignored
   on the platform (changelog 2026-08); state required extensions by name.

## Before ship

1. Run the security and performance advisors and act on every finding or record why
   it stands (`security definer` views, missing indexes on FKs, permissive policies).
2. Verify tenant isolation with a real cross-tenant read attempt, not by reading the
   policy: query as tenant A for tenant B's rows and expect zero.

Install it as a skill

Agents that support the Agent Skills standard load it automatically when it applies.

download
curl -fsSL https://uplift.page/p/supabase-conventions/SKILL.md --create-dirs -o .agents/skills/supabase-conventions/SKILL.md
or from any MCP client
MCP server: https://uplift.page/mcp
Tool: pull_skill  {"slug": "supabase-conventions"}

The full skill

Every client project runs on Supabase; these are the defaults that keep them safe and consistent. When a project's own docs conflict, the project wins — but say so.

When to use

  • Creating or altering tables, policies, functions, or triggers on a Supabase project
  • Wiring a client app or server to Supabase (keys, environment, SSR)
  • Reviewing a Supabase project before ship or handoff

API keys

  1. New code uses the current key family: sb_publishable_… in clients, sb_secret_… on servers only. Do not introduce the legacy anon / service_role JWT keys into new code — they are deprecated with removal planned for late 2026 (supabase.com/docs → migrating-to-new-api-keys).
  2. Touching a project still on legacy keys? Flag the migration as its own task; don't mix key families within one codebase.
  3. Secret keys never reach a browser, a client bundle, or a repo. Publishable keys are fine to expose — RLS is the security boundary, not key secrecy.

Schema & RLS

  1. RLS on from the first migration, deny-by-default; every policy names its table's tenant predicate explicitly (e.g. org_id = (select auth.jwt() ->> 'org_id')::uuid or a shared is_org_member(org_id) helper — one pattern per project, reused).
  2. Multi-tenant tables carry org_id (or the project's tenant column) NOT NULL with a foreign key; server-side code derives the tenant from the credential, never from the request body.
  3. security definer functions pin search_path and get an explicit revoke execute … from public decision, stated in the migration.

Migrations

  1. Schema changes are migration files in the repo (CLI-generated), applied in order — never hand-run statements that bypass the migration history.
  2. Don't pin extension versions in create extension — version clauses are ignored on the platform (changelog 2026-08); state required extensions by name.

Before ship

  1. Run the security and performance advisors and act on every finding or record why it stands (security definer views, missing indexes on FKs, permissive policies).
  2. Verify tenant isolation with a real cross-tenant read attempt, not by reading the policy: query as tenant A for tenant B's rows and expect zero.

Examples

Good: policy "orders_select" on orders for select using (org_id = app.current_org_id())
      — named predicate, reused helper, deny-by-default table.
Bad:  alter table orders disable row level security; -- "we filter in the API"
      — one forgotten endpoint and every tenant reads every order.

Served from the uplift.page library and refreshed within 5 minutes of every update.