Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.encoreos.io/llms.txt

Use this file to discover all available pages before exploring further.

Version: 1.0.0
Last Updated: 2026-01-11
Status: Active

Overview

This document provides guidelines and procedures for safely reverting database migrations, with a focus on RLS (Row Level Security) policy changes. Following these procedures ensures minimal downtime and prevents data exposure during rollback operations.

Table of Contents

  1. Pre-Migration Checklist
  2. RLS Policy Rollback Procedures
  3. Emergency Rollback Steps
  4. Policy State Capture Scripts
  5. Testing Rollback
  6. Rollback Templates

Pre-Migration Checklist

Before applying any migration that modifies RLS policies:

1. Document Current State

-- Capture all existing policies for affected tables
SELECT 
  schemaname,
  tablename,
  policyname,
  permissive,
  roles,
  cmd,
  qual AS using_expression,
  with_check
FROM pg_policies
WHERE schemaname = 'public'
  AND tablename IN ('table1', 'table2') -- List affected tables
ORDER BY tablename, policyname;

2. Create Rollback SQL

For every CREATE POLICY, prepare the inverse DROP POLICY:
-- Migration
CREATE POLICY "new_policy" ON table_name FOR SELECT USING (...);

-- Rollback
DROP POLICY IF EXISTS "new_policy" ON table_name;

3. Test in Staging

  • Apply migration to staging environment
  • Run RLS test suite
  • Verify no recursion risks
  • Confirm multi-tenant isolation
  • Test all CRUD operations

4. Backup Policy Definitions

Save the output of the state capture script to a file before migration:
# Save current state
psql -c "SELECT * FROM get_all_rls_policies()" > pre_migration_policies.sql

RLS Policy Rollback Procedures

Principle: Never Modify Policies In-Place

Always use the pattern:
  1. DROP POLICY IF EXISTS
  2. CREATE POLICY with new definition
This ensures atomic changes and clean rollback.

Standard Rollback Pattern

-- Step 1: Drop the problematic policy
DROP POLICY IF EXISTS "problematic_policy" ON affected_table;

-- Step 2: Recreate the original policy
CREATE POLICY "original_policy" ON affected_table
  FOR SELECT
  USING (original_using_expression);

Rolling Back Multiple Policies

Wrap in a transaction for atomicity:
BEGIN;

-- Drop new policies
DROP POLICY IF EXISTS "new_policy_1" ON table1;
DROP POLICY IF EXISTS "new_policy_2" ON table2;

-- Recreate original policies
CREATE POLICY "original_policy_1" ON table1 FOR SELECT USING (...);
CREATE POLICY "original_policy_2" ON table2 FOR SELECT USING (...);

COMMIT;

Emergency Rollback Steps

Severity Levels

LevelSymptomsResponse Time
CriticalData exposure across tenants, auth bypassImmediate (< 5 min)
HighUsers locked out, broken CRUD operations< 15 min
MediumPerformance degradation, partial feature failure< 1 hour
LowMinor inconsistencies, non-critical featuresNext maintenance window

Critical Emergency Procedure

  1. Identify the Problem
    -- Check for recursion risks
    SELECT * FROM get_rls_recursion_risks();
    
    -- Check for cross-tenant data leaks
    SELECT * FROM verify_tenant_isolation();
    
  2. Disable Problematic Policies Immediately
    -- Temporarily make table admin-only
    DROP POLICY IF EXISTS "problematic_policy" ON affected_table;
    
    CREATE POLICY "emergency_lockdown" ON affected_table
      FOR ALL
      USING (pf_is_platform_admin(auth.uid()));
    
  3. Notify Stakeholders
    • Alert development team
    • Document incident timeline
    • Prepare communication for affected users
  4. Apply Proper Rollback
    • Execute pre-prepared rollback SQL
    • Verify with test suite
    • Monitor for 30 minutes
  5. Post-Incident Review
    • Document root cause
    • Update rollback procedures if needed
    • Add regression tests

Policy State Capture Scripts

Capture All Policies

-- Create a function to capture current RLS state
CREATE OR REPLACE FUNCTION get_all_rls_policies()
RETURNS TABLE (
  schema_name text,
  table_name text,
  policy_name text,
  policy_cmd text,
  policy_roles text[],
  using_expr text,
  with_check_expr text,
  create_statement text
)
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
BEGIN
  RETURN QUERY
  SELECT 
    p.schemaname::text,
    p.tablename::text,
    p.policyname::text,
    p.cmd::text,
    p.roles::text[],
    p.qual::text,
    p.with_check::text,
    format(
      'CREATE POLICY %I ON %I.%I FOR %s TO %s USING (%s)%s;',
      p.policyname,
      p.schemaname,
      p.tablename,
      p.cmd,
      array_to_string(p.roles, ', '),
      COALESCE(p.qual::text, 'true'),
      CASE WHEN p.with_check IS NOT NULL 
           THEN format(' WITH CHECK (%s)', p.with_check::text)
           ELSE '' END
    )
  FROM pg_policies p
  WHERE p.schemaname = 'public'
  ORDER BY p.tablename, p.policyname;
END;
$$;

Capture Policies for Specific Tables

