Skip to content

Admin API

The admin API is a Strawberry GraphQL endpoint at POST /admin/graphql (REQ-533). It requires a superuser or admin role (REQ-125, REQ-060) and is separate from the data GraphQL endpoint (REQ-533).

Authentication

Pass your credentials in the Authorization header using the standard Provisa auth provider (REQ-120):

Authorization: Bearer <token>

Admin access is governed by the admin capability assigned to a role (REQ-060, REQ-042).

Personal access tokens

A personal access token is accepted anywhere a bearer token is, including this endpoint. Issuing and revoking one is self-service — it is the token holder's own credential, so it lives on the user's profile in the admin UI rather than under an admin page, beside leaving an org and deleting the account. An administrator does not mint tokens on someone else's behalf. (REQ-1263)

Route Effect
POST /auth/tokens Mint a token for the caller. Body: name, optional role_id, scopes, expires_in_days (1–366). The response is the only place the secret ever appears
GET /auth/tokens The caller's active tokens in this org — display prefix, name, lifecycle timestamps, and the hash that identifies a token for revocation. Never a working credential
DELETE /auth/tokens/{token_hash} Revoke one of the caller's tokens. 404 when it is not theirs or already revoked

Omitting role_id leaves the token resolving to whatever role its owner holds; naming one narrows the token below its owner. Revocation also happens implicitly: removing a user's org membership revokes their tokens for that org. See Security Model for the credential itself.

Capabilities

Config Management

Download the current running config (REQ-164):

GET /admin/config

Returns the full config.yaml as a YAML file. Upload a new config (REQ-164):

PUT /admin/config

Provisa validates the YAML, reloads catalogs, and regenerates schemas (REQ-012, REQ-253). No restart required.

Runtime Settings

Read and write runtime platform settings without editing the config file (REQ-165):

GET  /admin/settings
PUT  /admin/settings

The settings surface covers large-result redirect, default sampling and row limit, response-cache TTL, naming convention, relationship FK auto-tracking, materialization-store DSN, federation-engine memory (jvm_heap_gb, query_max_memory, query_max_memory_per_node, query_max_total_memory, fault_tolerant_execution, fault_tolerant_task_memory, exchange_spool_dir), and the full OpenTelemetry tracing-pipeline tuning surface (REQ-1082). Remote-GraphQL traversal limits and warm-tier/read-cache settings are also exposed (REQ-1081, REQ-1083).

Security posture — security.mode (standard | high) — applied on restart (REQ-1079):

GET  /admin/security
PUT  /admin/security

AI model assignments, the embedding/vector-model registry, and the NL rate limit — applied on restart (REQ-1080):

GET  /admin/ai-models
PUT  /admin/ai-models

The admin encryption tab derives its provider list live from the encryption registry; unavailable providers appear but are not selectable (REQ-1091).

GET/HEAD /health and GET /setup/status are always unauthenticated — they bypass the Authorization: Bearer requirement even when an auth provider is configured (REQ-539).

Relationship Editor

List relationships (REQ-166):

query {
  relationships {
    id
    sourceTableId
    targetTableId
    sourceColumn
    targetColumn
    cardinality
    materialize
  }
}

Create a relationship (REQ-019):

mutation {
  upsertRelationship(input: {
    id: "orders-to-customers"
    sourceTableId: "orders"
    targetTableId: "customers"
    sourceColumn: "customer_id"
    targetColumn: "id"
    cardinality: "many_to_one"
  }) {
    success
  }
}

AI Relationship Discovery

Trigger Claude-powered FK analysis via REST (REQ-167, REQ-018):

curl -X POST http://localhost:8001/admin/discover/relationships \
  -H "Content-Type: application/json" \
  -d '{"scope": "domain", "domain_id": "sales"}'

Returns FK candidates ranked by confidence. Accept a candidate:

curl -X POST http://localhost:8001/admin/discover/candidates/{id}/accept \
  -H "Content-Type: application/json" \
  -d '{"name": "orders_to_customers"}'

Schema Introspection

