Skip to content

Records

Typed CRUD over collections — list, get, create, update, delete, iterate, ETag concurrency, singletons.

vb.collection("name") returns a typed accessor over a single collection. Every method maps 1-1 to a server endpoint and returns the same shape the REST API does.

const posts = vb.collection<Post>("posts");
await posts.list({ filter: "published = true", sort: "-created", perPage: 50 });
await posts.get("p1");
await posts.create({ title: "hi", body: "<p>…</p>" });
await posts.update("p1", { title: "renamed" });
await posts.delete("p1");
for await (const r of posts.iterate({ filter: "published = true" })) { /* … */ }
await vb.collection("posts").list({
page: 1,
perPage: 30,
filter: 'status = "live" && @request.auth.id = author',
sort: "-created,title",
expand: "author,tags",
fields: "id,title",
skipTotal: true, // skip COUNT(*) for huge tables
});

Returns:

{
data: T[];
page: number;
perPage: number;
totalItems: number; // -1 when skipTotal
totalPages: number; // -1 when skipTotal
}
for await (const post of vb.collection<Post>("posts").iterate({
filter: 'tags ?= "tutorial"',
perPage: 100,
})) {
console.log(post.title);
}

Backed by repeated list calls (default perPage: 100) until the server returns a short or empty page, or the last page. A short page is measured against the page size the server used, so perPage above the server’s cap of 500 still reads every record (500 at a time). With skipTotal: true (totalPages is -1) it keeps paging until a short page.

const post = await vb.collection<Post>("posts").get("p1");
// Expand relations and pick fields, as on list (SDK 0.7.1, server 0.26.1+):
const slim = await vb.collection<Post>("posts").get("p1", { expand: "author", fields: "id,title" });

A missing record throws VaultbaseError with kind: "server" and e.data.status === 404 (see Errors). expand and fields work as on list; a related record the user can’t view is left out of expand. vb.singleton(name).get() takes the same two options.

const post = await vb.collection<Post>("posts").create({
title: "Hello",
body: "<p>World</p>",
author: "u_42",
});
// post.id is the freshly generated UUID; created/updated set by the server.

Validation failures throw VaultbaseError with kind: "validation" and a details: { fieldName: message } map. Match codes pre-1.0:

  • 400 → bad request (kind: "server")
  • 403 → rule denied (kind: "auth", reason: "forbidden")
  • 404 → collection / parent record missing (kind: "server")
  • 405 → the collection takes no creates: a view, a singleton, or an auth collection (kind: "server"; register users through vb.auth)
  • 409 → delete blocked by a restrict relation (kind: "conflict")
  • 412 → optimistic-concurrency mismatch (kind: "precondition_failed")
  • 422 → schema validation, including a unique value already taken (kind: "validation")
const updated = await vb.collection<Post>("posts").update("p1", {
title: "Renamed",
});

PATCH semantics — only the fields you send are touched.

ETag/If-Match concurrency runs automatically (see Errors + ETags): the SDK caches the ETag from prior reads and attaches If-Match on the next mutation. Override with:

// Skip the precondition entirely:
await posts.update("p1", { title: "x" }, { ifMatch: false });
// Use an explicit tag:
await posts.update("p1", { title: "x" }, { ifMatch: "W/\"42\"" });
// The default, spelled out: the cached tag, if any
await posts.update("p1", { title: "x" }, { ifMatch: "auto" });

If the server’s ETag has moved, the SDK throws VaultbaseError with kind: "precondition_failed" and the server’s current tag in e.data.currentEtag, so you can retry against the latest state.

await vb.collection("posts").delete("p1");

Same ifMatch semantics as update. Returns null on success.

A singleton collection has one record, read and updated at the collection’s own path. vb.singleton(name) (SDK 0.7.0) returns an accessor with get and update, no id:

const settings = vb.singleton<Settings>("settings");
const current = await settings.get(); // GET /api/v1/settings
await settings.update({ theme: "dark" }); // PATCH /api/v1/settings

update takes the same ifMatch option as collection().update. By default it sends the ETag from this singleton’s last get or update:

await settings.update({ theme: "dark" }, { ifMatch: false }); // no precondition
await settings.update({ theme: "dark" }, { ifMatch: "W/\"42\"" }); // explicit tag

A stale tag throws VaultbaseError with kind: "precondition_failed".

There is no list, create or delete: the server answers those with 405.

Filters live in the same expression language as collection rules. See API rules for the full grammar. For ergonomic, injection-safe construction use the vb.q tag:

const term = req.query.q;
const filter = vb.q`title ~ ${term} && status = ${"live"} && deleted = ${false}`;
// → 'title ~ "hello" && status = "live" && deleted = false'
await vb.collection("posts").list({ filter });
InterpolationEncoded as
string"…" (escaped)
numberbare; rejects NaN/Infinity
booleantrue/false
nullnull
Datequoted ISO string
Array(…) comma-joined — pairs with ?= / ?~
field("name")bare identifier (validated)

vb.q ALWAYS quotes strings — to interpolate a column name use the field() escape:

const sortable = vb.field("title");
const filter = vb.q`${sortable} ~ ${"hello"}`;
// → 'title ~ "hello"'
const ac = new AbortController();
posts.list({ signal: ac.signal });
ac.abort(); // → throws kind: "aborted"
posts.list({ requestKey: "search-suggest" });
posts.list({ requestKey: "search-suggest" });
// → first request is auto-cancelled when the second starts (defaultAutoCancel)

Disable auto-cancel via requestKey: null per-call or defaultAutoCancel: false at construction.

When you generate the Schema type via vb-types, every collection accessor becomes typed:

const vb = new Vaultbase<Schema>({ baseUrl: "..." });
await vb.collection("posts").create({
title: "hi", // ✓
body: "<p>…</p>", // ✓
fakeField: 1, // ✗ TS error
});
const list = await vb.collection("posts").list({ filter: "published = true" });
list.data[0].title; // string — inferred

See Codegen + migrate CLI.