Why RLS Matters for SaaS

Without RLS, your API is the only barrier between a user and every row in your database. One accidental "fetch all" query and you have a data breach.

With RLS, PostgreSQL enforces access rules at query execution time. No matter what your API sends, the database only returns rows the authenticated user is allowed to see. This is security-in-depth — your API and your database both enforce access control independently.

Enabling RLS

Enable RLS on every user-facing table. Tables without RLS policies are wide open by default (if accessed via the service role key) or completely inaccessible (if accessed via the anon key without policies).

ALTER TABLE projects ENABLE ROW LEVEL SECURITY;

Once enabled, no rows are returned by default until you create policies. This is the correct security posture.

The Basic User Ownership Policy

The most common pattern — users can only see and modify their own rows:
CREATE POLICY "Users can view own rows"
  ON projects FOR SELECT
  USING (auth.uid() = user_id);
CREATE POLICY "Users can insert own rows"
  ON projects FOR INSERT
  WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can update own rows"
  ON projects FOR UPDATE
  USING (auth.uid() = user_id);
CREATE POLICY "Users can delete own rows"
  ON projects FOR DELETE
  USING (auth.uid() = user_id);

Multi-Tenant Workspace Policies

For SaaS with teams and workspaces, check workspace membership:
CREATE POLICY "Workspace members can view projects"
  ON projects FOR SELECT
  USING (
    workspace_id IN (
      SELECT workspace_id FROM workspace_members
      WHERE user_id = auth.uid()
    )
  );

This checks the workspace_members junction table on every query. Index workspace_members(user_id, workspace_id) for performance.

Role-Based Access Control

Add role checking for admin operations:
CREATE POLICY "Only admins can delete workspace projects"
  ON projects FOR DELETE
  USING (
    workspace_id IN (
      SELECT workspace_id FROM workspace_members
      WHERE user_id = auth.uid() AND role = 'admin'
    )
  );

Store roles in your workspace_members table (role text DEFAULT 'member'). Roles: owner, admin, member, viewer.

Common Mistakes to Avoid

1. Forgetting to enable RLS on new tables. Create a checklist: every new table gets RLS enabled before it's connected to the frontend.

2. Using service role key in the frontend. The service role key bypasses RLS. Never expose it to users — use the anon key in WeWeb/FlutterFlow and the service role only in server-side Xano functions.

3. No policy for INSERT. Many developers add SELECT and UPDATE policies but forget INSERT. Without it, anon key users can't create rows at all.

4. N+1 policy lookups. If your policy JOINs a large table on every query, you'll see performance issues at scale. Materialise membership lookups or use indexes aggressively.