Groundfloor Docs

Data Vault

Query customer data through the Control Plane vault proxy — never call Dataplane directly.

Audience: Federated app developers
Base path: /v1/workspaces/{workspace_id}/vault
Auth: Keycloak bearer token + workspace permissions
Backend: Dataplane (proxied — apps never call Dataplane directly)

Per Lore decision controlplane-data-vault-via-dataplane-dsl-2026-05-03, all customer-data access flows through Control Plane. The API loads a per-workspace DATAPLANE_SERVICE_API_KEY from Infisical and proxies to Dataplane with workspace-scoped tenant context.


Why not Dataplane directly?

Call Dataplane from browserCall Control Plane /vault
Exposes service API keysKeys stay server-side
Bypasses SpiceDB checksReBAC enforced per request
No audit trail in PortalActions audited (vault.query, vault.record.*, DDL)

Federated remotes should use the same Control Plane routes as the Customer Portal Data Vault UI.


Permissions

OperationPermission
List collections, schema, query, countread
Create / update recordswrite
Delete recordsdelete
Create / drop collections (DDL)ddl (typically workspace admins)

Endpoints

MethodPathPurpose
GET…/vault/collectionsList collections
GET…/vault/collections/{name}/schemaJSON Schema + flattened fields
POST…/vault/collections/{name}/queryFiltered, paginated query
GET…/vault/collections/{name}/countDocument count
POST…/vault/collections/{name}Create record
PUT…/vault/collections/{name}/{document_id}Update record
DELETE…/vault/collections/{name}/{document_id}Delete record
POST…/vault/collectionsDDL — create collection
DELETE…/vault/collections/{name}DDL — drop collection

List collections

GET /v1/workspaces/{workspace_id}/vault/collections
Authorization: Bearer {token}
{
  "collections": [
    {
      "name": "customers",
      "engine": "arangodb",
      "document_count": 42,
      "description": null
    }
  ]
}

engine identifies the underlying connector (arangodb, postgres, clickhouse, qdrant, etc.) — useful for UI icons and query expectations.


Get schema

GET /v1/workspaces/{workspace_id}/vault/collections/customers/schema
Authorization: Bearer {token}
{
  "name": "customers",
  "engine": "arangodb",
  "fields": [
    { "name": "name", "type": "string", "required": true, "description": null },
    { "name": "email", "type": "string", "required": false, "description": null }
  ],
  "raw_schema": { "type": "object", "properties": {  }, "required": ["name"] }
}

Use fields for tables and forms; use raw_schema when you need full JSON Schema.


Query records

POST /v1/workspaces/{workspace_id}/vault/collections/customers/query
Authorization: Bearer {token}
Content-Type: application/json

{
  "filter": { "status": "active" },
  "sort": ["-created_at", "name"],
  "projection": ["name", "email"],
  "limit": 50,
  "offset": 0
}

Response (QueryResultPage):

{
  "items": [ { "_id": "…", "name": "Acme", "status": "active" } ],
  "total": 128,
  "has_more": true,
  "next_offset": 50
}
FieldDefaultMaxNotes
limit50500Page size
offset0Skip N records
sortnullField names; prefix - for descending
projectionnullSubset of fields; null = all
filternullEngine-aware; see below

Paginate with offset += len(items) while has_more is true, or use next_offset when present.

Filter expressions

Filters are passed through to Dataplane. The exact operators depend on the collection's engine. Common patterns:

Equality (ArangoDB / default):

{ "filter": { "filter_tag": "my-value" } }

Multiple fields (AND semantics — engine-dependent):

{ "filter": { "status": "active", "region": "us-east" } }

For advanced DSL (cross-engine portable queries), Dataplane exposes POST /v1/{collection}/dsl/query — not yet wrapped on Control Plane's vault router. Use simple field filters via /vault/.../query for federated apps today.

Dataplane reference docs (sibling repo groundfloor-dataplane-oss):

  • docs/RBAC_REFERENCE.md — filter operators and auth behavior
  • docs/REBAC_REFERENCE.md — permission model on Dataplane side
  • Shell bundled copy: baas-api/dataplane-api/GROUND_FLOOR_API.md

