dist/plugins/api-baas-supabase/skills/api-baas-supabase/SKILL.md
Supabase backend-as-a-service — Auth, Database, Realtime, Storage, Edge Functions, RLS policies, typed client
npx skillsauth add agents-inc/skills api-baas-supabaseInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Quick Guide: Use Supabase as your backend-as-a-service for Postgres database, authentication, realtime subscriptions, file storage, and edge functions. Always use the typed client with
Databasegeneric, enable RLS on every table, and use the secret key only on the server.
<critical_requirements>
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST enable Row Level Security (RLS) on EVERY table in an exposed schema — no exceptions)
(You MUST use the Database generic type with createClient<Database>() for type-safe queries)
(You MUST NEVER expose the secret key in client-side code — use the publishable key in browsers, the secret key only on the server)
(You MUST use (select auth.uid()) wrapped in a subquery inside RLS policies for performance)
(You MUST handle all Supabase responses with { data, error } destructuring — never assume success)
</critical_requirements>
Auto-detection: Supabase, createClient, @supabase/supabase-js, @supabase/ssr, supabase-js, auth.uid(), RLS, row level security, realtime, postgres_changes, supabase.auth, supabase.from, supabase.storage, supabase.functions, supabase.channel, edge function, Deno.serve
When to use:
Key patterns covered:
Database generic and environment variablesonAuthStateChangeUSING vs WITH CHECK, auth.uid(), role-based accesschannel().on('postgres_changes')Deno.serve, CORS headers, secrets, Supabase client in functionsWhen NOT to use:
Detailed Resources:
Client & Queries:
Authentication:
Database:
Storage:
Edge Functions:
Deno.serve(), CORS, secretsSupabase is an open-source Firebase alternative built on Postgres. It provides a complete backend through a combination of Postgres extensions, auto-generated REST/GraphQL APIs, authentication, realtime subscriptions, file storage, and edge functions.
Core principles:
supabase gen types. Pass the Database generic to createClient for fully typed queries.{ data, error }. Never assume success. Always check error before using data.When to use Supabase:
When NOT to use:
Always pass the Database generic to createClient for full autocomplete on table names, column names, and return types. Use environment variables for URL and keys.
export const supabase = createClient<Database>(
SUPABASE_URL,
SUPABASE_PUBLISHABLE_KEY,
);
Without the generic, typos in table/column names are not caught at compile time. See examples/core.md for browser, server, and admin client setup patterns.
Every Supabase method returns { data, error }. Always destructure and check error before using data. Never use non-null assertions on data.
const { data, error } = await supabase
.from("profiles")
.select("id, username")
.eq("id", userId)
.single();
if (error) throw new Error(`Failed to fetch profile: ${error.message}`);
See examples/core.md for the reusable error handler pattern and common mistakes.
Supabase Auth supports email/password (signInWithPassword), OAuth (signInWithOAuth), magic links (signInWithOtp), and phone OTP. Register onAuthStateChange early in the app lifecycle and always clean up with subscription.unsubscribe().
Key gotcha: Do NOT call Supabase methods directly inside onAuthStateChange — use setTimeout(..., 0) to defer.
See examples/auth.md for sign up, sign in, OAuth, magic link, session management, middleware protection, and password reset patterns.
Use the query builder for type-safe CRUD with filters, joins, ordering, and pagination. Always add .select() after .insert() or .update() to return the affected row.
const { data, error } = await supabase
.from("posts")
.select("id, title, author:profiles(username)")
.eq("published", true)
.order("created_at", { ascending: false })
.range(0, PAGE_SIZE - 1);
See examples/database.md for complex queries, upserts, RPC calls, conditional filters, counting, and migrations.
RLS is the primary security mechanism. Enable it on every table, write separate policies per operation (not FOR ALL), and wrap auth.uid() in a subquery for performance.
alter table public.posts enable row level security;
create policy "posts_select" on public.posts for select to authenticated
using ( published = true or (select auth.uid()) = author_id );
Never trust user_metadata from JWT for access control — it is user-modifiable. See examples/database.md for full CRUD policies, team-based access, and anti-patterns.
Subscribe to database changes via channel().on('postgres_changes', ...). Always unsubscribe on cleanup. DELETE events cannot be filtered — all deletes are received. UPDATE/DELETE payloads need replica identity full for old record data.
const channel = supabase
.channel("room-messages")
.on(
"postgres_changes",
{
event: "INSERT",
schema: "public",
table: "messages",
filter: `room_id=eq.${roomId}`,
},
(payload) => {
/* handle */
},
)
.subscribe();
Use for chat, live dashboards, notifications. Avoid for high-frequency data (> 100 updates/sec).
Upload files with supabase.storage.from(bucket).upload(). Use getPublicUrl() for public buckets, createSignedUrl() for private buckets with time-limited access. Storage access control uses RLS on storage.objects.
See examples/storage.md for upload, signed URLs, public URLs, image transforms, bucket policies, and signed upload URLs.
Use Deno.serve() (not the deprecated serve import). Import supabase-js with npm: prefix: import { createClient } from "npm:@supabase/supabase-js@2". Handle CORS on every response. Use Deno.env.get() for secrets. Forward user JWT for RLS enforcement.
See examples/edge-functions.md for basic functions, authenticated access, shared utilities, webhooks, multi-route "fat functions", and background processing with EdgeRuntime.waitUntil().
<decision_framework>
Where is the code running?
├─ Browser / Client-side → publishable key (RLS enforced)
├─ Server / API route → publishable key + user JWT (RLS enforced per user)
└─ Admin / Migration script → secret key (bypasses RLS)
└─ NEVER expose the secret key in client bundles
What auth flow does the user need?
├─ Email + Password → signInWithPassword
├─ Social login (GitHub, Google, etc.) → signInWithOAuth
├─ Passwordless email → signInWithOtp (magic link)
├─ Phone + SMS → signInWithOtp (phone)
└─ SSO / SAML → signInWithSSO (enterprise)
How fresh must the data be?
├─ Instant (< 1 second) → Realtime subscription (postgres_changes)
├─ Near-instant (1-5 seconds) → Realtime subscription
├─ Periodic (> 5 seconds ok) → Polling with setInterval
└─ On-demand (user refresh) → Re-fetch on action
└─ High-frequency updates (> 100/sec)?
├─ YES → Polling or batch (Realtime has per-subscriber checks)
└─ NO → Realtime is fine
Who should access the files?
├─ Anyone (public assets, avatars) → Public bucket + getPublicUrl()
├─ Authenticated users only → Private bucket + createSignedUrl()
├─ Specific users (own files) → Private bucket + RLS on storage.objects
└─ Server-only processing → secret key for upload/download
Does the operation need server-side logic?
├─ Simple CRUD → Client query with RLS (no edge function needed)
├─ Multi-step / transactional → Edge function or Postgres function (RPC)
├─ Third-party API call → Edge function
├─ Webhook receiver → Edge function
└─ Heavy computation → Edge function with EdgeRuntime.waitUntil() for background work
</decision_framework>
<red_flags>
High Priority Issues:
service_role key) bypasses all RLS. Exposing it in browser bundles gives every user full admin database access.{ data, error } returns — Accessing data without checking error leads to runtime crashes when operations fail.auth.jwt() ->> 'user_metadata' in RLS policies — user_metadata is modifiable by authenticated users via updateUser(). Never use it for access control decisions.Medium Priority Issues:
FOR ALL in RLS policies — Separate into SELECT, INSERT, UPDATE, DELETE policies for clarity and auditability.auth.uid() in policies without subquery — Wrap in (select auth.uid()) for up to 94-99% performance improvement per Supabase benchmarks.to authenticated or to anon in policies — Without a role, policies apply to all roles, which may expose data unintentionally.select("*") everywhere — Fetches all columns including sensitive data. Select only the columns you need.serve import in Edge Functions — import { serve } from "https://deno.land/std/http/server.ts" is deprecated. Use Deno.serve().Common Mistakes:
.select() after .insert() or .update() — Without .select(), these methods return no data (only null).import { createClient } from "@supabase/supabase-js" fails in Deno. Use npm:@supabase/supabase-js@2.getSession() to verify auth — getSession() reads from local storage and can be tampered with. Use getUser() for secure server-side verification.Gotchas & Edge Cases:
replica identity full for old record data — By default, UPDATE and DELETE payloads only include the new record. Set alter table X replica identity full to access payload.old.onAuthStateChange fires on tab focus — SIGNED_IN events fire when a browser tab regains focus, not just on actual sign-in.onAuthStateChange callback — This can cause deadlocks. Use setTimeout(..., 0) to defer.createSignedUrl() URLs expire after the specified duration. Signed upload URLs expire after 2 hours./tmp — The /tmp directory is the only writable path in edge functions.</red_flags>
<critical_reminders>
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST enable Row Level Security (RLS) on EVERY table in an exposed schema — no exceptions)
(You MUST use the Database generic type with createClient<Database>() for type-safe queries)
(You MUST NEVER expose the secret key in client-side code — use the publishable key in browsers, the secret key only on the server)
(You MUST use (select auth.uid()) wrapped in a subquery inside RLS policies for performance)
(You MUST handle all Supabase responses with { data, error } destructuring — never assume success)
Failure to follow these rules will create security vulnerabilities, type-unsafe queries, and silent runtime failures.
</critical_reminders>
development
Material Design component library for Vue 3
development
VitePress 1.x — Vue-powered static site generator for documentation sites, built on Vite
tools
Docusaurus 3.x documentation framework — site configuration, docs/blog plugins, sidebars, versioning, MDX, swizzling, and deployment
development
TanStack Form patterns - useForm, form.Field, validators, arrays, linked fields, createFormHook, type safety