When building with Supabase, Postgres Views can be a powerful tool for simplifying complex queries. But they come with a critical security consideration that isn't immediately obvious: Views bypass Row Level Security (RLS) by default, potentially exposing sensitive data even when your tables are properly secured.
As a reminder: RLS is what allows you to safely query your Supabase database directly from the frontend, without routing through your backend server. But if RLS isn't working on a table, that table is like your phone screen on public transport—anyone who wants to take a glance, can.
The Security Challenge
Even if you've carefully configured RLS policies on your tables, views can create an unintended backdoor because:
- By default, views don’t use RLS
- Supabase's RLS policies page doesn't show or warn about exposed views (last checked 09.03.2025)
- Views don't support RLS in the same way tables do
Testing Your View's Security
Before deploying any view to production, it's crucial to verify that it properly respects your RLS policies. Here's a quick way to test if your view is secure:
// First, sign in as a specific user
// Then try to fetch ALL rows from your view
const { data } = await supabase.from('my_view').select('*')
// If your view respects RLS, you should only see rows this user has permission to access.
// If you see ALL rows, your view is bypassing RLS! 🚨
console.log("view response" ,data)
Securing Your Views
To protect your data, you have several options:
- For Postgres 15+:
CREATE VIEW public.my_view
WITH (security_invoker = true) AS
SELECT * FROM my_table;
This applies the RLS of my_table
to the view you’re creating.
- For older Postgres versions:
- Create an internal schema:
CREATE SCHEMA internal;
- Re-create the sensitive view in the internal schema
- Delete the public version of the view
When to Use Views
Views are particularly valuable when you need to:
- Simplify complex queries that you use frequently
- Add computed columns that can't be generated columns
- Create virtual tables that recalculate with each request
Example: Active Subscription Status
I recently built a subscription system and wanted to avoid having to write active_until > NOW()
in every query where I'd need to check for active subscriptions. Planning ahead, I first considered adding an is_active
generated column to the table. But I hit a wall: Postgres doesn't allow volatile functions like now()
in generated columns. That's when I turned to views as a solution:
CREATE VIEW public.active_subscriptions
WITH (security_invoker = true) AS
SELECT
*,
(active_until > now()) AS is_active
FROM
public.subscriptions;
This view has been working perfectly, giving me clean queries while maintaining security through security_invoker
.
Top comments (0)