Groundfloor Docs

Data Vault API

Browse, query and change your workspace's collections and records, and manage who can use each collection.

Base path: /v1/workspaces/{workspace_id}/vault

Use these APIs to work with your data, in the shared database or a managed one: list collections, query, create, update and delete records, and manage collections and who can use them. Your code only ever calls this API; never put database credentials or service keys in an app.

See also: Data Vault (browse databases in the UI) · Managed Databases (provision clusters)

Permissions (two caller types)

CallerTokenEnforcement
Team memberTeam tokenWorkspace read / write / delete / ddl
Shell site userWorkspace-site JWTActive site user + collection Access (read / write / delete). No DDL.
ActionEndpoints (builders)Site users
readlist, schema, query, countSame, if Access grants read (or higher)
writecreate / update recordsIf Access grants write (implies read)
deletedelete recordsIf Access grants delete (implies write + read)
ddlcreate / update / drop collections403

write ⊃ read and delete ⊃ write ⊃ read for site Access.

Agent rule: Before querying a table with a workspace-site token, assume only admin has Access until you verify or set grants. A role pack with vault:write is not enough.

List collections (browse databases)

GET /v1/workspaces/{workspace_id}/vault/collections
curl -s -H "Authorization: Bearer $TOKEN" \
  "https://platform.groundfloor.cloud/v1/workspaces/$WORKSPACE_ID/vault/collections" | jq .

For site users, the list is filtered to collections with at least read.

Each collection includes created_by, created_by_display_name, created_by_email, and created_at when Groundfloor recorded its creation (explicit create or first write). In the list they are null for collections created before this was recorded. GET …/schema returns the same fields, falling back to the audit log for older collections while that event is still retained.

Get collection schema

GET /v1/workspaces/{workspace_id}/vault/collections/{name}/schema

Query records

POST /v1/workspaces/{workspace_id}/vault/collections/{name}/query
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"filter":{},"limit":25,"offset":0}' \
  "https://platform.groundfloor.cloud/v1/workspaces/$WORKSPACE_ID/vault/collections/tenants/query" | jq .

Request body (query)

FieldTypeDescription
filterobjectShorthand ({ "status": "active" }, { "$text": "alice" }) is translated for you; structured field / and / or trees pass through
sortarrayShorthand strings like ["-created_at"] or objects { "field", "direction" }
projectionstring[]Fields to return. Omit for all fields
limitintegerPage size, 1 to 500 (default 50)
offsetintegerPagination offset (default 0)
connectionstringDatabase to query: postgresql (the shared default) or the name of a dedicated database. Can also be given as ?connection=

Filter operators

Shorthand filters take a field name mapped to a value (equality) or to an object of operators:

OperatorMeaning
$eq, $neEqual, not equal
$gt, $gte, $lt, $lteGreater / less than (or equal)
$in, $ninValue is, or isn't, in a list
$containsContains the text
$regexMatches a regular expression
$existsField is present and not null
{ "filter": { "status": "active", "age": { "$gte": 18 } } }

Several fields in one shorthand filter are combined with AND. To combine conditions with OR, or to negate, use the structured form. Each leaf is a field object with operator (eq, ne, gt, gte, lt, lte, in, nin, contains, starts_with, ends_with, exists, regex) and value; trees nest with and, or and not, and id_eq matches one record ID:

{
  "filter": {
    "or": [
      { "field": { "field": "status", "operator": "eq", "value": "active" } },
      { "field": { "field": "name", "operator": "starts_with", "value": "A" } }
    ]
  }
}

$and and $or are not shorthand operators. In a shorthand filter they are read as field names, not as combinators, so they don't combine your conditions. Use the structured and / or form above.

$text expands across searchable string-like fields on the collection.

Query total vs limit

total is the unpaged match count. limit only sizes items. A page of 5 rows with 40 matches returns total: 40, has_more: true.

Nested filters

Dot-paths such as metadata.sector are applied against JSON/object fields. A malformed path (metadata.) returns 400 unsupported path instead of an empty page.

Implicit collections

GET …/vault/collections?include_implicit=true (default) lists collections created by the first POST …/vault/collections/{name} even when no DDL schema was posted. include_implicit=false is schema-registered names only. A collection created this way gets the same starting Access as one created with DDL: admin → delete.

Count records

GET /v1/workspaces/{workspace_id}/vault/collections/{name}/count

Returns the number of records. Add ?filter= with a JSON filter, in the same shorthand or structured form as the query body, to count only matching records. A $text filter is counted with a bounded scan: on a collection larger than the scan limit it's 400, never a partial count; page the query route with chunked_scan instead.

Create record

POST /v1/workspaces/{workspace_id}/vault/collections/{name}
{ "record": { "name": "Ada", "status": "active" } }

Returns 201 with { "document": { … } }. The record must match the collection's schema, otherwise 400; an unknown collection is 404.

Permission: write (builder) or collection Access write (site)

Update record

