Skip to main content

Database Security

CommonPlace uses PostgreSQL Row Level Security (RLS) to enforce access control at the database level.

Why RLS?

Row Level Security ensures that:

  1. Security is enforced at the database level — Not just application code
  2. Every query is filtered — No accidental data leaks
  3. Direct database access is safe — Even with the anon key

RLS Principles

Enable RLS on All Tables

ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

Every table with user data has RLS enabled.

Default Deny

By default, with RLS enabled, all access is denied. Policies explicitly grant access.

User Context

Policies use auth.uid() to get the current user:

CREATE POLICY "Users can read own posts"
ON posts FOR SELECT
USING (user_id = auth.uid());

Common Policy Patterns

Owner-Only Access

-- Only owner can read
CREATE POLICY "Users read own data"
ON user_settings FOR SELECT
USING (user_id = auth.uid());

-- Only owner can update
CREATE POLICY "Users update own data"
ON user_settings FOR UPDATE
USING (user_id = auth.uid());

Friend Access

-- Friends can read posts
-- Note: friendships uses requester_id/addressee_id columns, checked bidirectionally
CREATE POLICY "Friends can read posts"
ON posts FOR SELECT
USING (
user_id = auth.uid()
OR EXISTS (
SELECT 1 FROM friendships
WHERE status = 'accepted'
AND (
(requester_id = posts.user_id AND addressee_id = auth.uid())
OR (requester_id = auth.uid() AND addressee_id = posts.user_id)
)
)
);

Block Filtering

-- Blocked users don't exist
CREATE POLICY "Blocked users invisible"
ON profiles FOR SELECT
USING (
NOT EXISTS (
SELECT 1 FROM blocks
WHERE (blocker_id = auth.uid() AND blocked_id = profiles.id)
OR (blocker_id = profiles.id AND blocked_id = auth.uid())
)
);

Admin Access

-- Admins can read all reports
-- Note: Admin status is determined via the admin_roles table, NOT an is_admin column on profiles.
-- See 20260202_admin_system.sql for the admin_roles schema.
CREATE POLICY "Admins can view all reports"
ON reports FOR SELECT TO authenticated
USING (
EXISTS (
SELECT 1 FROM admin_roles
WHERE admin_roles.user_id = auth.uid()
)
);

Security Functions

admin_resolve_report()

Allows admins to resolve reports with proper permission checks. Caveat: This function was added in migration 20260207_admin_report_functions.sql, but its implementation references a profiles.is_admin column that does not exist. The actual admin system uses the admin_roles table (see 20260202_admin_system.sql). This function needs to be updated to use admin_roles instead. See CLAUDE.md known bugs (P1).

is_friend()

Returns true if two users are friends.

can_message()

Checks if messaging is allowed between two users.

Testing Policies

Test policies in the SQL Editor:

-- Set user context
SET request.jwt.claim.sub = 'user-uuid-here';

-- Test query
SELECT * FROM posts;

Security Checklist

  • RLS enabled on all user data tables
  • No SELECT * without policy
  • Block filtering applied everywhere
  • Admin functions verify role via admin_roles table (not is_admin column)
  • Sensitive operations use SECURITY DEFINER

Last updated: 2026-02-07