Neo4j logoGovernance

Read the Roles, Not the Brochure

You can predict how a tool will behave inside your systems by reading which of its operations require which permission. Roles are enforced by code paths; brochures are not.

Read the Roles, Not the Brochure

You can predict how a tool will behave inside your systems by reading which of its operations require which permission. Roles are enforced by code paths; brochures are not.

So here is the Neo4j integration's permission map, unabridged. Import runs at Admin. Audit, re-audit, generate, history, badge and status all run at Viewer — the read-only role. That asymmetry is the whole governance posture in one line: exactly one verb can write to your governed model, and the verb that writes is the one that only ever adds. Everything below elaborates that sentence.

Additive means additive

When you import meta_schema and constraints, CoreModels creates governed Types from your node labels, Elements from your properties, and references from your outgoing typed relationships. On the second import — a month later, after the graph has moved — existing governed nodes are not mutated and not deleted. New material is added; everything already governed is left exactly as it is.

The response carries the receipt rather than asking you to trust it: alongside datasetsAdded and fieldsAdded sits datasetsSkippedExisting, which is the count of labels the import declined to touch because they were already governed.

The asymmetry is deliberate. A model that silently rewrote itself to match whatever the database currently contains is not a governed model — it is a mirror, and a mirror cannot detect drift. Detecting change is the audit's job. Applying a change in meaning is a person's job.

One carve-out, stated plainly because hiding it would undermine the rest: the per-vendor metadata we record about each node — native type, checks, physical name, materialization — is refreshed on every import. That is estate bookkeeping, not governed meaning, and the two travel in separate lanes precisely so the distinction stays legible.

Meaning changes pass through a human

When the graph and the model disagree, the audit says so and then stops. A removed property arrives as field-removed. A retyped property arrives as field-type-drift with the governed and observed types in the detail line. A narrowed value list arrives as enum-narrowed. Each finding carries a stable kebab-case code, a severity, and the subject it is about. None of them edits anything.

Somebody with authority over the model then decides which side is wrong — the graph, or the definition — and makes the change deliberately. That is not a limitation we plan to remove. It is the product.

Where we help with the mechanics, the same rule holds. The sync propose verb takes fresh artifacts and returns a classified, replayable plan: additive material and pure estate bookkeeping travel in the automatic lane, while anything that would mutate governed semantics is classified as meaning-affecting and rides a gated lane where every operation needs an explicit decision. Where the estate and the governed model both moved the same fact, the operation is marked as a conflict and requires an explicit override rather than a heuristic. A proposal is a document, not an action.

Lossiness is a success channel

Every translation between two systems loses something. Most tools resolve this by not mentioning it. We report it as a first-class array on successful responses, because "success" here means "it ran, and here is precisely what it could not carry" — not "nothing was lost".

For Neo4j specifically, three losses are worth knowing about:

Types with no exact governed equivalent. STRING, INTEGER, FLOAT, BOOLEAN and the date and date-time families map cleanly. Points, durations and list-valued properties do not; they are carried as strings, and the approximation is recorded.

Polymorphic relationships. When a typed relationship targets more than one label, the reference is governed to the first target and the full target list is retained as metadata — and the audit raises polymorphic-relationship so the narrowing shows up in the report, not just in a field somewhere. You are told which hops in your graph are ambiguous, by name.

Snapshots that do not fit. Import stores the parsed schema snapshot so re-audit can run later without fresh artifacts. When the encoded snapshot exceeds the storage cap, the import returns snapshotStored: false with a lossiness record explaining the refusal, instead of silently truncating your schema. Fresh-artifact audits keep working; only snapshot-based re-audit is unavailable — and you knew that the moment it happened.

Drift is evidence, and evidence has a chain

A governance claim is worth what its audit trail is worth. Every audit is anchored to a content fingerprint of the artifacts it examined, so a finding ties back to a specific, reproducible input. The report also returns codes — finding code to occurrence count — and driftedObjects, the distinct subjects of Drift findings. Those two fields are what a pipeline keys off; the markdown report is what a person reads.

Recording is opt-in, and the default matters. An ordinary audit records nothing unless the request sets recordHistory: true, because the audit verb stays strictly read-only unless asked. Runs on the CI surface are recorded with a ci trigger, so the trail distinguishes a machine gate from someone clicking a button. Re-audit is the one verb that always records — an unrecorded re-audit would defeat its own purpose.

GET .../graph/integrations/neo4j/history/{projectId} returns the trail: newest first, each entry carrying its timestamp, trigger, counts, code map and fingerprint. The badge renders the latest entry as a color. Neither of them is an opinion.

Review and deploy, in that order

The outbound direction obeys the same posture. generate is a Viewer-role, read-only verb that returns coremodels_constraints.cypher as content in a response body. It does not connect to your database, because there is nothing to connect with — CoreModels holds no Neo4j credentials and opens no Bolt session. Every statement is written IF NOT EXISTS. Property-existence constraints are noted as Enterprise-only in the script itself. The header tells the next reader that meaning changes belong upstream in CoreModels and that the file should be regenerated rather than hand-edited.

You read it, you review it, you deploy it through whatever change process you already trust — or you don't, and nothing happens.

Why the posture is the point

Individually these are conservative engineering choices. Together they make a specific claim: the governed model is authored by people; evidence flows continuously in both directions; translation losses are declared rather than absorbed; and nothing in the loop — not an import, not an audit, not an agent — turns a machine's opinion into governed meaning. Your graph stays yours. What it now has is a definition with an owner, a history, and a way to prove conformance on demand.

Our Neo4j quickstart lays out every verb with the role it runs at, including what each one refuses to do.