Collections API
Read, create, update, delete collections (admin-only).
All endpoints require an admin JWT.
GET /api/v1/collections → { "data": [ {Collection}, ... ] }Get one
Section titled “Get one”GET /api/v1/collections/<id_or_name> → { "data": {Collection} }Create
Section titled “Create”POST /api/v1/collections{ "name": "posts", "type": "base", // "base" | "auth" | "view" | "singleton" "fields": [ { "name": "title", "type": "text", "required": true }, { "name": "body", "type": "text" } ], "view_query": "...", // required for type=view "list_rule": null, "view_rule": null, // null = public, "" = admin only "create_rule": "", "update_rule": "", "delete_rule": "" // omitted = "" (admin only)}Returns the created collection. Errors:
400— duplicate name422— bad type, missingview_queryfor view, reserved field names (on every collection:id,created,updated,created_at,updated_at,collectionId,collectionName,expand; on auth also the auth columns), malformed view query, or a rule that doesn’t parse (the error names the rule)422— a reserved collection name:admin,auth,files,collections,health,batch,custom,realtime,mcp(the API’s own paths, in any case). Existing collections with one of these names keep it.
If the table can’t be created, nothing is saved.
For auth collections, email + verified implicit fields are auto-injected
if you don’t supply them.
A singleton collection is created with its one record, every field empty. See
Singleton collections.
Rules you omit default to "" (admin only), for every collection type — pass null
explicitly to make an action public.
A view query must be a single SELECT. It can’t reference SQLite metadata
(sqlite_master, sqlite_schema or pragma_*() functions), read Vaultbase’s
own vaultbase_* tables, use U&"…" identifiers, or call functions that run
SQL given as a string (query_to_xml and the like). Over a table with private
columns (an auth collection, or one with a password field) it also can’t
serialize whole rows (row_to_json(u), ROW(u.*)) or rename columns with an
alias list (AS u(a, b, …)). On Postgres the created view is also checked
as Postgres parsed it: it can’t hold a whole row of such a table, and may
read a private column only as a plain output column under its own name (not
in an expression, a filter, a rename or a UNION). Names inside E'…' or
dollar-quoted strings are checked too. Any of these is rejected with 422.
A view saved before these checks keeps working. Altering or renaming it needs a query that passes them.
Update
Section titled “Update”PATCH /api/v1/collections/<id_or_name>{ "fields": [...], "view_query": "...", "list_rule": "...", // ...same shape as create, but partial — type can't change}A collection can’t be turned into a singleton or out of one. Create a new collection and move the data.
Re-runs ALTER TABLE on field-list changes. A change to a table that a view
reads (directly or through another view) is refused with 409, naming the
view, when the view would then expose a private column: adding a password
field, for example. Change or delete the view first. Type changes within an existing
field are blocked (422) — drop + re-add to change a type. Rules are checked
when saved: one that doesn’t parse (a collection rule, or a file field’s
viewRule) answers 422 naming the rule. An update checks only the rules it
sets. An unknown collection is a 404.
Unique fields: adding a field with "options": { "unique": true }, or switching unique
on for an existing field, creates a unique index (422 if existing records
already have duplicate values). Switching it off drops the index. A field that
was created as unique together with the collection keeps a built-in
constraint that can’t be removed in place (422); drop and re-add the field.
Renaming ("name": "new_name") renames the table (or rebuilds the view)
and its indexes, and updates relation fields in other collections, hooks and
record history to the new name. 400 if the name is taken or not a valid
identifier; 422 for a reserved name. Free-text references are not rewritten: update rules that use
@collection.old_name, webhook event patterns such as old_name.create, and
API token scopes yourself.
Delete
Section titled “Delete”DELETE /api/v1/collections/<id_or_name> → { "data": null }Drops the underlying table or view, and its record history.
Records are gone. No undo. An unknown collection is a 404.
The Collection shape
Section titled “The Collection shape”interface Collection { id: string; name: string; type: "base" | "auth" | "view" | "singleton"; fields: string; // JSON-encoded array of FieldDef view_query: string | null; list_rule: string | null; view_rule: string | null; create_rule: string | null; update_rule: string | null; delete_rule: string | null; history_enabled: number; // 0/1 — see Record history (1 by default for new non-view collections since 0.40.0) created_at: number; // unix seconds updated_at: number;}Schema migrations
Section titled “Schema migrations”Use the snapshot endpoints to ship schemas across environments:
GET /api/v1/admin/migrations/snapshot → JSON file with every collection's full definition
POST /api/v1/admin/migrations/apply{ "snapshot": { ...JSON snapshot... }, "mode": "additive" | "sync" // default "additive"}additive creates missing collections; sync also updates existing ones.
Neither deletes anything. See Backups & migrations.
Renaming a field
Section titled “Renaming a field”Send the field under its new name with renamedFrom set to the old one; the column is
renamed in place and keeps its data, its unique index and its search index. A field that
arrives under a new name without the hint is treated as a new field: the old column is
dropped. The admin editor sends the hint for every field whose name you changed. A rename
can’t change the field’s type in the same step.
{ "fields": [{ "name": "headline", "type": "text", "renamedFrom": "title" }] }DB indexes
Section titled “DB indexes”Per-collection indexes are managed via:
GET /api/v1/admin/collections/<name>/indexesPOST /api/v1/admin/collections/<name>/indexes { fields: [...], unique?: bool }DELETE /api/v1/admin/collections/<name>/indexes/<index_name>An index covers one to four fields, in the order given: fields of the collection, or
id, created_at or updated_at. POST also accepts the older { field } for a
single field. The name is generated from the fields (idx_posts_status_created_at, or
uniq_…); GET lists each index with its fields. Asking again for an index
that exists returns it; a name already taken by an index on other fields answers
409 (names join with _, so blog + posts_title and blog_posts + title
meet). Editable from collection → Indexes in the admin UI.
Put the fields a list filters on with = first, then the field it sorts by: a list
filtered on status with the default sort is served by (status, created_at), one
sorted by -views by (views). The Monitoring page records every list query’s
shape and, for lists that read a whole table or sort in memory, names the index that
would serve them and creates it in one click. See Indexes.