Row Level Security (RLS) in Expo + Supabase: Your Backend Access Control
When building mobile apps with Expo and Supabase, Row Level Security (RLS) becomes your primary defense mechanism for data access control. Since your mobile app connects directly to the database through Supabase's API, RLS policies act as your backend security layer.
What is RLS?
Row Level Security is a Postgres feature that lets you define rules (policies) for who can access or modify specific rows in a table. Instead of granting blanket table permissions, you can create fine-grained access control based on user identity, roles, or any other criteria.
In your Expo + Supabase stack:
- Database: Supabase (Postgres with RLS)
- Frontend: Expo (React Native)
- Auth: Supabase Auth with JWT tokens
How RLS Works in Your App Flow
- User authenticates via Supabase Auth and receives a JWT token
- Expo app makes requests to Supabase with that token
- RLS policies inspect the JWT's
sub
(user ID), roles, and other claims - Postgres returns only rows that match your policy rules
This means you don't need a separate API layer for basic access control - RLS handles it at the database level.
Common RLS Policy Patterns
User-Owned Data Access
The most common pattern: users can only access their own data.
-- Enable RLS on the table
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
-- Users can only see their own profile
CREATE POLICY "Users can view own profile" ON profiles
FOR SELECT USING (auth.uid() = id);
-- Users can only update their own profile
CREATE POLICY "Users can update own profile" ON profiles
FOR UPDATE USING (auth.uid() = id);
-- Users can insert their own profile
CREATE POLICY "Users can insert own profile" ON profiles
FOR INSERT WITH CHECK (auth.uid() = id);
Public Read, Private Write
Perfect for public profiles or posts where anyone can read but only owners can modify.
-- Anyone can read profiles
CREATE POLICY "Profiles are publicly readable" ON profiles
FOR SELECT USING (true);
-- Only profile owners can update
CREATE POLICY "Users can update own profile" ON profiles
FOR UPDATE USING (auth.uid() = user_id);
Relationship-Based Access
For data involving multiple users, like messages or shared documents.
-- Users can read messages they sent or received
CREATE POLICY "Users can read their messages" ON messages
FOR SELECT USING (
auth.uid() = sender_id OR auth.uid() = receiver_id
);
-- Users can only send messages as themselves
CREATE POLICY "Users can send messages" ON messages
FOR INSERT WITH CHECK (auth.uid() = sender_id);
Implementing RLS in Your Expo App
The beauty of RLS is that your Expo code remains clean and simple:
// This query automatically respects RLS policies
const { data, error } = await supabase
.from('profiles')
.select('*');
// Only returns profiles the user is allowed to see
// No need for manual filtering or authorization checks
For real-time subscriptions:
// Subscriptions also respect RLS
const subscription = supabase
.channel('profiles')
.on('postgres_changes',
{ event: '*', schema: 'public', table: 'profiles' },
(payload) => console.log('Change received!', payload)
)
.subscribe();
Advanced Patterns
Role-Based Access
-- Create a function to get user role
CREATE OR REPLACE FUNCTION get_user_role()
RETURNS text AS $$
SELECT raw_user_meta_data->>'role'
FROM auth.users
WHERE id = auth.uid()
$$ LANGUAGE sql SECURITY DEFINER;
-- Admin users can see all profiles
CREATE POLICY "Admins can view all profiles" ON profiles
FOR SELECT USING (get_user_role() = 'admin');
Time-Based Access
-- Users can only edit posts within 24 hours of creation
CREATE POLICY "Users can edit recent posts" ON posts
FOR UPDATE USING (
auth.uid() = author_id AND
created_at > now() - interval '24 hours'
);
Testing Your RLS Policies
Always test with actual user tokens, not the service role key:
// Create a test user
const { data: user } = await supabase.auth.signUp({
email: 'test@example.com',
password: 'password'
});
// Make requests as that user to verify policies work
const { data } = await supabase
.from('profiles')
.select('*');
Important Gotchas
- Default Deny: Once RLS is enabled, all access is blocked until you create policies
- Service Role Bypasses RLS: Server-side functions using the service key ignore RLS
- Policy Order Matters: More specific policies should come first
- Performance: Complex policies can impact query performance
Security Benefits
With proper RLS policies, you get:
- Data isolation between users automatically
- Protection against client-side tampering or reverse engineering
- Consistent security across all database access points
- Real-time security for subscriptions and live updates
RLS transforms Supabase from just a database-as-a-service into a secure backend-as-a-service, making it perfect for mobile apps that need robust data protection without the complexity of building custom APIs.
The key is starting with simple policies and gradually adding complexity as your app's requirements grow. Your Expo app stays clean while Postgres handles the heavy lifting of access control.