Files
supabase-postgres-best-prac…/packages/evals/scenarios/team-rls-security-definer.md
2026-02-20 15:02:59 +00:00

7.5 KiB

Scenario: team-rls-security-definer

Summary

The agent must create a SQL migration for a team-based project management app where users belong to organizations via a membership table. The migration must define tables for organizations, memberships, and projects, then secure them with RLS policies that use a security definer helper function in a private schema to efficiently resolve team membership without per-row joins.

Real-World Justification

Why this is a common and important workflow:

  1. Multi-tenant team access is the most-asked RLS question on Supabase -- The official Supabase GitHub has multiple high-engagement discussions about how to write RLS policies that check team/org membership without causing performance issues or security holes.

  2. security_definer in public schema is a documented security anti-pattern -- Developers frequently place security_definer functions in the public schema, inadvertently exposing them via the PostgREST API. The Supabase docs and community discussions explicitly warn against this.

  3. RLS policy performance with joins is a top pain point -- Naive policies that join against a memberships table execute per-row, causing severe performance degradation. The recommended pattern is a security_definer function that caches results via subselect.

Skill References Exercised

Which reference files the agent should consult and what each teaches:

Reference File What It Teaches What the Agent Should Apply
references/db-rls-mandatory.md RLS must be enabled on all public tables Enable RLS on organizations, memberships, and projects
references/db-rls-policy-types.md PERMISSIVE vs RESTRICTIVE policies Use PERMISSIVE policies for team OR owner access patterns
references/db-rls-common-mistakes.md Missing TO clause, user_metadata pitfalls Always use TO authenticated on all policies
references/db-rls-performance.md Wrap auth.uid() in SELECT, use security_definer for joins Use (select auth.uid()) and a private-schema helper function
references/db-security-functions.md security_definer in private schema with search_path = '' Create helper function in private schema, revoke default permissions
references/db-schema-auth-fk.md FK to auth.users with ON DELETE CASCADE Reference auth.users with cascade on memberships
references/db-schema-timestamps.md Use timestamptz not timestamp All time columns use timestamptz
references/db-perf-indexes.md Index columns used in RLS policies Index user_id and org_id columns used in policy lookups
references/db-migrations-idempotent.md IF NOT EXISTS for safe reruns Idempotent DDL throughout the migration

Workspace Setup

What the workspace starts with before the agent runs:

  • Pre-initialized Supabase project (supabase/config.toml exists)
  • Empty supabase/migrations/ directory
  • The agent creates migration files within this structure

Agent Task (PROMPT.md draft)

The prompt to give the agent. Written as a developer would ask it:

I'm building a project management app where users can belong to multiple organizations. Each organization has projects that all members can view and edit.

Create a SQL migration with:

  1. An organizations table (name, slug)
  2. A memberships table linking users to organizations with a role column (owner, admin, member)
  3. A projects table (name, description, status) belonging to an organization

Set up Row Level Security so:

  • Users can only see organizations they belong to
  • Users can only see and manage projects in their organizations
  • Only org owners can delete projects

The migration should handle the case where a user is deleted from auth.

Evaluation Criteria

What vitest should assert on the agent's output. Each assertion tests a specific quality signal:

# Test Name What It Checks Quality Dimension
1 migration file exists A .sql file exists in supabase/migrations/ structure
2 creates organizations table SQL contains CREATE TABLE for organizations correctness
3 creates memberships table SQL contains CREATE TABLE for memberships correctness
4 creates projects table SQL contains CREATE TABLE for projects correctness
5 enables RLS on all tables ALTER TABLE ... ENABLE ROW LEVEL SECURITY for all three tables security
6 FK to auth.users with ON DELETE CASCADE memberships references auth.users with cascade correctness
7 org_id FK on projects projects references organizations correctness
8 private schema created CREATE SCHEMA ... private present security
9 security_definer helper function A function in the private schema with SECURITY DEFINER and SET search_path = '' security
10 policies use (select auth.uid()) Subselect form in all policies referencing auth.uid() performance
11 policies use TO authenticated All policies scoped to authenticated role security
12 index on membership lookup columns CREATE INDEX on user_id and/or org_id in memberships performance
13 uses timestamptz No plain timestamp for time columns correctness
14 idempotent DDL Uses IF NOT EXISTS or DROP ... IF EXISTS patterns idempotency
15 delete policy restricted to owner role A delete policy on projects checks for owner/admin role security
16 overall quality score At least 10/14 best-practice signals present overall

Reasoning

Step-by-step reasoning for why this scenario is well-designed:

  1. Baseline differentiator: An agent without the skill would likely put the security_definer function in the public schema, omit SET search_path = '', use bare auth.uid() instead of the subselect form, write inline joins in policies instead of using a helper function, and possibly forget TO authenticated on some policies. These are all patterns that require specific knowledge of Supabase conventions.

  2. Skill value: The skill explicitly teaches: (a) private schema for security_definer functions, (b) SET search_path = '' to prevent injection, (c) (select auth.uid()) for per-statement caching, (d) using security_definer functions to avoid per-row joins in policies, (e) TO authenticated on every policy. This is a scenario where reading 5+ reference files materially improves the output.

  3. Testability: Every assertion checks for specific SQL patterns via regex. The private schema, security_definer, search_path, subselect auth.uid(), TO authenticated, indexes, and timestamptz are all reliably detectable in SQL text without runtime execution.

  4. Realism: Multi-tenant team-based access control is one of the most common Supabase use cases. The GitHub discussions linked above have hundreds of comments from developers working on exactly this pattern. Project management apps (Notion, Linear, Asana clones) are a canonical example.

Difficulty

Rating: MEDIUM

  • Without skill: ~35-50% of assertions expected to pass
  • With skill: ~85-95% of assertions expected to pass