SQL Only
Using pgmorbac from any stack with plain SQL calls.
pgmorbac has no client library requirement. Any language that can run SQL can use it: Python, Go, Rust, Java, PHP, or psql in a shell script. This page is the whole contract.
The three calls you need
-- 1. authorize an action
SELECT morbac.is_allowed($1::uuid, $2::uuid, $3, $4);
-- 2. set the session context (per request or transaction)
SELECT set_config('morbac.user_id', $1, true),
set_config('morbac.org_id', $2, true);
-- 3. inspect the effective roles (for UIs and debugging)
SELECT r.name, cr.source
FROM morbac.get_comprehensive_roles($1::uuid, $2::uuid) cr
JOIN morbac.roles r ON r.id = cr.role_id;Everything else is administration: inserts into morbac.orgs, morbac.roles,
morbac.user_roles and the rule stores.
A request, end to end
Pseudocode that applies in any stack:
tx = begin()
tx.exec("SELECT set_config('morbac.user_id',$1,true), set_config('morbac.org_id',$2,true)",
userId, orgId)
if not tx.query_one("SELECT morbac.is_allowed($1,$2,'prescribe','prescriptions')", userId, orgId):
return 403
tx.exec("INSERT INTO app.prescriptions (org_id, patient, drug) VALUES ($1,$2,$3)",
orgId, patient, drug)
tx.commit()Two independent gates, both in the database: the explicit is_allowed check
returns a clean 403, and the RLS policy on app.prescriptions refuses the write
anyway if the check were ever skipped.
Connection pooling
Use transaction-local settings (set_config(..., true)) so a pooled
connection never leaks context between requests. If your pooler runs in
statement mode, put the set_config and the query in one round trip.
BEGIN;
SELECT set_config('morbac.user_id', $1, true), set_config('morbac.org_id', $2, true);
SELECT * FROM app.prescriptions;
COMMIT;Administration snippets
-- create an org under an existing one
INSERT INTO morbac.orgs (name, parent_id)
VALUES ('Cardiologie Valmont Nord',
(SELECT id FROM morbac.orgs WHERE name = 'Clinique Valmont Nord'));
-- create a role and grant it a capability over the subtree
INSERT INTO morbac.roles (org_id, name) VALUES (:org, 'physician');
INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality, scope)
VALUES (:org, :role, 'read', 'patients',
(SELECT id FROM morbac.contexts WHERE name = 'always'), 'permission', 'subtree');
-- assign, with SoD and cardinality enforced
SELECT morbac.assign_role(:user, :role, :org);
SELECT morbac.revoke_role(:user, :role, :org);Operational queries
SELECT morbac.get_config('schema_version');
SELECT morbac.get_config('cache_ttl_seconds');
SELECT morbac.invalidate_cache(:user, :org);
SELECT morbac.is_allowed_nocache(:user, :org, 'read', 'patients');
SELECT morbac.enable_audit('rules');Error handling
Constraint functions raise exceptions rather than returning false:
assign_role on a separation-of-duty conflict, a cardinality overflow, or a
protected system principal all raise. Catch
the SQLSTATE in your driver and map it to a 409 or 422; is_allowed itself
never raises, it returns a boolean.