Browse published tables across all sources (REQ-008):

query {
  tables {
    id
    sourceId
    columns {
      columnName
      unmaskedTo
      writableBy
    }
  }
}

View Management

Register a materialized view (REQ-133, REQ-135):

mutation {
  registerTable(input: {
    viewSql: "SELECT o.id, o.amount, c.name FROM orders o JOIN customers c ON o.customer_id = c.id"
    mvRefreshInterval: 300
    materialize: true
  }) {
    success
  }
}

Trigger a manual refresh (REQ-135):

mutation {
  refreshMv(mvId: "orders-with-customers") {
    success
  }
}

Graph Source Registration

Neo4j and SPARQL sources are registered via REST endpoints (not the GraphQL admin API) (REQ-295, REQ-297):

Neo4j:

# 1. Register the Neo4j source
curl -X POST http://localhost:8001/admin/sources/neo4j \
  -H "Content-Type: application/json" \
  -d '{"source_id": "graph", "host": "neo4j", "port": 7474, "database": "neo4j"}'

# 2. Preview a Cypher query (validates scalar projections)
curl -X POST http://localhost:8001/admin/sources/neo4j/graph/preview \
  -H "Content-Type: application/json" \
  -d '{"cypher": "MATCH (p:Person) RETURN p.name AS name, p.age AS age"}'

# 3. Register a table (runs preview+validate automatically)
curl -X POST http://localhost:8001/admin/sources/neo4j/graph/tables \
  -H "Content-Type: application/json" \
  -d '{"table_name": "people", "cypher": "MATCH (p:Person) RETURN p.name AS name, p.age AS age", "ttl": 300}'

SPARQL:

# 1. Register the SPARQL source
curl -X POST http://localhost:8001/admin/sources/sparql \
  -H "Content-Type: application/json" \
  -d '{"source_id": "kg", "endpoint_url": "http://fuseki:3030/ds/sparql"}'

# 2. Register a table (probes endpoint and infers columns)
curl -X POST http://localhost:8001/admin/sources/sparql/kg/tables \
  -H "Content-Type: application/json" \
  -d '{"table_name": "products", "sparql_query": "SELECT ?name ?category WHERE { ?p a :Product ; :name ?name ; :category ?category . }", "ttl": 600}'

Once registered, tables appear in the GraphQL schema and are queryable like any other source (REQ-016).

GraphiQL

The admin API ships with GraphiQL at GET /admin/graphql in the browser (REQ-622). Use it to explore the full admin schema interactively.

Ops-domain management views (REQ-1386)

Eight SQL views are seeded into the built-in ops domain on every install. [tool-verified: provisa/api/startup_seed.py:225-331 _seed_ops_domain] They expose the query audit log as governed tables — queryable through SQL (pgwire), GraphQL, and Cypher under the same domain access, RLS, and masking rules as any business table.

org_admin is designated as the ops-domain steward at seed time, so the domain never appears as a governance gap in stale_metadata. [tool-verified: startup_seed.py:326-331]

View What it answers
usage_ranking Query count and distinct users per registered table; zero-hit tables surface as deprecation candidates
deprecated_usage Every access to a table or column carrying the deprecated tag — the active consumers blocking safe removal
pii_access Every access to a table or column carrying the pii tag: who queried it, under which role, over which surface
policy_denials All access attempts that governance rejected (HTTP 401/403)
surface_mix Daily query count and distinct users per protocol surface (SQL, GraphQL, Cypher, gRPC, etc.)
query_health Daily error count and average/max latency per surface
stale_metadata Tables and columns missing descriptions; domains missing a steward
join_hotspots Table pairs co-queried most often — candidates for materialization or caching

Two limits apply today. Granularity is at the table level — the audit log records table_ids, not individual columns accessed. Query text is encrypted (REQ-689) and excluded from every view here; it is accessible only through the authorised admin decrypt path. [tool-verified: _meta_views.py:148-162 — comment notes query_text_enc exclusion]

A role needs ops domain access before these views are visible. Grant it the same way you grant access to any other domain.