PUT /v1/workspaces/{workspace_id}/vault/collections/{name}/{document_id}

Takes the same { "record": { … } } body and returns { "document": { … } } with the full stored record. A caller who can write the collection but not read it gets back only the fields it sent, plus the fields Groundfloor sets.

There is no route that fetches a single record by ID. Use query with { "filter": { "id_eq": "<document_id>" } }.

Permission: write

Delete record

DELETE /v1/workspaces/{workspace_id}/vault/collections/{name}/{document_id}

Returns 204 with no body.

Permission: delete

DDL — create collection

POST /v1/workspaces/{workspace_id}/vault/collections

Permission: ddl (builders only). Seeds collection Access with one grant, admin → delete. You can change or remove it like any other grant.

Body: { "schema": { "name", "engine" or "connection", "json_schema" or "fields" } }. With no engine or connection the collection goes to shared PostgreSQL (postgresql).

Connections. Every Vault route takes an optional ?connection=: postgresql (shared, the default) or the name of a dedicated PostgreSQL in this workspace. Any other value — another engine, or a name that isn't one of this workspace's databases — is 400. Missing fields / json_schema.properties is 400 with a field error (schema.fields … must not be empty), not a 500.

id, tenant_id, and gf_extra are reserved — Groundfloor adds them automatically as system columns on every Postgres table. A schema that declares any of them is 400 with a field error, not a 500. PostgreSQL is the only engine offered. On Postgres, record fields the schema doesn't declare are stored in gf_extra and merged back into the record on read; filters can target them.

Returns 201.

DDL — update collection

PUT /v1/workspaces/{workspace_id}/vault/collections/{name}

Body: { "schema": { … } } with the updated definition, either { "description", "json_schema" } or { "fields", "indexes" }. The collection's name and engine can't change. Returns the updated collection; 404 if the collection doesn't exist, 400 if the definition is refused. Permission: ddl (builders only).

  • Fields you add are added to the table.
  • Marking an existing field unique adds the constraint. If existing records already have duplicate values for it, the update is refused with 409 and nothing changes. Sending unique: false, or leaving the flag out, doesn't remove uniqueness; there's no way to turn it off yet.
  • An unknown field type is 400, naming the field. autoincrement is accepted, so a schema you read with GET …/schema can be sent back unchanged.
  • When the collection is busy or too large to change right now, the update is 503; try again later.

DDL — drop collection

DELETE /v1/workspaces/{workspace_id}/vault/collections/{name}

Permission: ddl — irreversible. Removes Access grants for that collection. Returns 204.

Collection Access (Shell site roles)

Grants target workspace-site role names (Authentication realm roles), not individual users. A grant can also target a workload identity with principal_type: "workload_identity" and its principal_id.

MethodPathWho may call
GET…/vault/aclBuilders with read
GET…/vault/collections/{name}/aclBuilders with read
PUT…/vault/collections/{name}/aclBuilders with ddl

Only your team manages Access, in the Client Portal or with a builder token. Site users get 403 on these routes, whatever their role, admin included.

Get one collection

GET /v1/workspaces/{workspace_id}/vault/collections/{name}/acl
{
  "workspace_id": "…",
  "collection": "orders",
  "grants": [
    { "role": "admin", "action": "delete" },
    { "role": "user", "action": "write" },
    { "role": "viewer", "action": "read" }
  ]
}

Replace grants

PUT /v1/workspaces/{workspace_id}/vault/collections/{name}/acl
Content-Type: application/json

{
  "grants": [
    { "role": "admin", "action": "delete" },
    { "role": "user", "action": "write" },
    { "role": "viewer", "action": "read" }
  ]
}

PUT replaces the whole list and saves it exactly as sent. A new grant to a role that doesn't exist in your workspace's sign-in is 422, naming the role; admin is always accepted. The built-in admin role is a role like any other: if you leave it out, its grant is removed and admins lose access to the collection. The same goes for workload identity grants. Read the current grants with GET first and send them back with your change. Do not rely on catalog packs alone.

Access matrix

GET /v1/workspaces/{workspace_id}/vault/acl

Returns all collections and their grants — useful for admin UIs and agent audits.

Agent suggestion on 403

  1. Confirm token type (platform vs site).
  2. If site: with a builder token, GET …/collections/{name}/acl → if the role is missing, add it and PUT the full list back (keep admin and the other grants), or tell the operator to use Access in the Client Portal. A site token can't read or change Access, even with the admin role.
  3. If platform: check Team for workspace read/write/ddl.
  4. Never open managed-data backends with a service key from agent/client code.

One-time workspace setup

Before vault queries work, the workspace needs data APIs connected. Use the setup card in Data Vault, or:

POST /v1/workspaces/{workspace_id}/dataplane/provision

Errors

StatusMeaning
403Missing workspace permission or missing collection Access for site role
404Collection or document not found
409A schema update would make a field unique while records have duplicate values for it
502Upstream data API error (check detail message)
503The data API is busy or briefly unavailable; try again