Records
Versioned records are stored in each collection's typed physical table and exposed through plain HTTP. The same request and response envelopes work with browser fetch, server runtimes and generated clients.
Dashboard editor
Open a collection and choose New record. Trestle opens a syntax-highlighted JSON editor prefilled with every field from the collection schema. Editing a row opens the same editor with the complete current values, so a user never starts from an unexplained empty object. Records are fetched in bounded cursor pages without leaving the dashboard, and page checkboxes support version-safe bulk deletion.
Endpoints
GET /api/v1/collections/:collection/records
POST /api/v1/collections/:collection/records
GET /api/v1/collections/:collection/records/:id
PATCH /api/v1/collections/:collection/records/:id
DELETE /api/v1/collections/:collection/records/:id
Each response includes a stable ID, positive version, created and updated timestamps, and a values object. Use ?fields=title,status for value projection and ?limit=1..100 for bounded lists.
Create and read a record
Send the access token in the Authorization header and wrap collection fields in values.
const createResponse = await fetch(
"/api/v1/collections/issues/records",
{
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
values: { title: "Investigate latency", priority: 2 },
}),
},
);
if (!createResponse.ok) throw new Error("Could not create issue");
const created = await createResponse.json();
const readResponse = await fetch(
`/api/v1/collections/issues/records/${encodeURIComponent(created.id)}`,
{ headers: { Authorization: `Bearer ${accessToken}` } },
);
if (!readResponse.ok) throw new Error("Could not load issue");
const issue = await readResponse.json();
console.log(issue.id, issue.version, issue.values.title);
The idempotency key makes a retried create safe after a network interruption. Record IDs must still be encoded before being inserted into a URL path.
Update with optimistic concurrency
const response = await fetch(
`/api/v1/collections/issues/records/${encodeURIComponent(issue.id)}`,
{
method: "PATCH",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
"If-Match": `"${issue.version}"`,
},
body: JSON.stringify({ values: { priority: 1 } }),
},
);
if (!response.ok) throw new Error("Could not update issue");
const updated = await response.json();
If-Match prevents an older screen from silently overwriting a newer edit. If another request already changed the record, Trestle returns 412 version_conflict; reload the current record and ask the user how to reconcile it.
Filter and follow cursor pages
const query = new URLSearchParams({
limit: "50",
sort: "-updatedAt",
filter: "priority <= 2",
fields: "title,priority",
});
const response = await fetch(
`/api/v1/collections/issues/records?${query}`,
{ headers: { Authorization: `Bearer ${accessToken}` } },
);
if (!response.ok) throw new Error("Could not load issues");
const page = await response.json();
renderIssues(page.items);
URLSearchParams encodes the filters safely. Pass nextCursor back as cursor to load another page; an empty value means there are no more records.
Language-neutral HTTP
curl -X POST http://127.0.0.1:7333/api/v1/collections/issues/records \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: import-42' \
-d '{"values":{"title":"First issue"}}'
See API clients for the same boundary in TypeScript, Python and Go.
Dogfood path
The Incident Desk creates incidents through application-user rules and queries related timeline entries explicitly by incident_id. Follow the complete Incident Desk walkthrough.
Provider storage
Record semantics are provider-neutral: stable IDs, positive versions, timestamps, optimistic concurrency, defaults and projections behave identically on SQLite and the PostgreSQL provider. Boolean and numeric values are stored using each engine's native types; the API envelope never changes. Concurrent updates with the same version yield exactly one winner and one 412 version_conflict on both providers.