pgmorbacMulti-OrBAC for PostgreSQL
    DocumentationDownloadSource
    Use cases
    • How do I...?
    Getting Started
    • Introduction
    • Quick start
    • How pgmorbac compares
    • Upgrade
    Concepts
    • Organizations
    • Roles
    • Activities
    • Views
    • Contexts
    • Rules & Modalities
    • Global Rules
    • System Principals
    • Constraints
    • Row-Level Security
    • Decision Cache
    Resolution
    • is_allowed
    Recipes
    • Org Hierarchies
    • Multi-Tenant Isolation
    • Wildcard Admin
    • Delegation
    • Service Accounts
    • Deny Rules
    • Separation of Duty
    • Derived Roles
    • Cross-Org Access
    • Obligations & Recommendations
    • Temporal Access
    Integration
    • Fastify
    • PostgREST & RLS Context
    • SQL Only
    Reference
    • Function Reference
    Integration

    Fastify

    The @crudy/pgmorbac plugin: hasPermission, org scopes, management routes.

    The @crudy/pgmorbac npm package wraps the SQL engine for Node applications: permission helpers for Fastify routes, an RLS session-context helper, and ready-made management routes.

    npm install @crudy/pgmorbac --registry https://npm.villains.fr

    fastify and pg are optional peer dependencies: install them if you use the Fastify or Pool-based helpers.

    Authorizing a route

    hasPermission resolves the acting user from the request, runs morbac.is_allowed, and sends the error response itself. It returns a boolean so the handler can bail out in one line:

    import { hasPermission } from '@crudy/pgmorbac/permissions';
     
    app.post('/prescriptions', async (req, reply) => {
        const orgId = req.headers['x-org-id'] as string;
        if (!await hasPermission(db, req, reply, orgId, 'prescribe', 'prescriptions')) {
            return reply;                       // 401 or 403 already sent
        }
        // ... create the prescription for Clinique Valmont Nord
    });

    A null org means a check with no organization: only global rules can satisfy it.

    The same call also enforces API-key scopes: when the request was authenticated by a scoped key, an activity/view pair outside the scope is rejected before the database is consulted.

    Request helpers

    ExportPurpose
    getUser(req)acting user id, or null
    getOrgScope(req, reply, required)validated org ids from the request
    orgScopeExpr(orgIds, column)SQL fragment + params to filter by those orgs
    isGlobalAdmin(db, userId)wildcard global rule holder
    requireGlobalAdmin(db, req, reply)guard that answers 403 itself
    canActOnUser(...), canGrantRule(...)guards for user and rule administration
    SYSTEM_ORG_IDsentinel meaning "no org filter", never a real org

    SYSTEM_ORG_ID is not a row in morbac.orgs. It is a header value meaning the caller is asking without an org filter, plus a write guard; never store records against it.

    Session context for RLS

    Route handlers that read tables protected by row-level security must tell the database who is acting. withMorbacContext opens a transaction on a dedicated client, sets morbac.user_id and morbac.org_id as transaction-local settings, and rolls back on error:

    import { withMorbacContext } from '@crudy/pgmorbac';
     
    const rows = await withMorbacContext(pool, { userId: user.id, orgId }, async (client) => {
        const { rows } = await client.query('SELECT * FROM app.prescriptions');
        return rows;                            // RLS already filtered them
    });

    Transaction-local settings are the safe choice behind a connection pool: the next borrower of the connection never inherits them.

    If your runtime role has BYPASSRLS, policies do not apply and this helper is only bookkeeping. Connect with a role without BYPASSRLS to make RLS a real second gate behind hasPermission.

    Management routes

    createMgmtRoutes registers CRUD endpoints for orgs, roles, assignments and rules, each already gated by the corresponding morbac permission:

    import { createMgmtRoutes } from '@crudy/pgmorbac/plugin';
     
    app.register(createMgmtRoutes({
        db,
        checkLimit: async (kind, orgId) => { /* your quota policy */ },
        onOrgEvent:  (event) => auditLog(event),
        onRoleEvent: (event) => auditLog(event),
    }));

    The event sinks let you mirror organization and role changes into your own audit or search infrastructure without patching the plugin.

    Layering

    Authentication proves who is calling. hasPermission decides what they may do. RLS enforces it per row even if a handler forgets. Use all three: the database is the last line that cannot be bypassed by an application bug.

    PreviousTemporal AccessNext PostgREST & RLS Context
    pgmorbac - Multi-OrBAC permission engine for PostgreSQL
    DocumentationDownloadGitea