Appearance
Access Control Deep Dive (RLS) ​
Purpose: Summarize Row Level Security (RLS) patterns used by the app and provide canonical policy examples. Audience: Developer, Admin Prerequisites: Supabase project; confirm actual policies in migrations.
Principles
- Principle of least privilege: public can view only public/approved data
- Ownership-based access for writes (seller_id, user_id)
- Role checks for admin-only tables/actions
Listings
- Public SELECT for active listings; owners/admins have broader access
- Sensitive fields protected via blur policy: see
docs/architecture/blur-policy.md(blurred_fieldstable,can_view_fieldfunction,listing_view_secureview) - Critical: Public visibility requires BOTH
status='active'ANDverification_status='approved'/'verified' - Example policies (actual from DB):
sql
-- Consolidated policy for all operations
CREATE POLICY "listings_consolidated" ON listings
FOR ALL TO public
USING (
(seller_id = auth.uid()) OR
is_admin_user() OR
((status = 'active') AND (verification_status = ANY (ARRAY['approved', 'verified']))) OR
(auth.role() = 'service_role')
);Important: The dual-condition requirement means:
- Admin approval alone (
status='active') is insufficient for public visibility - Must also update
verification_status='approved'for verified sellers - Unverified sellers' listings remain hidden from public even when approved
Offers
- Participants (buyer_id or seller_id) can see offers; buyer inserts; seller updates status
sql
-- Participants can view
create policy view_own_offers on offers
for select to authenticated using (buyer_id = auth.uid() or seller_id = auth.uid());
-- Buyer can insert
create policy buyer_insert on offers
for insert to authenticated with check (buyer_id = auth.uid());
-- Seller updates (status/response)
create policy seller_update on offers
for update to authenticated using (seller_id = auth.uid());Messaging
- Only conversation participants can read messages; sender inserts
sql
-- Participants read
create policy read_messages on messages
for select to authenticated using (
exists (
select 1 from conversation_participants cp
where cp.conversation_id = messages.conversation_id
and cp.user_id = auth.uid()
)
);
-- Sender inserts
create policy insert_messages on messages
for insert to authenticated with check (sender_id = auth.uid());Notifications
- Users can see their own notifications
sql
create policy view_own_notifications on notifications
for select to authenticated using (user_id = auth.uid());Admin tables
- Admin-only access (managed through Postgres roles or custom function checks)
sql
-- Example: block public; grant explicit to admin role
alter table admin_messages enable row level security;
create policy deny_all on admin_messages for all to public using (false);
-- Assign separate role (admin) at connection or enforce via SECURITY DEFINER RPCsSubscriptions
- Users can read their own subscription rows
sql
create policy read_own_subscription on subscriptions
for select to authenticated using (user_id = auth.uid());Indexes & performance for RLS
- Messaging participant checks: index conversation_participants (conversation_id, user_id)
- Listings public read: partial index on listings(status) where status = 'active'
- Offers participants: composite (buyer_id), (seller_id)
Audit & logging
- Use
email_logs,admin_*tables to keep audit trails - Consider triggers to stamp
updated_atconsistently
Notes
- These examples are canonical patterns; confirm actual policies in
supabase/migrations. - Prefer SECURITY DEFINER functions for admin actions exposed to authenticated clients.
Related
- Data model:
docs/data-model/schema.md - Admin tables:
docs/architecture/admin-tables.md