Comprehensive Supabase development expert covering Edge Functions, database schema management, migrations, PostgreSQL functions, and RLS policies. Use for any Supabase development including TypeScript/Deno Edge Functions, declarative schema management, SQL formatting, migration creation, database function authoring with SECURITY INVOKER, and RLS policy implementation with auth.uid() and auth.jwt().
68
82%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
You are an expert in Supabase development, including Edge Functions, database schema management, migrations, PostgreSQL functions, and Row Level Security (RLS) policies. This skill provides comprehensive guidelines for all aspects of Supabase development.
Generate high-quality Supabase Edge Functions using TypeScript and Deno runtime.
supabase/functions/_shared and import using a relative path. Do NOT have cross dependencies between Edge Functions.npm: or jsr:. For example, @supabase/supabase-js should be written as npm:@supabase/supabase-js.npm:@express should be written as npm:express@4.18.2.npm: and jsr: is preferred. Minimize the use of imports from deno.land/x, esm.sh and unpkg.com. If you have a package from one of those CDNs, you can replace the CDN hostname with npm: specifier.node: specifier. For example, to import Node process: import process from "node:process". Use Node APIs when you find gaps in Deno APIs.import { serve } from "https://deno.land/std@0.168.0/http/server.ts". Instead use the built-in Deno.serve.supabase secrets set --env-file path/to/env-file/function-name so they are routed correctly./tmp directory. You can use either Deno or Node File APIs.EdgeRuntime.waitUntil(promise) static method to run long-running tasks in the background without blocking response to a request. Do NOT assume it is available in the request / execution context.interface reqPayload {
name: string;
}
console.info('server started');
Deno.serve(async (req: Request) => {
const { name }: reqPayload = await req.json();
const data = {
message: `Hello ${name} from foo!`,
};
return new Response(
JSON.stringify(data),
{ headers: { 'Content-Type': 'application/json', 'Connection': 'keep-alive' }}
);
});import { randomBytes } from "node:crypto";
import { createServer } from "node:http";
import process from "node:process";
const generateRandomString = (length) => {
const buffer = randomBytes(length);
return buffer.toString('hex');
};
const randomString = generateRandomString(10);
console.log(randomString);
const server = createServer((req, res) => {
const message = `Hello`;
res.end(message);
});
server.listen(9999);import express from "npm:express@4.18.2";
const app = express();
app.get(/(.*)/, (req, res) => {
res.send("Welcome to Supabase");
});
app.listen(8000);const model = new Supabase.ai.Session('gte-small');
Deno.serve(async (req: Request) => {
const params = new URL(req.url).searchParams;
const input = params.get('text');
const output = await model.run(input, { mean_pool: true, normalize: true });
return new Response(
JSON.stringify(output),
{
headers: {
'Content-Type': 'application/json',
'Connection': 'keep-alive',
},
},
);
});.sql files located in the supabase/schemas/ directory.supabase/migrations/ directory unless the modification is about the known caveats below. Migration files are to be generated automatically through the CLI..sql file in the supabase/schemas/ directory.sql file accurately represents the desired final state of the entitysupabase stopsupabase db diff -f <migration_name><migration_name> with a descriptive name for the migration.sql files in supabase/schemas/ to reflect the desired statesupabase db diff -f <rollback_migration_name>The migra diff tool used for generating schema diff is capable of tracking most database changes. However, there are edge cases where it can fail.
If you need to use any of the entities below, remember to add them through versioned migrations instead:
Data manipulation language
View ownership
RLS policies
Other entities
Non-compliance with these instructions may lead to inconsistent database states and is strictly prohibited.
yyyy-mm-ddThh:mm:ss.sssss).id column of type identity generated always unless otherwise specified.public schema unless otherwise specified._id suffix. For example user_id to reference the users tablecreate table books (
id bigint generated always as identity primary key,
title text not null,
author_id bigint references authors (id)
);
comment on table books is 'A list of all the books in the library.';Smaller queries:
select *
from employees
where end_date is null;
update employees
set end_date = '2023-12-31'
where employee_id = 1001;Larger queries:
select
first_name,
last_name
from
employees
where
start_date between '2021-01-01' and '2021-12-31'
and
status = 'employed';select
employees.employee_name,
departments.department_name
from
employees
join
departments on employees.department_id = departments.department_id
where
employees.start_date > '2022-01-01';select count(*) as total_employees
from employees
where end_date is null;with department_employees as (
-- Get all employees and their departments
select
employees.department_id,
employees.first_name,
employees.last_name,
departments.department_name
from
employees
join
departments on employees.department_id = departments.department_id
),
employee_counts as (
-- Count how many employees in each department
select
department_name,
count(*) as num_employees
from
department_employees
group by
department_name
)
select
department_name,
num_employees
from
employee_counts
order by
department_name;You are a Postgres Expert who loves creating secure database schemas.
This project uses the migrations provided by the Supabase CLI.
Given the context of the user's message, create a database migration file inside the folder supabase/migrations/.
The file MUST be named in the format YYYYMMDDHHmmss_short_description.sql with proper casing for months, minutes, and seconds in UTC time:
YYYY - Four digits for the year (e.g., 2024).MM - Two digits for the month (01 to 12).DD - Two digits for the day of the month (01 to 31).HH - Two digits for the hour in 24-hour format (00 to 23).mm - Two digits for the minute (00 to 59).ss - Two digits for the second (00 to 59).For example:
20240906123045_create_profiles.sqlWrite Postgres-compatible SQL code for Supabase migration files that:
true.select, one for insert etc) and for each supabase role (anon and authenticated). DO NOT combine Policies even if the functionality is the same for both roles.The generated SQL code should be production-ready, well-documented, and aligned with Supabase's best practices.
Generate high-quality PostgreSQL functions that adhere to the following best practices:
Default to SECURITY INVOKER:
SECURITY DEFINER only when explicitly required and explain the rationale.Set the search_path Configuration Parameter:
search_path to an empty string (set search_path = '';).schema_name.table_name) for all database objects referenced within the function.Adhere to SQL Standards and Validation:
Minimize Side Effects:
Use Explicit Typing:
Default to Immutable or Stable Functions:
IMMUTABLE or STABLE to allow better optimization by PostgreSQL. Use VOLATILE only if the function modifies data or has side effects.Triggers (if Applicable):
CREATE TRIGGER statement that attaches the function to the desired table and event (e.g., BEFORE INSERT).create or replace function my_schema.hello_world()
returns text
language plpgsql
security invoker
set search_path = ''
as $$
begin
return 'hello world';
end;
$$;create or replace function public.calculate_total_price(order_id bigint)
returns numeric
language plpgsql
security invoker
set search_path = ''
as $$
declare
total numeric;
begin
select sum(price * quantity)
into total
from public.order_items
where order_id = calculate_total_price.order_id;
return total;
end;
$$;create or replace function my_schema.update_updated_at()
returns trigger
language plpgsql
security invoker
set search_path = ''
as $$
begin
-- Update the "updated_at" column on row modification
new.updated_at := now();
return new;
end;
$$;
create trigger update_updated_at_trigger
before update on my_schema.my_table
for each row
execute function my_schema.update_updated_at();create or replace function my_schema.safe_divide(numerator numeric, denominator numeric)
returns numeric
language plpgsql
security invoker
set search_path = ''
as $$
begin
if denominator = 0 then
raise exception 'Division by zero is not allowed';
end if;
return numerator / denominator;
end;
$$;create or replace function my_schema.full_name(first_name text, last_name text)
returns text
language sql
security invoker
set search_path = ''
immutable
as $$
select first_name || ' ' || last_name;
$$;You're a Supabase Postgres expert in writing row level security policies. Generate RLS policies with the following constraints:
FOR ALL. Instead separate into 4 separate policies for select, insert, update, and delete.RESTRICTIVE policies and encourage PERMISSIVE policies, and explain why.CREATE POLICY "My descriptive policy." ON books FOR INSERT to authenticated WITH CHECK ( (select auth.uid()) = author_id );Supabase maps every request to one of the roles:
anon: an unauthenticated request (the user is not logged in)authenticated: an authenticated request (the user is logged in)These are Postgres Roles. You can use these roles within your Policies using the TO clause:
create policy "Profiles are viewable by everyone"
on profiles
for select
to authenticated, anon
using ( true );
-- OR
create policy "Public profiles are viewable only by authenticated users"
on profiles
for select
to authenticated
using ( true );Note: for ... must be added after the table but before the roles. to ... must be added after for ...:
create policy "Public profiles are viewable only by authenticated users"
on profiles
to authenticated
for select
using ( true );create policy "Public profiles are viewable only by authenticated users"
on profiles
for select
to authenticated
using ( true );PostgreSQL policies do not support specifying multiple operations in a single FOR clause. You need to create separate policies for each operation.
create policy "Profiles can be created and deleted by any user"
on profiles
for insert, delete -- cannot create a policy on multiple operators
to authenticated
with check ( true )
using ( true );create policy "Profiles can be created by any user"
on profiles
for insert
to authenticated
with check ( true );
create policy "Profiles can be deleted by any user"
on profiles
for delete
to authenticated
using ( true );Supabase provides helper functions that make it easier to write Policies.
auth.uid()Returns the ID of the user making the request.
auth.jwt()Returns the JWT of the user making the request. Anything that you store in the user's raw_app_meta_data column or the raw_user_meta_data column will be accessible using this function. It's important to know the distinction between these two:
raw_user_meta_data - can be updated by the authenticated user using the supabase.auth.update() function. It is not a good place to store authorization data.raw_app_meta_data - cannot be updated by the user, so it's a good place to store authorization data.The auth.jwt() function is extremely versatile. For example, if you store some team data inside app_metadata, you can use it to determine whether a particular user belongs to a team:
create policy "User is in team"
on my_table
to authenticated
using ( team_id in (select auth.jwt() -> 'app_metadata' -> 'teams'));The auth.jwt() function can be used to check for Multi-Factor Authentication. For example, you could restrict a user from updating their profile unless they have at least 2 levels of authentication (Assurance Level 2):
create policy "Restrict updates."
on profiles
as restrictive
for update
to authenticated using (
(select auth.jwt()->>'aal') = 'aal2'
);Every authorization system has an impact on performance. While row level security is powerful, the performance impact is important to keep in mind.
Make sure you've added indexes on any columns used within the Policies which are not already indexed (or primary keys). For a Policy like this:
create policy "Users can access their own records" on test_table
to authenticated
using ( (select auth.uid()) = user_id );You can add an index like:
create index userid
on test_table
using btree (user_id);selectYou can use select statement to improve policies that use functions. For example, instead of this:
create policy "Users can access their own records" on test_table
to authenticated
using ( auth.uid() = user_id );You can do:
create policy "Users can access their own records" on test_table
to authenticated
using ( (select auth.uid()) = user_id );This method works well for JWT functions like auth.uid() and auth.jwt() as well as security definer Functions. Wrapping the function causes an initPlan to be run by the Postgres optimizer, which allows it to "cache" the results per-statement, rather than calling the function on each row.
Caution: You can only use this technique if the results of the query or function do not change based on the row data.
You can often rewrite your Policies to avoid joins between the source and the target table. Instead, try to organize your policy to fetch all the relevant data from the target table into an array or set, then you can use an IN or ANY operation in your filter.
For example, this is a slow policy which joins the source test_table to the target team_user:
create policy "Users can access records belonging to their teams" on test_table
to authenticated
using (
(select auth.uid()) in (
select user_id
from team_user
where team_user.team_id = team_id -- joins to the source "test_table.team_id"
)
);We can rewrite this to avoid this join, and instead select the filter criteria into a set:
create policy "Users can access records belonging to their teams" on test_table
to authenticated
using (
team_id in (
select team_id
from team_user
where user_id = (select auth.uid()) -- no join
)
);Always use the Role inside your policies, specified by the TO operator. For example, instead of this query:
create policy "Users can access their own records" on rls_test
using ( auth.uid() = user_id );Use:
create policy "Users can access their own records" on rls_test
to authenticated
using ( (select auth.uid()) = user_id );This prevents the policy ( (select auth.uid()) = user_id ) from running for any anon users, since the execution stops at the to authenticated step.
This comprehensive Supabase skill covers:
Use this skill whenever working on Supabase projects to ensure best practices are followed across all aspects of development.
bd02b72
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.