PostgreSQL-native auth, permissions, versioned config, usage tracking, and job queues. Pure SQL functions - works with any language or driver.
# 1. Clone and build
git clone https://github.com/varunchopra/postkit.git
cd postkit && make build
# 2. Install on your database (PostgreSQL 14+)
psql $DATABASE_URL -f dist/postkit.sql # the eight extension-free modules
psql $DATABASE_URL -f dist/memory.sql # agent memory (separate; requires pgvector)
# Or individual:
psql $DATABASE_URL -f dist/authn.sql # users, sessions, tokens
psql $DATABASE_URL -f dist/authz.sql # permissions
psql $DATABASE_URL -f dist/config.sql # versioned config
psql $DATABASE_URL -f dist/lease.sql # leases, fencing, leader election
psql $DATABASE_URL -f dist/meter.sql # usage metering
psql $DATABASE_URL -f dist/outbox.sql # transactional event feed
psql $DATABASE_URL -f dist/presence.sql # heartbeat liveness
psql $DATABASE_URL -f dist/queue.sql # job queuesFor the optional Python SDK: pip install postkit
Postkit uses Row-Level Security for tenant isolation. Every query runs in the context of a namespace set via set_tenant inside a transaction. Context is transaction-local (SET LOCAL) - it clears on commit. Without set_tenant, queries return nothing (fail-closed).
Functions that write data need the p_namespace parameter to match the tenant context. set_tenant() controls RLS visibility; p_namespace controls what gets stored.
Each module has set_tenant / clear_tenant. Call clear_tenant() before returning connections to a pool. The Python SDK handles tenant context automatically - pass namespace to the constructor.
Maintenance functions accept p_namespace => NULL meaning all namespaces, but only for roles that bypass RLS (superuser or BYPASSRLS); other roles get a loud insufficient_privilege error and must iterate namespaces explicitly. Each module also has assert_rls_active(), which raises when the current role would bypass RLS - call it from CI setup so a test suite connecting as a superuser does not silently skip the tenancy model.
Postkit stores hashes, never plaintext. This applies in every language.
- Passwords: Hash with argon2 or bcrypt. The parameter is named
p_password_hash- seecreate_user. PassNULLfor SSO-only users. - Session tokens, API keys, refresh tokens: SHA-256. Generate a random token, hash it, store the hash in Postkit, send the raw token to the client - see
create_session.
Common operations by module. All require tenant context - call {module}.set_tenant(namespace) in a transaction first (see Multi-Tenancy). Queue takes namespace as first parameter; other modules default to 'default'. Each function's docs include full signatures, parameters, and usage examples - read them before integrating.
Auth: create_user · create_session · validate_session · revoke_session - full reference
Login flow: is_locked_out → get_credentials → verify hash + check disabled_at → record_login_attempt → create_session
Permissions: write_tuple · check · add_hierarchy - full reference
Config: set · get · rollback - full reference
Metering: allocate · reserve · commit - full reference
Leases: acquire · renew · verify · release - full reference
Memory: record · recall · consolidate · supersede - full reference
Events: emit · subscribe · poll · ack - full reference
Liveness: register · heartbeat · sweep · deregister - full reference
Queues: push · pull · ack - full reference
The SDK wraps every SQL function 1:1 with type hints, automatic tenant context, error mapping, and dict returns. Requires psycopg3 with the default tuple row_factory. Each function's docs include full signatures, parameters, and usage examples - read the relevant module docs before integrating.
Setup:
import psycopg
from postkit.authn import AuthnClient
from postkit.authz import AuthzClient
from postkit.config import ConfigClient
from postkit.lease import LeaseClient
from postkit.memory import MemoryClient
from postkit.meter import MeterClient
from postkit.outbox import OutboxClient
from postkit.presence import PresenceClient
from postkit.queue import QueueClient
conn = psycopg.connect("postgresql://localhost/myapp")
cursor = conn.cursor() # default tuple factory - do NOT use dict_row
authn = AuthnClient(cursor, namespace="my-app")
authz = AuthzClient(cursor, namespace="my-app")
config = ConfigClient(cursor, namespace="my-app")
lease = LeaseClient(cursor, namespace="my-app")
memory = MemoryClient(cursor, namespace="my-app")
meter = MeterClient(cursor, namespace="my-app")
outbox = OutboxClient(cursor, namespace="my-app")
presence = PresenceClient(cursor, namespace="my-app")
queue = QueueClient(cursor, namespace="my-app")Auth: create_user · create_session · validate_session · revoke_session - full reference
Login flow: is_locked_out → get_credentials → verify hash + check disabled_at → record_login_attempt → create_session
Permissions: grant · check · set_hierarchy - full reference
Config: set · get · rollback - full reference
Metering: allocate · reserve · commit - full reference
Leases: acquire · renew · verify · release - full reference
Memory: record · recall · consolidate · supersede - full reference
Events: emit · subscribe · poll · ack - full reference
Liveness: register · heartbeat · sweep · deregister - full reference
Queues: push · pull · ack - full reference
SDK-specific gotchas:
psycopg>=3.1.0required (not psycopg2) -import psycopg- Default
conn.cursor()only -row_factory=dict_rowraisesValueError - Namespace is a required constructor argument
- Call
client.clear_actor()/authz.clear_viewer()before returning pooled connections
Module independence. No foreign keys between modules. Disabling a user in authn does NOT revoke their authz grants. The application coordinates cleanup. Install only what you need.
Permission hierarchy. add_hierarchy defines that one permission implies another (e.g., admin implies write). Without hierarchy rules, only direct grants match. The Python SDK has a convenience wrapper: set_hierarchy.
Audit context. set_actor tags write operations with who made the change and why. Optional but recommended.
Outbox delivery is database-global. Outbox reads return only events whose transaction has finished, and that horizon spans the whole database: one tenant's long-open transaction delays event delivery for every tenant. Set idle_in_transaction_session_timeout in multi-tenant deployments and watch the horizon column of lag; when it stalls, outbox.horizon_blockers() names the sessions responsible.
| Module | Schema | SQL Reference | Python SDK | Purpose |
|---|---|---|---|---|
| authn | authn |
sql.md | sdk.md | Users, sessions, tokens, MFA, impersonation |
| authz | authz |
sql.md | sdk.md | ReBAC permissions, hierarchies, cross-tenant sharing |
| config | config |
sql.md | sdk.md | Versioned key-value, JSON schema validation |
| lease | lease |
sql.md | sdk.md | TTL leases, fencing tokens, leader election |
| memory | memory |
sql.md | sdk.md | Agent memory: episodes, distilled facts, recall (pgvector) |
| meter | meter |
sql.md | sdk.md | Usage tracking, reservations, billing periods |
| outbox | outbox |
sql.md | sdk.md | Transactional event feed, fan-out, durable cursors |
| presence | presence |
sql.md | sdk.md | Heartbeat liveness, edge detection, alert hooks |
| queue | queue |
sql.md | sdk.md | Job queues, scheduling, retries, dead letters |
Each module's README has a function index with deep links. For usage examples: sdk/tests/{module}/.
{module}/src/functions/ # SQL function source
sdk/src/postkit/
{module}/client.py # Python SDK client (optional)
sdk/tests/ # Usage examples
dist/
postkit.sql # Combined SQL (the eight extension-free modules; excludes memory)
{module}.sql # Individual module SQL
API docs are auto-generated. Run make docs after changing function signatures or docstrings. Don't edit files in docs/ directly.
SQL documentation tags:
-- @function authz.check
-- @brief Check if subject has permission on resource
-- @param p_subject_type Subject type ('user', 'api_key')
-- @returns True if permitted
-- @example SELECT authz.check('user', 'alice', 'read', 'doc', '1');Section banners (-- ====) belong in schema files only. Function files carry a -- @group header plus the documentation tags above; anything a banner would say goes in the doc comment prose.
The SQL surface is the contract, and the postkit:module:CODE namespace belongs to it exclusively: every such code must be raised by SQL, and sdk/tests/test_error_codes.py enforces that the SDK constants mirror it exactly. SDK clients may add client-side checks where they help (config validates values against stored schemas, lease and outbox refuse calls that need an open transaction), but such checks raise a typed module exception with a plain message - they never mint a postkit: code, because codes the database cannot raise falsify the contract for raw SQL callers and other-language SDKs.
The changelog lives in GitHub Releases; there is no CHANGELOG.md. /release writes the release notes from the commits since the previous tag and puts them in the annotated tag message, and the release workflow publishes that message as the GitHub Release body. The notes end with the compare link to the previous tag. Each entry is one line: a single plain sentence that says what changed, ending with the short hash of the commit that made it, which GitHub links automatically. Group entries under breaking, added, and fixed headings as applicable, breaking first.
Adding a module means registering it everywhere a module list is hardcoded: scripts/build.sh (build loop, case arm, usage string), Makefile (dist target), scripts/gendocs/cli.py (extraction block), README.md (install line, module table), this file (install line, SQL quick reference, SDK example, module table), sdk/src/postkit/errors.py (error-code class), and sdk/tests/test_error_codes.py (MODULE_CONFIG). docs/ regenerates via make docs.
Development: make setup (Postgres in Docker), make build, make test, make lint, make format.
If you encounter unclear documentation, missing examples, incorrect signatures, or unexpected behavior while integrating Postkit, create or append to a POSTKIT_ISSUES.md file in your project root:
## [date] Brief description
- **Category:** docs | implementation | sdk | sql
- **File/function:** e.g., `AGENTS.md` line 32, `authz.add_hierarchy`
- **What happened:** What you were trying to do and what went wrong
- **Suggested fix:** If you have one
This gives the development team a record to review and file upstream at https://github.com/varunchopra/postkit/issues.