Row-Level Security (RLS)
All user-facing tables in Supabase have Row-Level Security enabled. RLS ensures users can only access their own data, even if an API bug exposes a direct database query.
Standard Pattern
Every user table follows the same RLS pattern:
-- Enable RLS
ALTER TABLE table_name ENABLE ROW LEVEL SECURITY;
-- Users can only see their own rows
CREATE POLICY "select_own" ON table_name
FOR SELECT USING (auth.uid() = user_id);
-- Users can only insert rows for themselves
CREATE POLICY "insert_own" ON table_name
FOR INSERT WITH CHECK (auth.uid() = user_id);
-- Users can only update their own rows
CREATE POLICY "update_own" ON table_name
FOR UPDATE USING (auth.uid() = user_id);
-- Users can only delete their own rows
CREATE POLICY "delete_own" ON table_name
FOR DELETE USING (auth.uid() = user_id);
Tables with RLS
| Table | RLS Policy |
|---|---|
user_profiles | auth.uid() = user_id |
token_balances | auth.uid() = user_id |
token_transactions | auth.uid() = user_id |
learning_progress | auth.uid() = user_id |
referral_codes | auth.uid() = user_id |
workspace_emails | auth.uid() = user_id |
workspace_projects | auth.uid() = user_id |
workspace_brands | auth.uid() = user_id |
SECURITY DEFINER Functions
Some operations require modifying tables that users can't directly write to (e.g., token_balances). These use SECURITY DEFINER functions:
CREATE OR REPLACE FUNCTION spend_tokens(p_action_name TEXT, p_description TEXT)
RETURNS TABLE(success BOOLEAN, new_balance INTEGER, cost INTEGER, error_message TEXT)
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
BEGIN
-- auth.uid() still resolves to the calling user
-- But the function runs with the function owner's privileges
UPDATE token_balances
SET balance = balance - v_cost
WHERE user_id = auth.uid()
AND balance >= v_cost;
...
END;
$$;
Key points:
SECURITY DEFINERruns with elevated privileges (table owner)auth.uid()still resolves to the calling user's ID- The atomic
WHERE balance >= costcheck prevents negative balances - These functions are the only way to modify
token_balances— users cannot UPDATE the table directly
Service Role Bypass
Admin operations that need to access data across users use the SUPABASE_SERVICE_ROLE_KEY, which bypasses RLS entirely. This key is:
- Only used server-side (never exposed to frontend)
- Only used in
verifyAdmin()flows - Stored as an environment variable in Vercel