A PostgreSQL extension that adds a recycle bin for dropped tables. Instead of permanently losing data when a DROP TABLE is executed, pg_flashback intercepts the command, moves the table to a safe schema, and lets you restore it later.
Built with pgrx in Rust.
-- Enable extension
CREATE EXTENSION pg_flashback;
-- Create test table
CREATE TABLE test (id int);
INSERT INTO test VALUES (1), (2);
-- Drop it
DROP TABLE test;
-- See recycle bin
SELECT * FROM flashback_list_recycled_tables();
-- Restore it
SELECT flashback_restore('test');
-- Verify
SELECT * FROM test;- Automatically captures
DROP TABLE(including multi-tableDROP TABLE t1, t2),DROP TABLE ... CASCADE, andTRUNCATE TABLE - Captures
DROP SCHEMA ... CASCADE— all tables in the schema are saved before deletion - Stores dropped/truncated tables in a
flashback_recycleschema until restored or expired - Restore by table name or by specific operation ID (useful when a table was dropped multiple times)
- Full metadata capture and restore:
- Dependent views are dropped and recreated on restore
- Incoming FK constraints (from other tables pointing at the dropped table) are captured and re-applied
- RLS policies are captured and re-applied, with
ENABLE ROW LEVEL SECURITYif needed - Triggers survive restore automatically (they travel with the physical table)
- Partitioned tables: children are co-moved and re-attached; missing partitions are recreated as empty shells with a warning
- Quoted identifiers fully supported: table and schema names with hyphens, spaces, or special characters work correctly (e.g.
DROP TABLE "my-table",DROP TABLE "My Schema"."My Table") - Sequence / IDENTITY column state automatically restored after DROP and TRUNCATE recovery
- FIFO eviction when table count or total size limits are reached
- Background worker that automatically purges expired entries
- Permission model: superusers see and manage all tables; regular users can only see and restore their own
- Configurable via PostgreSQL GUCs (no config files needed)
- Skips temporary tables, internal schemas, and user-defined excluded schemas
- Race condition safe: advisory locks keyed by
op_idprevent concurrent restores of the same operation
| PostgreSQL | Status |
|---|---|
| 18 | Supported |
| 17 | Supported |
| 16 | Supported |
| 15 | Supported |
| 14 | Supported |
- PostgreSQL 14 – 18
- pgrx 0.16.1
- Rust (stable)
git clone https://github.com/CaghanTU/pg_flashback.git
cd pg_flashback
./install.sh # builds with pgrx and installs to PostgreSQL
# Alternative (manual/pro)
cargo pgrx install --release- Set
shared_preload_librariesinpostgresql.conf:
shared_preload_libraries = 'pg_flashback'
Requires restart.
- Restart PostgreSQL.
- Extension is created in the target database:
CREATE EXTENSION pg_flashback;- Optional but recommended for non-
postgresdatabases: configure the worker connection DB.
ALTER SYSTEM SET flashback.database_name = 'your_database_name';
SELECT pg_reload_conf();A DDL hook fires on every DROP TABLE and TRUNCATE TABLE. On DROP, the table is moved to flashback_recycle and renamed with a unique op_id suffix. On TRUNCATE, the data is copied to flashback_recycle before truncation proceeds. Metadata (original schema, owner, operation type, retention deadline) is written to flashback.operations. A background worker periodically purges expired entries; FIFO eviction runs before each capture if a size or count limit is reached.
Lists all tables currently in the recycle bin.
SELECT * FROM flashback_list_recycled_tables();| Column | Type | Description |
|---|---|---|
schema_name |
text | Original schema |
table_name |
text | Original table name |
recycled_name |
text | Internal name in flashback_recycle schema |
dropped_at |
text | Timestamp when the table was dropped/truncated |
role_name |
text | The user who dropped/truncated the table |
retention_until |
text | Expiry date (auto-purged after this) |
op_id |
bigint | Unique operation ID |
operation_type |
text | DROP or TRUNCATE |
Superusers see all entries. Regular users see only the tables they dropped.
Restores the most recently dropped or truncated version of a table.
-- Restore to the original schema (two ways)
SELECT flashback_restore('orders');
SELECT flashback_restore('orders', NULL);
-- Restore to a different schema
SELECT flashback_restore('orders', 'archive');Returns true on success, false on failure (with a WARNING message explaining why).
Regular users can only restore tables they dropped themselves.
Restores a specific version of a table by its operation ID. Useful when the same table was dropped multiple times and you need a specific version.
-- Find which version you want
SELECT op_id, dropped_at FROM flashback_list_recycled_tables() WHERE table_name = 'orders';
-- Restore that specific version
SELECT flashback_restore_by_id(3);
-- Or restore to a different schema
SELECT flashback_restore_by_id(3, 'archive');Permanently removes the most recently dropped/truncated version of a table from the recycle bin without restoring it.
SELECT flashback_purge('orders');Returns true on success. Regular users can only purge tables they dropped.
Permanently removes a specific entry from the recycle bin by operation ID.
SELECT flashback_purge_by_id(42);Returns true on success. Regular users can only purge their own entries.
Permanently removes all entries from the recycle bin.
SELECT flashback_purge_all();Returns the number of tables that were purged. Superusers purge everything; regular users purge only their own tables.
Restores all tables currently in the recycle bin to their original schemas in a single call. Useful after a mass-drop incident (e.g. accidental DROP SCHEMA ... CASCADE across multiple schemas).
SELECT flashback_restore_all();Returns the count of successfully restored tables. Schemas that no longer exist are automatically recreated. Superusers restore all entries; regular users restore only the tables they dropped themselves. Tables are restored newest-first so that a DROP followed by a TRUNCATE on the same table is always handled in the correct order.
Restores all unrecovered entries that originally belong to one schema.
-- Restore everything back to its original schema name
SELECT flashback_restore_schema('sales', NULL);
-- Restore everything from one source schema into another destination schema
SELECT flashback_restore_schema('sales', 'sales_restored');Returns the number of successfully restored tables.
Shows the current state of the recycle bin alongside the configured limits.
SELECT * FROM flashback_status();| Column | Type | Description |
|---|---|---|
table_count |
bigint | Number of tables currently in the recycle bin |
table_limit |
int | Maximum allowed tables (flashback.max_tables) |
total_size_bytes |
bigint | Total physical size of recycled tables |
size_limit_mb |
int | Maximum allowed size in MB (flashback.max_size) |
retention_days |
int | Retention period in days (flashback.retention_days) |
worker_interval_seconds |
int | Background worker run interval (flashback.worker_interval_seconds) |
oldest_entry |
text | Timestamp of the oldest entry in the recycle bin |
newest_entry |
text | Timestamp of the newest entry in the recycle bin |
All settings are adjustable at runtime without restarting PostgreSQL (requires superuser or pg_suset role).
| GUC | Default | Range | Description |
|---|---|---|---|
flashback.retention_days |
7 |
1 – 365 | Days to keep dropped tables before auto-purge |
flashback.max_tables |
100 |
1 – 10000 | Maximum number of tables in the recycle bin |
flashback.max_size |
102400 (100 GB) |
1 – 1048576 MB | Maximum total size of the recycle bin |
flashback.worker_interval_seconds |
60 |
10 – 86400 | How often the cleanup worker runs |
flashback.excluded_schemas |
(empty) | — | Comma-separated list of schemas to exclude |
flashback.database_name |
postgres |
— | Database the background worker connects to; set this when pg_flashback is installed in a database other than postgres |
-- Keep tables for 30 days
ALTER SYSTEM SET flashback.retention_days = 30;
-- Allow at most 50 tables in the recycle bin
ALTER SYSTEM SET flashback.max_tables = 50;
-- Set a 10 GB size cap
ALTER SYSTEM SET flashback.max_size = 10240;
-- Run the cleanup worker every 10 minutes
ALTER SYSTEM SET flashback.worker_interval_seconds = 600;
-- Exclude staging and temp schemas from the recycle bin
ALTER SYSTEM SET flashback.excluded_schemas = 'staging,temp_data';
-- Tell the background worker to connect to a non-default database
ALTER SYSTEM SET flashback.database_name = 'myapp';
-- Apply without restart
SELECT pg_reload_conf();| Scenario | Behavior |
|---|---|
| Superuser drops a table | Captured; superuser can restore, purge, or list it |
| Regular user drops a table | Captured; only that user (or a superuser) can restore or purge it |
Regular user calls flashback_list_recycled_tables() |
Returns only their own dropped tables |
| Regular user tries to restore another user's table | Returns false with a WARNING |
All public functions are SECURITY DEFINER so that regular users can access the flashback schema without being granted direct schema privileges.
pg_flashback intercepts TRUNCATE TABLE: before the truncation runs, the current data is copied into a backup table in flashback_recycle. The actual TRUNCATE executes normally, leaving the table empty.
TRUNCATE TABLE orders;
SELECT * FROM flashback_list_recycled_tables(); -- backup entry visible
SELECT flashback_restore('orders', NULL); -- data restored
SELECT * FROM orders; -- rows are backThe following DROP TABLE commands are silently ignored by pg_flashback (the table is dropped normally):
- Temporary tables (
CREATE TEMP TABLE ... DROP TABLE ...) - Tables in the
flashbackorflashback_recycleschemas - Tables in schemas matching
pg_temp* - Tables in schemas listed in
flashback.excluded_schemas
pg_flashback creates two schemas on install:
flashback— contains theoperationsmetadata table and all public SQL functionsflashback_recycle— physical storage for dropped/truncated tables, named<original_name>_<op_id>
flashback_purgeremoves only the most recently dropped version; useflashback_purge_by_idto remove a specific version.- TRUNCATE on large tables: the backup is a full data copy (
CREATE TABLE ... AS SELECT *). On very large tables this will consume additional disk space equal to the original table size and may take noticeable time. For tables in that size range consider excluding the schema viaflashback.excluded_schemas. - TRUNCATE restore constraints: restoring a TRUNCATE entry requires the target table to still exist, and it fails if the table is currently referenced by incoming foreign keys from other tables.
- When a partitioned table's child partitions are missing after restore (edge case: child was in a different schema that was also dropped), pg_flashback recreates the partition shell but the original data is gone — a
WARNINGis emitted. - Materialized views dependent on a dropped table are dropped along with it (on
CASCADE) and recreated from captured definitions on restore. Mview-specific runtime state (for example last refresh timing/history) is not preserved.
For operation-ID based flows, both names are available and equivalent:
flashback_restore_by_idandflashback_restore_by_op_idflashback_purge_by_idandflashback_purge_by_op_id