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):
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):
Returns the full config.yaml as a YAML file. Upload a new config (REQ-164):
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):
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):
AI model assignments, the embedding/vector-model registry, and the NL rate limit — applied on restart (REQ-1080):
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):
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):
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-96 — registerTable 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:151 — const 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:24 — size="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-155 — requiredParamColumns 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:72 — LIMIT ${pageSize + 1} OFFSET ${page * pageSize}; types.ts:74 — PAGE_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-171 — collapsedGroups 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]