Skip to content

Architecture decisions

The decisions that shaped this library, with the reasoning and where each one is recorded. A decision that is only in someone's head is re-litigated every quarter.

One source for the tenant

Decision. The tenant is resolved once, recorded in SecurityContext, and read from there. Why. Every alternative — a header, a path segment, a body field, a per-layer lookup — is a second source, and two sources disagree exactly when it matters. Where. jdlib/security-model.md, the resolver order in jdlib.tenancy.resolution; the assertion in the enterprise example's tenant-isolation suite.

Denial is non-execution

Decision. The PEP runs before the handler or the tool body; a denial means the body never ran. Why. A check inside a handler is a code path someone can forget, and a denial that half-ran is worse than an error. Where. jdlib.security.authz.pep; the security layers measure denial as non-execution, with an allowing control in the same test.

A degraded dependency is a refusal

Decision. An unreachable or unreadable decision point is treated as a denial, not as an allow. Why. The alternative makes an outage into a permission — the one failure mode where the security system silently stops being one. Where. jdlib.security.authz.pep, jdlib.security.authz.decision.

Isolation is enforced twice

Decision. The unit of work is tenant-bound and the database's row-level security applies to the application role. Why. The first is discipline, the second is enforcement. A forgotten predicate returns nothing instead of another tenant's rows. Where. jdlib.persistence.session, jdlib.persistence.strategies.rls, jdlib/03-isolation-invariants.md.

Allow-list, not deny-list

Decision. Span attributes and metric labels come from closed sets; anything else is dropped or refused. Why. A deny-list has to know every bad thing and fails open; an allow-list has to know every good thing and fails closed. It is also shorter and reviewable. Where. jdlib.security.tracing, jdlib.security.telemetry.MetricLabelError.

Two roles, always

Decision. Migrations run as an owner role; the application connects as an application role that the policy applies to. Why. A single role makes row-level security decorative, and the mistake is invisible until an audit. Where. JDLIB_RLS__APP_ROLE; jdlib rls verify.

The graph is derived

Decision. Neo4j holds relationships the relational plane cannot express efficiently, and it is rebuilt from the relational plane rather than being its source of truth. Why. Two authoritative stores for tenant data is two answers to "what is true", and the graph is the one that can be rebuilt. Where. jdlib.graph, jdlib/architecture/neo4j-flow.md.

Generated, not written, where a table would go stale

Decision. The matrices, the inventory and the API reference are generated from the tree and checked by the gate. Why. A hand-written table of line numbers is wrong the first time someone edits the file above it. Where. scripts/feature-matrix.py, scripts/docs-inventory.py, scripts/api-reference.py.

One authority per topic

Decision. Each topic has one design authority page; the portal routes to it instead of restating it. Why. Two pages that describe the same mechanism disagree eventually, and the reader has no way to tell which is current. Where. documentation.md, and the inventory's alias table.