Count

GET /v1/workspaces/{workspace_id}/vault/collections/customers/count
Authorization: Bearer {token}
{ "count": 128 }

Filtered count via query param is not exposed at MVP — use query with limit: 1 and read total if you need filtered counts.


Create record

POST /v1/workspaces/{workspace_id}/vault/collections/customers
Authorization: Bearer {token}
Content-Type: application/json

{
  "record": {
    "name": "Acme Corp",
    "email": "ops@acme.example",
    "status": "active"
  }
}
{ "document": { "_id": "customers/abc123", "name": "Acme Corp",  } }

Returns 201. Body must conform to the collection JSON Schema — 400 on validation failure.


Update record

PUT /v1/workspaces/{workspace_id}/vault/collections/customers/{document_id}
Authorization: Bearer {token}
Content-Type: application/json

{ "record": { "name": "Acme Corp Updated", "status": "inactive" } }

Use the document id from create/query responses (_id, id, or _key depending on engine).


Delete record

DELETE /v1/workspaces/{workspace_id}/vault/collections/customers/{document_id}
Authorization: Bearer {token}

Returns 204.


DDL — create collection

Requires ddl permission (workspace administer tier).

POST /v1/workspaces/{workspace_id}/vault/collections
Authorization: Bearer {token}
Content-Type: application/json

{
  "schema": {
    "name": "my_app_events",
    "engine": "arangodb",
    "json_schema": {
      "type": "object",
      "properties": {
        "event_type": { "type": "string" },
        "payload": { "type": "object" }
      },
      "required": ["event_type"]
    }
  }
}

Alternative Dataplane-native shape:

{
  "schema": {
    "name": "my_app_events",
    "connection": "arangodb",
    "fields": [
      { "name": "event_type", "field_type": "text", "required": true },
      { "name": "payload", "field_type": "json", "required": false }
    ]
  }
}

Control Plane translates UI-style json_schema to Dataplane fields via to_dataplane_create_collection.


DDL — drop collection

DELETE /v1/workspaces/{workspace_id}/vault/collections/my_app_events
Authorization: Bearer {token}

Returns 204. Destructive — drops all data in the collection.


TypeScript example

import {
  configureControlPlaneClient,
  setControlPlaneAuthProvider,
  useQueryCollectionV1WorkspacesWorkspaceIdVaultCollectionsCollectionQueryPost,
} from "@groundfloor/api-client";

configureControlPlaneClient({ baseURL: process.env.NEXT_PUBLIC_API_URL });
setControlPlaneAuthProvider(async () => keycloak.token ?? null);

function CustomerList({ workspaceId }: { workspaceId: string }) {
  const query = useQueryCollectionV1WorkspacesWorkspaceIdVaultCollectionsCollectionQueryPost(
    workspaceId,
    "customers",
    { filter: { status: "active" }, limit: 25, offset: 0 },
    { query: { enabled: !!workspaceId } },
  );
  // query.data?.items, query.data?.has_more
}

Hook names follow OpenAPI operation ids — regenerate after API changes.


Errors

StatusMeaning
401Invalid or missing Keycloak token
403Missing workspace permission
404Workspace, collection, or document not found
503Workspace not provisioned, or missing DATAPLANE_SERVICE_API_KEY
502Dataplane rejected request (auth, timeout, upstream error)

502 auth hint: Workspace needs a provisioned Dataplane service key in Infisical (DATAPLANE_SERVICE_API_KEY, environment ws-{workspace_id prefix}). Re-run workspace provisioning or scripts/backfill_dataplane_workspace_key.py.

503 workspace: Workspace status must be active or frozen; provisioning workspaces return not ready.


Prerequisites (local dev)

  1. Dataplane running (groundfloor-dataplane-oss, default http://localhost:8080)
  2. Workspace provisioning saga completed (mints Dataplane tenant + API key)
  3. Control Plane .env: DATAPLANE_URL=http://localhost:8080
  4. Optional: create test collections via Portal Data Vault UI or DDL API

On this page