Outline
- Learning objectives — RLS, audit, RBAC, bypass tracking, SOC2
- Key concept — compliance-first by design; runtime governance (NOT dev-time configuration like CLAUDE.md which lives in module 1)
- Diagram walkthrough — layered stack (UI → API → service → DB) + cross-cutting concerns (audit, bypass, observability, alerting)
- Tenant isolation: how it actually works — Postgres RLS, fail-closed design, migration enforcement
- The 15-role hierarchy — system / workspace / operational / contributor roles + Zod single source of truth
- The audit log: what gets captured — event shape, append-only, retention, read access for compliance
- Bypass tracking — observe-don't-block; structured
policy_bypassevents - SOC2 alignment — Trust Service Criteria → PM33 implementation mapping
- Workflow narrative — security review answers in 3 minutes (deletion event audit)
- Workflow narrative — the bypass case — observation beats enforcement (real example)
- What this enables for buyers — 3 pitches (compliance review in 1 meeting, no shadow IT, DB-level tenant isolation)
- Further reading — CLAUDE.md §4, security invariants, roles SSOT, module 6
Learning objectives
After this module you should be able to:
- Explain how PM33 enforces tenant isolation
- Describe the audit trail and what it captures
- Walk through the 15-role hierarchy and how role-gates work
- Explain how bypass tracking turns ad-hoc overrides into observable signals
Key concept
PM33 was designed for multi-tenant, compliance-first deployment from day one. Every query is RLS-scoped to a tenant. Every action is an audit event. Every role transition is logged. Every policy bypass emits a structured signal.
For enterprises evaluating PM33, the question isn't "can we trust the AI agents?" It's "can we audit what they did and prove compliance?" The answer is yes — because the audit and policy infrastructure was built first, with the agents added on top.
Diagram walkthrough
A layered stack diagram, with a cross-cutting "audit + observability" column on the right.
Layers (bottom to top):
- Database — Postgres with RLS policies on every table. Tenant column on every row. Schema-parity validator runs in CI.
- Service layer — every service call carries
tenantContext.getDb()injects RLS context.BacklogQueryServiceis the only path to work-item queries. - API layer — Zod contracts at every boundary. Auth middleware injects userId + tenantId. Scope checks at every MCP tool entry.
- UI layer — role-based feature flags.
useCanRolehook gates components. No frontend bypass paths to backend authority.
Right column — cross-cutting:
- Audit log (every transition)
- Bypass tracking (every
--no-verify, everyPM33_ALLOW_*) - Observability (Pino structured logs, OpenTelemetry traces)
- Alerting (Slack ops alerts on critical events)
Top label: "Designed compliance-first, not compliance-bolted-on"
Tenant isolation: how it actually works
Every table in PM33's schema has a tenant_id column. Every row has a tenant. Postgres RLS (Row-Level Security) policies enforce that queries can only see rows for the current request's tenant. The tenant context is set per-request via:
SET LOCAL app.current_tenant_id = '<tenant-uuid>';
The getDb() helper sets this on every connection checkout. The NULLIF(current_setting('app.current_tenant_id', TRUE), '')::uuid pattern in policies catches the "tenant context wasn't set" case (returning NULL → no rows visible) — failing closed, not open.
This means: a bug in service code that forgets to filter by tenant still can't leak cross-tenant data, because RLS will refuse the query at the row level. The DB is the security boundary, not the application.
Migration enforcement: every CREATE TABLE migration MUST include ALTER TABLE ... ENABLE ROW LEVEL SECURITY + a tenant-isolation policy + GRANT ... TO pm33_app. A missing GRANT silently returns empty results — caught by the new-table checklist in .claude/examples/security-invariants.md.
The 15-role hierarchy
PM33's auth has 15 canonical roles, designed for enterprise org structures:
- System roles:
super_admin,owner,admin - Workspace roles:
workspace_admin,portfolio_admin,executive,manager,engineering_lead,strategic_reviewer - Operational roles:
security_officer,billing_admin,support_admin - Contributor roles:
member,viewer,user
These live as a CHECK constraint on user_memberships.role + user_roles.role. The RoleSchema Zod type is the single source of truth for role values. Frontend uses useCanRole hooks; backend uses parseRolesSafely() at trust boundaries.
Role-gates work like this: a route or MCP tool declares the required role. The middleware checks the request's user → role mapping. If insufficient, 403 returned. The role hierarchy is RBAC, not ABAC — simple to reason about, simple to audit.
The audit log: what gets captured
Every state-changing action in PM33 emits an audit event:
{
"event_type": "work_item_status_changed",
"event_id": "uuid",
"tenant_id": "uuid",
"workspace_id": "uuid",
"user_id": "uuid",
"correlation_id": "uuid",
"timestamp": "iso-8601",
"actor": { "type": "user", "id": "...", "email": "..." },
"subject": { "type": "work_item", "id": "..." },
"before": { "status": "in_progress" },
"after": { "status": "in_review" },
"metadata": {
"client": "claude-code",
"session_id": "...",
"ip_address": "..."
}
}
The audit log table is append-only (no UPDATE or DELETE granted to the app role). Retention is configurable per tenant (default: 7 years for SOC2/HIPAA-aligned workspaces).
Reading the audit log:
mcp__pm33-staging__pm33_query_audit_log \
filters='{"work_item_id": "<id>", "event_type": "status_changed"}' \
limit=50
Returns the chronological history of every transition for that work item. Compliance reviewers love this. Investigators love this. Engineers debugging "who marked this done?" love this.
Bypass tracking
Real-world software development has emergencies. Policy bypasses happen — git commit --no-verify when a pre-commit hook is broken, PM33_ALLOW_SHARED_INDEX=true when the per-agent index isn't activated, PM33_ALLOW_TREE_SHRINK=true for legitimate mass-deletions.
Most bypasses are legitimate. Blocking them all would grind work to a halt.
PM33's approach: observe, don't block. Every bypass emits a structured event:
{
"event_type": "policy_bypass",
"bypass_kind": "schema_drift_validator",
"actor": "...",
"justification": "docs-only change, schema validator requires local OrbStack",
"command": "git commit --no-verify",
"timestamp": "...",
"expires_at": null
}
These events:
- Show up in the PR comment when CI runs
- Aggregate on the compliance dashboard ("20% of PRs last sprint bypassed schema-drift")
- Feed into trend analysis ("schema-drift bypasses are growing — is the validator too strict?")
The signal is the value. Blocking is the failure mode. PM33 trusts the team while making behavior observable.
SOC2 alignment
PM33's compliance posture (claimed, ongoing audit):
| SOC2 Trust Service Criteria | How PM33 addresses |
|---|---|
| Security: access controls | 15-role RBAC + RLS at the DB |
| Security: encryption | TLS in transit, AES-256 at rest, JWT signing with key rotation |
| Availability: monitoring | Pino + OpenTelemetry, ops Slack alerts, status page |
| Confidentiality: data isolation | Per-tenant RLS, no cross-tenant fanout |
| Processing integrity: audit | Append-only audit log, 7-year default retention |
| Privacy: GDPR/CCPA | Data subject rights endpoints, retention purge cron, PII-redacted logs |
Specific compliance-facing features:
npm run db:backup:fullbefore any deployment touching production datapm33_query_audit_logMCP tool for auditor read access (read-only scope)data retention purgeRailway cron job (deletes per workspace retention policy)- PII-redacted structured logs (Pino redaction rules in
server/services/logging/StructuredLogger)
Workflow narrative
Acme Corp's security review asks: "Can you prove that User X's deletion of Work Item Y was authorized and recorded?"
Answer in 3 minutes:
# 1. Find the deletion event
mcp__pm33-staging__pm33_query_audit_log \
filters='{"subject_id": "<work-item-y>", "event_type": "work_item_deleted"}'
# Returns:
# {
# "event_id": "...", "timestamp": "2026-04-12T14:33:00Z",
# "actor": { "id": "<user-x>", "email": "user-x@acme.com" },
# "actor_role": "workspace_admin",
# "request_id": "...",
# "ip_address": "1.2.3.4"
# }
# 2. Verify user had the role at that time
mcp__pm33-staging__pm33_query_audit_log \
filters='{"actor_id": "<user-x>", "event_type": "role_changed"}' \
before='2026-04-12T14:33:00Z'
# Returns the role history. Confirms user-x had workspace_admin at deletion time.
# 3. Verify the role authorized the action
# Check shared/roles.ts: workspace_admin includes 'work_item:delete' scope. ✅
Documented answer with audit trail. Compliance team satisfied.
Workflow narrative — the bypass case
Two months ago, a developer set PM33_ALLOW_SHARED_INDEX=true to bypass the per-agent git index protection. The bypass got recorded. Compliance dashboard shows: "3 bypasses of per-agent index in the last 30 days, all from same engineer, all during 11pm-2am."
Investigation: the engineer was working on a feature where their CI bot kept dropping the agent ID env var, causing the bypass. Root cause: a CI script bug. Fix: patch the script.
Outcome: bypass count drops to 0. The signal worked. No one was punished (the bypass was legitimate). The underlying tooling got fixed. Compliance moved on.
If PM33 had blocked the bypass instead of tracking it, the engineer would have either:
- Found a different way to disable the check (worse — no audit trail)
- Stopped using the tool (worse — productivity loss)
Observation beat enforcement. This is the design philosophy.
What this enables for buyers
Three buyer-facing pitches:
-
Compliance review takes 1 meeting, not 6. Auditors get read-only audit log access. They run queries themselves. They write the report.
-
No "shadow IT" risk. Bypasses are observable. Unusual access patterns surface. You're not flying blind on what your team is doing with AI agents.
-
Tenant isolation is at the DB, not the app. A bug in service code can't leak cross-tenant data. The blast radius of a code defect is one tenant, never multiple.
Further reading
CLAUDE.md§4 (Security is non-negotiable) — the canonical rules.claude/examples/security-invariants.md— RLS, GRANT, enum sync patternsshared/roles.ts— the 15 canonical rolesserver/services/auditLogService.ts— the audit log implementation- Module 6: The Pitch — how to communicate this to a buyer