Use PostgreSQL event triggers only in a disposable admin database to observe and guard DDL, understand event coverage and superuser requirements, and rehearse safe disable/recovery procedures.

Event Triggers, DDL Governance, Policy Enforcement, and Deployment Concerns

Use PostgreSQL event triggers only in a disposable admin database to observe and guard DDL, understand event coverage and superuser requirements, and rehearse safe disable/recovery procedures.

Intermediate → Advanced180–240 minutesServer-side programming and deployment safetyPostgreSQL 18.6 baselineCore SQL + built-in PL/pgSQL; no third-party extension requiredServiceHub disposable objects: app.ch18_*Lesson 4 uses a disposable admin database and superuser-equivalent local lab accountLocal/free tooling; psql recommendedLast reviewed: August 18, 2026

Learning outcomes

Table triggers react to DML on one relation. Event triggers are different: they are database-wide hooks around supported DDL events. That makes them powerful for auditing and governance and dangerous enough that PostgreSQL permits only superusers to create them. A broken policy can block migrations—or, for login event triggers, even make normal connections unusable.

01

Create event-trigger functions and distinguish ddl_command_start, ddl_command_end, sql_drop, table_rewrite, and login events.

02

Capture DDL metadata with pg_event_trigger_ddl_commands() and dropped-object metadata with pg_event_trigger_dropped_objects().

03

Explain which shared-object commands are outside normal DDL event-trigger coverage.

04

Demonstrate an over-broad migration guard and a safe ALTER EVENT TRIGGER ... DISABLE recovery path.

05

Know the emergency event_triggers=off / single-user recovery boundary for a database made inaccessible by event-trigger code.

Safety boundary

Run every command in this lesson only in a disposable local admin database. Event triggers are database-wide, only superusers can create them, and a mistake can block DDL or login. Do not install the lab triggers into the normal ServiceHub database.

1. Create and enter a disposable admin database

psql · create the isolated database
CREATE DATABASE servicehub_ch18_event_lab;\c servicehub_ch18_event_labSELECT current_database(), current_user,       rolsuperFROM pg_rolesWHERE rolname = current_user;

The final rolsuper value must be true for this lab because PostgreSQL 18 restricts CREATE EVENT TRIGGER to superusers. If you do not have a disposable local superuser account, read the lesson and skip the live event-trigger creation rather than weakening a production role.

2. ddl_command_end can observe completed DDL before transaction commit

sql · DDL audit storage and event-trigger function
CREATE SCHEMA ch18_admin;CREATE TABLE ch18_admin.ddl_audit (  audit_id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,  event_name text NOT NULL,  command_tag text NOT NULL,  object_type text,  object_identity text,  schema_name text,  actor name NOT NULL,  recorded_at timestamptz NOT NULL DEFAULT clock_timestamp());CREATE OR REPLACE FUNCTION ch18_admin.capture_ddl_end()RETURNS event_triggerLANGUAGE plpgsqlAS $$BEGIN  INSERT INTO ch18_admin.ddl_audit(    event_name, command_tag, object_type, object_identity, schema_name, actor  )  SELECT TG_EVENT,         command_tag,         object_type,         object_identity,         schema_name,         session_user  FROM pg_event_trigger_ddl_commands();END$$;CREATE EVENT TRIGGER ch18_capture_ddl_endON ddl_command_endEXECUTE FUNCTION ch18_admin.capture_ddl_end();
sql · generate and inspect DDL audit evidence
CREATE TABLE public.ch18_event_demo(id bigint PRIMARY KEY, note text);ALTER TABLE public.ch18_event_demo ADD COLUMN created_at timestamptz;SELECT event_name, command_tag, object_type, object_identity, schema_name, actorFROM ch18_admin.ddl_auditORDER BY audit_id;

ddl_command_end fires after the command's catalog changes are visible but before the containing transaction commits. If the event trigger itself raises an error, PostgreSQL rolls back the DDL with the transaction.

3. sql_drop exposes objects removed by one DDL action

sql · capture dropped objects
CREATE TABLE ch18_admin.drop_audit (  audit_id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,  object_type text NOT NULL,  object_identity text NOT NULL,  original boolean NOT NULL,  normal boolean NOT NULL,  actor name NOT NULL,  recorded_at timestamptz NOT NULL DEFAULT clock_timestamp());CREATE OR REPLACE FUNCTION ch18_admin.capture_drop()RETURNS event_triggerLANGUAGE plpgsqlAS $$BEGIN  INSERT INTO ch18_admin.drop_audit(    object_type, object_identity, original, normal, actor  )  SELECT object_type, object_identity, original, normal, session_user  FROM pg_event_trigger_dropped_objects();END$$;CREATE EVENT TRIGGER ch18_capture_dropON sql_dropEXECUTE FUNCTION ch18_admin.capture_drop();DROP TABLE public.ch18_event_demo;SELECT * FROM ch18_admin.drop_audit ORDER BY audit_id;

A single DROP ... CASCADE or dependency cleanup can report multiple dropped objects. original distinguishes directly named objects from secondary drops. This is governance evidence, not a restore mechanism.

4. Event coverage is intentionally not “every admin command”

PostgreSQL 18 supports event triggers for login, ddl_command_start, ddl_command_end, sql_drop, and table_rewrite. The DDL command start/end events cover many CREATE/ALTER/DROP/COMMENT/GRANT/REINDEX-style commands, but not commands on certain shared objects such as databases, roles, tablespaces, parameter privileges, or ALTER SYSTEM. Commands targeting event triggers themselves are also excluded, which is important for recovery.

