RLS Policies
Row Level Security policies per table, testing strategies, and common patterns.
What Is RLS
Row Level Security (RLS) is a PostgreSQL feature that restricts which rows a user can access. When enabled on a table, every query is filtered through policies -- even if you SELECT *, you only get rows you're authorized to see.
In ScaleRocket, RLS is the primary authorization layer. The Supabase anon key (used by clients) connects as the anon role, which is restricted by RLS. The service role key bypasses RLS entirely.
Policies by Table
profiles
-- Users can read their own profile
CREATE POLICY "profiles_select_own"
ON profiles FOR SELECT
USING (auth.uid() = id);
-- Users can update their own profile
CREATE POLICY "profiles_update_own"
ON profiles FOR UPDATE
USING (auth.uid() = id)
WITH CHECK (auth.uid() = id);
-- Profile creation handled by trigger (no INSERT policy needed for users)subscriptions
-- Users can read their own subscription
CREATE POLICY "subscriptions_select_own"
ON subscriptions FOR SELECT
USING (auth.uid() = user_id);
-- No INSERT/UPDATE/DELETE for users
-- Managed exclusively by Edge Functions via service rolecredits
-- Users can read their own credit balance
CREATE POLICY "credits_select_own"
ON credits FOR SELECT
USING (auth.uid() = user_id);
-- No INSERT/UPDATE/DELETE for users
-- Managed by Edge Functions and database functions (SECURITY DEFINER)posts (blog)
-- Anyone can read published posts (public blog)
CREATE POLICY "posts_select_published"
ON posts FOR SELECT
USING (published = true);
-- No INSERT/UPDATE/DELETE for regular users
-- Admin panel uses service roleTesting Policies
Using the Supabase dashboard
- Go to SQL Editor in your Supabase dashboard.
- Run queries as a specific user:
-- Set the role and user ID for testing
SET request.jwt.claims = '{"sub": "user-uuid-here"}';
SET role = 'authenticated';
-- This should return only the user's own profile
SELECT * FROM profiles;
-- This should return only the user's subscription
SELECT * FROM subscriptions;Using the client SDK
// This query is automatically filtered by RLS
const { data, error } = await supabase
.from("profiles")
.select("*");
// data will only contain the current user's profileVerifying policies block unauthorized access
-- As an authenticated user, try to read another user's data
SET request.jwt.claims = '{"sub": "user-a-uuid"}';
SET role = 'authenticated';
SELECT * FROM profiles WHERE id = 'user-b-uuid';
-- Should return 0 rows
UPDATE profiles SET full_name = 'hacked' WHERE id = 'user-b-uuid';
-- Should affect 0 rowsCommon Patterns
User owns the row
The most common pattern -- users can only access rows where their user_id matches:
CREATE POLICY "user_owns_row"
ON my_table FOR ALL
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);Public read, authenticated write
Useful for shared content:
-- Anyone can read
CREATE POLICY "public_read"
ON my_table FOR SELECT
USING (true);
-- Only authenticated users can insert their own rows
CREATE POLICY "authenticated_insert"
ON my_table FOR INSERT
WITH CHECK (auth.uid() = user_id);Read-only for users, service role manages
For data that users can view but only the server can modify (subscriptions, credits):
-- Users can read their own
CREATE POLICY "select_own"
ON my_table FOR SELECT
USING (auth.uid() = user_id);
-- No INSERT/UPDATE/DELETE policies
-- Edge Functions use service role to bypass RLSTeam/organization access
If you add teams or organizations:
CREATE POLICY "team_members_read"
ON team_data FOR SELECT
USING (
EXISTS (
SELECT 1 FROM team_members
WHERE team_members.team_id = team_data.team_id
AND team_members.user_id = auth.uid()
)
);Important Notes
- RLS is deny-by-default. If no policy matches, the query returns zero rows (SELECT) or fails (INSERT/UPDATE/DELETE).
- Enable RLS on every table. A table with RLS disabled is accessible to anyone with the anon key.
WITH CHECKis for writes.USINGfilters which rows can be read/modified.WITH CHECKvalidates new data on INSERT/UPDATE.- Service role bypasses RLS. This is intentional -- Edge Functions need unrestricted access to manage subscriptions and credits.
- Test after every migration. Always verify that new tables have RLS enabled and appropriate policies.
-- Quick check: which tables have RLS disabled?
SELECT tablename
FROM pg_tables
WHERE schemaname = 'public'
AND tablename NOT IN (
SELECT tablename FROM pg_tables t
JOIN pg_class c ON c.relname = t.tablename
WHERE c.relrowsecurity = true
);Done reading? Mark this page as complete.