Skip to main content

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

TableRLS Policy
user_profilesauth.uid() = user_id
token_balancesauth.uid() = user_id
token_transactionsauth.uid() = user_id
learning_progressauth.uid() = user_id
referral_codesauth.uid() = user_id
workspace_emailsauth.uid() = user_id
workspace_projectsauth.uid() = user_id
workspace_brandsauth.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 DEFINER runs with elevated privileges (table owner)
  • auth.uid() still resolves to the calling user's ID
  • The atomic WHERE balance >= cost check 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