sql · inventory installed event triggers
SELECT evtname,       evtevent,       evtenabled,       evttags,       evtfoid::regprocedure AS functionFROM pg_event_triggerORDER BY evtname;

Do not infer governance coverage from “we installed a DDL event trigger.” Maintain an explicit command/event coverage table for your deployment system.

5. Deliberately overzealous policy: block every ALTER TABLE

sql · bad migration guard for demonstration
CREATE OR REPLACE FUNCTION ch18_admin.block_alter_table()RETURNS event_triggerLANGUAGE plpgsqlAS $$BEGIN  RAISE EXCEPTION USING    ERRCODE = 'P1805',    MESSAGE = format('DDL tag %s is blocked by ch18 lab policy', TG_TAG),    HINT = 'Disable the lab guard before approved migrations.';END$$;CREATE EVENT TRIGGER ch18_block_alter_tableON ddl_command_startWHEN TAG IN ('ALTER TABLE')EXECUTE FUNCTION ch18_admin.block_alter_table();CREATE TABLE public.ch18_migration_target(id bigint PRIMARY KEY);ALTER TABLE public.ch18_migration_target ADD COLUMN note text;-- Expected: P1805; ALTER TABLE does not execute.

This proves a governance hook can become an outage generator. A blanket policy has no context about migration approval, object ownership, deployment identity, or maintenance windows.

6. Safe recovery: commands targeting event triggers remain available

sql · disable, perform approved migration, re-enable
ALTER EVENT TRIGGER ch18_block_alter_table DISABLE;ALTER TABLE public.ch18_migration_target ADD COLUMN note text;ALTER EVENT TRIGGER ch18_block_alter_table ENABLE;SELECT evtname, evtenabledFROM pg_event_triggerWHERE evtname = 'ch18_block_alter_table';

Commands that alter/drop event triggers do not fire normal DDL event triggers, so a broken DDL guard can usually be disabled by an authorized superuser. Build this recovery command into the deployment runbook before enabling governance.

7. Login event triggers have a stronger failure mode

A login event trigger runs after authentication when a connection enters the database. A bug can prevent successful logins. PostgreSQL documents two emergency bypasses: start/connect with the event_triggers setting disabled, or use single-user mode, where event triggers are disabled. Login triggers also fire on standby servers, so code must not attempt writes there.

Do not live-test a broken login trigger casually

The lesson does not create a failing login trigger. The operational lesson is the recovery path: know how to disable event triggers before introducing login-time policy code. Keep login handlers short, read-only on standbys, and independently tested.

8. Event triggers are transactional policy code

sql · prove DDL audit rows roll back with the DDL transaction
BEGIN;CREATE TABLE public.ch18_rollback_demo(id integer);SELECT count(*) AS audit_rows_inside_transactionFROM ch18_admin.ddl_auditWHERE object_identity LIKE '%ch18_rollback_demo%';ROLLBACK;SELECT to_regclass('public.ch18_rollback_demo') AS relation_after_rollback;SELECT count(*) AS audit_rows_after_rollbackFROM ch18_admin.ddl_auditWHERE object_identity LIKE '%ch18_rollback_demo%';

The event-trigger audit row participates in the same transaction. That is useful for atomic governance but means the audit table is not an immutable external trail of rolled-back attempts. If rejected-attempt telemetry matters, capture it in server logs or an external deployment/control system as well.

9. Cleanup the disposable database

sql · remove lab event triggers before leaving the database
DROP EVENT TRIGGER IF EXISTS ch18_block_alter_table;DROP EVENT TRIGGER IF EXISTS ch18_capture_drop;DROP EVENT TRIGGER IF EXISTS ch18_capture_ddl_end;DROP SCHEMA ch18_admin CASCADE;DROP TABLE IF EXISTS public.ch18_migration_target;-- Reconnect to an admin database before dropping this disposable database.
psql · leave and drop disposable database
\c postgresDROP DATABASE servicehub_ch18_event_lab WITH (FORCE);
Production judgment

Use event triggers for narrow, well-tested database-local governance or observation. Keep authoritative deployment policy in a system that can be reviewed and recovered even when the target database is unhealthy. Never install an event trigger without a disable/bypass procedure.

10. Checkpoint

Check your understanding

  1. Who can create an event trigger in PostgreSQL 18?
  2. What does pg_event_trigger_ddl_commands() expose, and at which event?
  3. Why did the blanket ALTER TABLE guard break a legitimate migration?
  4. Why can ALTER EVENT TRIGGER DISABLE recover from many bad DDL guards?
  5. What emergency mechanism exists if a bad login event trigger prevents normal connections?
Review the answers

Only superusers can create event triggers. pg_event_trigger_ddl_commands() is used from ddl_command_end to inspect completed base DDL commands before commit. The guard lacked contextual authorization and blocked all ALTER TABLE. Commands targeting event triggers themselves are excluded from normal event-trigger firing, so an authorized user can disable them. PostgreSQL documents event_triggers=false or single-user mode as emergency bypasses for broken login hooks.

Authoritative references

Routines, trigger timing, privileges, and planner promises are version-sensitive. These PostgreSQL 18 primary sources define the behavior used in this lesson.

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only assets compatible with the Ethereum/ERC-20 network. Do not send TRC-20/TRON assets.