Skip to main content

audit-framework-postgres

The PostgreSQL adapters for audit-framework, three implementations sharing one database:

Adapter Port Role
PostgresAuditStore AuditStore append-only, queryable system of record for the audit log (vs. the best-effort ExternalSink fan-out)
PostgresNotificationStore NotificationStore the mutable notification delivery lifecycle (pending → delivered → read / failed)
PgListenNotifyBus EventBus cross-context bus on LISTEN/NOTIFY (e.g. audit → case-management escalation.requested) without Redis/NATS

Ships the audit_log schema (compact LISTEN/NOTIFY trigger + append-only guards) and the notifications schema.

Install

pip install audit-framework-postgres            # bring your own executor
pip install audit-framework-postgres[asyncpg]   # + asyncpg for a real pool

Use

import asyncpg
from audit_framework_postgres import PostgresAuditStore, apply_schema
from audit_framework.core.middlewares.store import StoreMiddleware

pool = await asyncpg.create_pool(dsn, min_size=5, max_size=20)
await apply_schema(pool, app_role="app_user")     # create table + triggers (run once, as a privileged role)

store = PostgresAuditStore(pool)                  # the asyncpg pool *is* the executor
pipeline.use(StoreMiddleware(store))              # now events are persisted authoritatively

Query it back (this is what backs an admin audit-log view — sinks can't do this):

await store.query({"actor_id": "alice", "action": "DELETE", "from": "2026-06-01", "to": "2026-07-01"},
                  offset=0, limit=50)
await store.get_by_resource("contract", "c-42")

Notifications + event bus

from audit_framework_postgres import (
    PostgresNotificationStore, PgListenNotifyBus, apply_notifications_schema,
)

await apply_notifications_schema(pool)            # create the notifications table (run once)
notifications = PostgresNotificationStore(pool)   # mutable delivery lifecycle
await notifications.get_unread("alice")           # initial UI load (not read, not failed)

# The bus needs a *dedicated* asyncpg connection (LISTEN holds it open), not the pool:
conn = await asyncpg.connect(dsn)
bus = PgListenNotifyBus(conn, max_queue=1000)     # per-subscription buffer (bounded)
sub = await bus.subscribe("escalation.requested")
msg = await sub.receive(timeout=30)               # None on timeout; raises once closed
# sub.dropped counts messages evicted when a slow consumer overflows the buffer

publish() raises NotifyPayloadTooLargeError (before the DB call) if the serialised payload would reach Postgres's NOTIFY size limit — the backend rejects payloads of 8000 bytes or more, so keep bus payloads to identifiers/ids and let the subscriber fetch the full row by id (the same trade the audit_log NOTIFY trigger makes). Each subscription's buffer is bounded (max_queue ≥ 1; default 1000); when a slow consumer overflows it the oldest message is evicted, logged once, and counted in sub.dropped rather than growing without limit.

All three register under the postgres provider name via the audit_framework.plugins entry point (audit_store, notification_store, event_bus), so they're discoverable through the registry.

No hard driver dependency

All DB access goes through an injected Executor — anything exposing fetchval / fetch / execute (an asyncpg pool or connection satisfies this structurally). So the SQL logic is fully unit-testable without a database (50 stdlib-only tests use a fake executor/connection), and you control pooling. The PgListenNotifyBus likewise depends only on a ListenConnection (execute / add_listener / remove_listener), satisfied structurally by a dedicated asyncpg connection.

Schema design (validated against PostgreSQL guidance)

schema_sql() / apply_schema() emit:

  • an append-only audit_log table — REVOKE UPDATE, DELETE from the app role and a BEFORE UPDATE OR DELETE guard trigger that raises, so the log is immutable even for the table owner (only a superuser can bypass it). Run the app under an unprivileged, non-owning role that can only INSERT/SELECT;
  • a compact AFTER INSERT pg_notify trigger on channel audit_events that sends identifiers only — never the unbounded changes diff — so every payload stays well under PostgreSQL's hard 8000-byte NOTIFY limit. A LISTEN consumer fetches the full row by id when it needs detail (this is also the upgrade path to the multi-worker event bus, AD-6);
  • request_id / ip_address as TEXT (not UUID/INET), because the event model treats them as free-form strings and redaction can replace ip_address with a mask.

Tamper-evidence: REVOKE + guards make the log append-only, but a superuser can still rewrite history. For tamper-detection, add per-row hash chaining (prev_hash/hash) on top — a natural follow-up.

notifications_schema_sql() / apply_notifications_schema() emit the notifications table. It is intentionally not append-only — a notification has a mutable lifecycle (pending → delivered → read / failed), so there is no REVOKE/guard trigger. created_at / delivered_at are TEXT (not TIMESTAMPTZ) so the store round-trips the event model's free-form ISO-8601 strings without a custom asyncpg timestamptz codec; ISO-8601 UTC strings sort chronologically for the inbox (newest-first) and pending-worker (oldest-first) read paths.

SQL-injection posture

The table name is validated against a strict identifier whitelist at construction; every value is a bound $n parameter; query filters are mapped through a fixed column allow-list (unknown keys raise ValueError). The only interpolation is the validated table name (Postgres can't bind an identifier as a parameter).

The whitelist accepts a bare or schema-qualified name (audit_log or reporting.audit_log) — exactly the set the schema_sql() / notifications_schema_sql() DDL generators also accept, so any table a store can target is one the package can also create. For a schema-qualified table the generated DDL is self-contained: it emits a leading CREATE SCHEMA IF NOT EXISTS, references the table qualified, derives the index/trigger names from the bare table part (Postgres forbids schema-qualifying those), and places the trigger functions in the table's schema.

Development

pip install -e ".[dev]"
pytest        # 50 stdlib-only tests (fake executor/connection; no database needed)

A live-DB integration test (real asyncpg + a throwaway Postgres) is the recommended next layer; the unit suite already pins the generated SQL, the parameter binding, the append-only DDL, and the compact-notify payload.

License

MIT

For AI agents & coding assistants

This package ships its agent guide — AGENTS.md — inside the wheel (installed at <site-packages>/audit_framework_postgres/AGENTS.md). Read it offline, with no docs site and no network, even from an airgapped Nexus PyPI mirror:

python -m audit_framework_postgres
python -c "import audit_framework_postgres; print(audit_framework_postgres.overview())"

AGENTS.md is the single source of truth: mental model, one runnable quickstart, and the exact public API. audit_framework_postgres.overview() returns it, and tests/guide_test.py compiles its examples so the guide can't drift from the code.

Metadata

Release files for audit-framework-postgres 0.1.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for audit-framework-postgres 0.1.2
File Size Uploaded Provenance
audit_framework_postgres-0.1.2.tar.gz 23.8 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for audit-framework-postgres 0.1.2
File Interpreter ABI Platform
audit_framework_postgres-0.1.2-py3-none-any.whl Python 3 none any Details

Total release size: 46.3 kB

Release files / audit_framework_postgres-0.1.2.tar.gz

Download URL audit_framework_postgres-0.1.2.tar.gz
Size 23.8 kB
Tags Source
SHA-256 checksum
How to use checksums
fe3f69e3e3eb122e73cc4e3bbc548f6e219e8130cb2bb0bcfcc7ef2301e7d821
BLAKE2b-256 checksum
How to use checksums
84b67956d63c536d682acde24db23ea7302f190f65dbf78453884b0bd32c8269
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 3, 2026.

Transparency log

Release files / audit_framework_postgres-0.1.2-py3-none-any.whl

Download URL audit_framework_postgres-0.1.2-py3-none-any.whl
Size 22.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c7a3523e147aa350453ece5a1df10a72d8c503761a55363df0358bde2d99a438
BLAKE2b-256 checksum
How to use checksums
caa45465c24b68295c2c1be90fe53a7fc77861c3019b2760e0ee445962adfcc5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 3, 2026.

Transparency log

Security for audit-framework-postgres 0.1.2

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for audit_framework_postgres-0.1.2.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 3, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page