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
- Pre-Migration Checklist
- RLS Policy Rollback Procedures
- Emergency Rollback Steps
- Policy State Capture Scripts
- Testing Rollback
- 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
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:
DROP POLICY IF EXISTS
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
| Level | Symptoms | Response Time |
|---|
| Critical | Data exposure across tenants, auth bypass | Immediate (< 5 min) |
| High | Users locked out, broken CRUD operations | < 15 min |
| Medium | Performance degradation, partial feature failure | < 1 hour |
| Low | Minor inconsistencies, non-critical features | Next maintenance window |
Critical Emergency Procedure
-
Identify the Problem
-- Check for recursion risks
SELECT * FROM get_rls_recursion_risks();
-- Check for cross-tenant data leaks
SELECT * FROM verify_tenant_isolation();
-
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()));
-
Notify Stakeholders
- Alert development team
- Document incident timeline
- Prepare communication for affected users
-
Apply Proper Rollback
- Execute pre-prepared rollback SQL
- Verify with test suite
- Monitor for 30 minutes
-
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
- Always prepare rollback SQL before migration
- Test in staging first
- Use transactions for atomic rollbacks
- Never modify policies in-place - always DROP + CREATE
- Document every rollback with date and reason
- Run verification queries after every rollback
- Keep rollback SQL files version-controlled
- Set up monitoring alerts for RLS failures
Changelog
| Version | Date | Changes |
|---|
| 1.0.0 | 2026-01-11 | Initial document creation |