Skip to content

Collections API

Read, create, update, delete collections (admin-only).

All endpoints require an admin JWT.

GET /api/v1/collections
→ { "data": [ {Collection}, ... ] }
GET /api/v1/collections/<id_or_name>
→ { "data": {Collection} }
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 name
  • 422 — bad type, missing view_query for 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)

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) — the query is rejected with 422.

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. 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. 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 /api/v1/collections/<id_or_name>
→ { "data": null }

Drops the underlying table or view. Records are gone. No undo. An unknown collection is a 404.

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
created_at: number; // unix seconds
updated_at: number;
}

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.

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" }] }

Per-collection indexes are managed via:

GET /api/v1/admin/collections/<name>/indexes
POST /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. 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.