-- Which tables have never been queried?
SELECT table_name, domain_id
FROM ops.usage_ranking
WHERE query_count = 0;

-- Who accessed PII-tagged data in the last 7 days?
SELECT user_id, role_id, source, pii_column, logged_at
FROM ops.pii_access
WHERE logged_at >= CURRENT_DATE - INTERVAL '7 days'
ORDER BY logged_at DESC;

-- Where does traffic originate by protocol?
SELECT source, day, query_count, distinct_users
FROM ops.surface_mix
ORDER BY day DESC, query_count DESC;

The same queries run as GraphQL or Cypher over any governed transport — pgwire, Arrow Flight, or Bolt. [inferred from governed-surface design]

Reports viewer (REQ-1390)

The Reports viewer is at /admin/reports. Roles without the observability capability cannot reach it.

The left panel lists every registered table in the ops domain, sorted by alias. [tool-verified: ReportsTab.tsx:46-52 — filters tables to domainId === "ops"] The eight seeded management views appear there automatically. Click any report to load it in the governed data viewer on the right.

Adding a custom report. The "Add report" button opens a dialog. Provide a name, an optional description, and a SELECT statement. Saving registers the view as a governed derived table in the ops domain — cataloged, access-controlled, and queryable through every surface alongside the seeded views. [tool-verified: ReportsTab.tsx:70-96registerTable called with sourceId: DERIVED_SOURCE_ID, domainId: "ops"]

Deleting. The trash icon appears only for custom reports. Seeded management views cannot be deleted from this interface. [tool-verified: ReportsTab.tsx:151const custom = report.sourceId === DERIVED_SOURCE_ID gates the delete button]

Table preview (REQ-1392)

Expand any table row on the Tables page. The Preview button opens a 90%-width modal with the table's live governed data. [tool-verified: TablePreviewModal.tsx:24size="90%"; GovernedTableViewer.tsx is the underlying viewer]

Tables backed by APIs with required path parameters block preview until those values are supplied. An inline form collects each required parameter before the first query runs; optional query parameters appear in the same form. [tool-verified: GovernedTableViewer.tsx:51-55, 153-155requiredParamColumns check; "paramsRequired" message shown when activeParams == null]

Governed data viewer (REQ-1391)

The same viewer component powers the preview modal and the Reports viewer. Its behavior is identical in both contexts.

Server-side paging. Each page is its own governed SELECT * with LIMIT 101 OFFSET n. 100 rows appear per page; the 101st signals whether more exist. The full dataset is never loaded into the browser. [tool-verified: nativeParams.ts:72LIMIT ${pageSize + 1} OFFSET ${page * pageSize}; types.ts:74PAGE_SIZE = 100]

Pushed-down filters and sorts. Each column header has a filter input. Filter terms become WHERE LOWER(CAST(col AS VARCHAR)) LIKE LOWER('%term%') predicates; sort clicks produce ORDER BY clauses. Both go to the database — a filter on a billion-row table scans the source, not the 100-row page in front of you. [tool-verified: nativeParams.ts:53-70]

Multi-level group-by. The Layers icon in any column header toggles that column into the grouping. Group columns lead the ORDER BY so group members land on the same page as their header across page boundaries. Primary-key columns are appended as a stable tiebreaker. [tool-verified: nativeParams.ts:61-70 — group columns first, then explicit sorts, then PKs] Group-header rows are collapsible; collapsing hides members without issuing a new query. [tool-verified: useResultsGrid.ts:150-171collapsedGroups set gates the build() recursion]

Persistent choices. Filter, sort, and group-by settings persist to localStorage under provisa.grid.table:<domain>.<table> and restore on the next visit. [tool-verified: useResultsGrid.ts:95-98, GovernedTableViewer.tsx:66]

Export. Download the current page as CSV, or copy it to the clipboard as tab-separated text. Export covers the visible page only. [tool-verified: useResultsGrid.ts:247-274 — both handlers iterate displayRows, which in server-paged mode is the current page]