-- Get policies for specific tables as rollback SQL
SELECT format(
  E'-- Rollback for %I\nDROP POLICY IF EXISTS %I ON %I;\nCREATE POLICY %I ON %I\n  FOR %s\n  USING (%s)%s;\n',
  tablename,
  policyname,
  tablename,
  policyname,
  tablename,
  cmd,
  COALESCE(qual::text, 'true'),
  CASE WHEN with_check IS NOT NULL 
       THEN format(E'\n  WITH CHECK (%s)', with_check::text)
       ELSE '' END
) AS rollback_sql
FROM pg_policies
WHERE schemaname = 'public'
  AND tablename = 'your_table_name'
ORDER BY policyname;

Generate DROP Statements for New Policies

-- Generate DROP statements for policies created after a specific date
-- (Requires tracking policy creation dates separately)
SELECT format('DROP POLICY IF EXISTS %I ON %I;', policyname, tablename)
FROM pg_policies
WHERE schemaname = 'public'
  AND policyname LIKE 'new_prefix_%';

Testing Rollback

Verification Queries

After rollback, run these checks:
-- 1. Verify no recursion risks
SELECT * FROM get_rls_recursion_risks();
-- Expected: 0 rows

-- 2. Check policy counts per table
SELECT tablename, COUNT(*) as policy_count
FROM pg_policies 
WHERE schemaname = 'public'
GROUP BY tablename
ORDER BY policy_count;

-- 3. Verify UPDATE policies have WITH CHECK
SELECT tablename, policyname
FROM pg_policies
WHERE schemaname = 'public'
  AND cmd = 'UPDATE'
  AND with_check IS NULL;
-- Expected: 0 rows (or known exceptions)

-- 4. Test multi-tenant isolation
-- Run as org1_user, should not see org2 data
SELECT COUNT(*) FROM your_table WHERE organization_id = 'org2_id';
-- Expected: 0

RLS Test Suite

Run the full RLS test suite after rollback:
# Run RLS-specific tests
npm run test -- --grep "RLS"

# Run multi-tenant isolation tests
npm run test -- tests/rls/

Rollback Templates

Template A: Single Policy Rollback

-- Rollback: [Policy Name] on [Table Name]
-- Date: YYYY-MM-DD
-- Reason: [Brief explanation]

BEGIN;

-- Remove problematic policy
DROP POLICY IF EXISTS "new_policy_name" ON table_name;

-- Restore original policy
CREATE POLICY "original_policy_name" ON table_name
  FOR [SELECT|INSERT|UPDATE|DELETE|ALL]
  USING (original_using_expression)
  WITH CHECK (original_with_check_expression);

COMMIT;

-- Verification
SELECT * FROM pg_policies WHERE tablename = 'table_name';

Template B: Multi-Table Rollback

-- Rollback: Migration YYYYMMDD_description
-- Date: YYYY-MM-DD
-- Affected Tables: table1, table2, table3
-- Reason: [Brief explanation]

BEGIN;

-- ============================================
-- Table 1: table1
-- ============================================
DROP POLICY IF EXISTS "new_table1_select" ON table1;
DROP POLICY IF EXISTS "new_table1_insert" ON table1;
DROP POLICY IF EXISTS "new_table1_update" ON table1;
DROP POLICY IF EXISTS "new_table1_delete" ON table1;

CREATE POLICY "old_table1_select" ON table1 FOR SELECT USING (...);
CREATE POLICY "old_table1_insert" ON table1 FOR INSERT WITH CHECK (...);
CREATE POLICY "old_table1_update" ON table1 FOR UPDATE USING (...) WITH CHECK (...);
CREATE POLICY "old_table1_delete" ON table1 FOR DELETE USING (...);

-- ============================================
-- Table 2: table2
-- ============================================
-- ... repeat pattern ...

COMMIT;

-- Verification
SELECT tablename, COUNT(*) 
FROM pg_policies 
WHERE tablename IN ('table1', 'table2', 'table3')
GROUP BY tablename;

Template C: Helper Function Rollback

-- Rollback: Helper Function [function_name]
-- Date: YYYY-MM-DD
-- Reason: [Brief explanation]

-- Step 1: Drop dependent policies first
DROP POLICY IF EXISTS "policy_using_function" ON affected_table;

-- Step 2: Drop new function
DROP FUNCTION IF EXISTS new_function_name(args);

-- Step 3: Recreate original function
CREATE OR REPLACE FUNCTION original_function_name(args)
RETURNS boolean
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path = public
AS $$
  -- Original function body
$$;

-- Step 4: Recreate policies using original function
CREATE POLICY "policy_using_function" ON affected_table
  FOR SELECT
  USING (original_function_name(auth.uid()));

Best Practices Summary

  1. Always prepare rollback SQL before migration
  2. Test in staging first
  3. Use transactions for atomic rollbacks
  4. Never modify policies in-place - always DROP + CREATE
  5. Document every rollback with date and reason
  6. Run verification queries after every rollback
  7. Keep rollback SQL files version-controlled
  8. Set up monitoring alerts for RLS failures


Changelog

VersionDateChanges
1.0.02026-01-11Initial document creation