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)
| Caller | Token | Enforcement |
|---|---|---|
| Team member | Team token | Workspace read / write / delete / ddl |
| Shell site user | Workspace-site JWT | Active site user + collection Access (read / write / delete). No DDL. |
| Action | Endpoints (builders) | Site users |
|---|---|---|
read | list, schema, query, count | Same, if Access grants read (or higher) |
write | create / update records | If Access grants write (implies read) |
delete | delete records | If Access grants delete (implies write + read) |
ddl | create / update / drop collections | 403 |
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/collectionscurl -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}/schemaQuery records
POST /v1/workspaces/{workspace_id}/vault/collections/{name}/querycurl -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)
| Field | Type | Description |
|---|---|---|
filter | object | Shorthand ({ "status": "active" }, { "$text": "alice" }) is translated for you; structured field / and / or trees pass through |
sort | array | Shorthand strings like ["-created_at"] or objects { "field", "direction" } |
projection | string[] | Fields to return. Omit for all fields |
limit | integer | Page size, 1 to 500 (default 50) |
offset | integer | Pagination offset (default 0) |
connection | string | Database 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:
| Operator | Meaning |
|---|---|
$eq, $ne | Equal, not equal |
$gt, $gte, $lt, $lte | Greater / less than (or equal) |
$in, $nin | Value is, or isn't, in a list |
$contains | Contains the text |
$regex | Matches a regular expression |
$exists | Field 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}/countReturns 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/collectionsPermission: 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
uniqueadds the constraint. If existing records already have duplicate values for it, the update is refused with 409 and nothing changes. Sendingunique: 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.
autoincrementis accepted, so a schema you read withGET …/schemacan 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.
| Method | Path | Who may call |
|---|---|---|
GET | …/vault/acl | Builders with read |
GET | …/vault/collections/{name}/acl | Builders with read |
PUT | …/vault/collections/{name}/acl | Builders 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/aclReturns all collections and their grants — useful for admin UIs and agent audits.
Agent suggestion on 403
- Confirm token type (platform vs site).
- If site: with a builder token,
GET …/collections/{name}/acl→ if the role is missing, add it andPUTthe full list back (keepadminand 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 theadminrole. - If platform: check Team for workspace
read/write/ddl. - 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/provisionErrors
| Status | Meaning |
|---|---|
| 403 | Missing workspace permission or missing collection Access for site role |
| 404 | Collection or document not found |
| 409 | A schema update would make a field unique while records have duplicate values for it |
| 502 | Upstream data API error (check detail message) |
| 503 | The data API is busy or briefly unavailable; try again |
Related
- Access API — site-role grants (Vault, Files, Apps)
- M2M API — Coderunner/CI JWT (skips collection ACL in v1)
- Shell Data Vault
- Authentication — site roles
- Team and roles — platform builders
- Managed Databases — provision clusters
- Secrets API
- Deploy with agents