Collections, filters and totals

Create collections, read and write records, filter, count and total.

Collections

You create collections, each with its safety rule; the SDK never does. Locally, use npx gemmein collection add <name> or the dev terminal. On Gemmein, use the dashboard's Data (one click), or carry them there with npx gemmein sync.

Names are lowercase letters, numbers, and underscores only (saved_games, never savedGames), and a bad name throws synchronously.

const tasks = g.collection("tasks")

const task = await tasks.create({ title: "Buy milk", done: false })
console.log(task.id, task.data.title)          // fields live under .data

const { records } = await tasks.list()      // { records, cursor, hasMore }
const recent   = await tasks.list({ limit: 10, sort: "newest" })
const filtered = await tasks.list({ where: { done: false } })   // a filter — see below
const one      = await tasks.get("rec_abc123")
await tasks.update("rec_abc123", { done: true })
await tasks.delete("rec_abc123")

Your fields live under data. Everything else on a record is set by the server and read-only:

FieldWhat it is
idThe record's id
createdAt / updatedAtWhen it was created and last changed
ownerUserIdThe person who wrote it; null for the app's own records (receipts, records added on the dashboard without a person)
collectionIdThe id of the collection it is in (the collection's own id, not its name)
appIdThe id of your app
environmentIdThe id of the environment it lives in, Development or Live
versionGoes up by 1 on every update; pass it as ifVersion to refuse a stale save
publishedfalse on a draft (public rules only)
keyThe create-if-absent key it was created with, when one was used
audienceUserId / recipientsWho an addressed or direct record is for; recipients lists everyone a group message names
writtenBy"server" when your server's secret key wrote it
mineYour own marks on its open fields (below); only you see it
mineRetiredFor a "one per person of" field whose choice you hold but that was taken off its list: true. mine still says the choice; show it as retired, and let the person choose again or withdraw it
flaggedByWho set each "per person flag" field (the first 100). Only on one record read by your dashboard or a secret key
expandThe linked records you asked for with expand
existingtrue when a keyed create returned the record you already made

Filters, counts and totals

A where names up to 5 of your data fields (link fields too), and every field must match. A field's value is a literal, an exact match, or an object of operators, 1 to 3 per field, every one of which must hold:

OperatorMatches whenValue
eq / neThe stored value equals / does not equal it, compared as text: { eq: "10" } matches the number 10, as a literal always has. Null-safe: { eq: null } matches a field that is absent or null, { ne: null } one that holds a value; a bare null literal is refusedstring, number, boolean or null
gt gte lt lteThe stored value is greater / at least / less / at most. A number compares as a number, and a text value in that field never matches; a string compares as text in code-unit order (uppercase before lowercase), so an ISO date orders correctlynumber, or a string of at most 200 characters
in / ninThe stored value is one of / none of the set1 to 50 strings, numbers or booleans
contains / startsWithThe stored text holds / begins with it, case-insensitive (ASCII letters fold; other scripts compare as stored)text of 1 to 200 characters
existstrue: the field is present and not null. false: absent or nullboolean
await orders.list({ where: { amount: { gte: 10 } } })
await orders.list({ where: { status: { in: ["paid", "sent"] } } })
await orders.list({ where: { title: { contains: "invoice" } } })
await orders.list({ where: { dueAt: { lt: "2026-10-01" } } })

const open = await orders.count({ where: { status: "open" } })                // a number
const paid = await orders.stats("amount", { where: { status: "paid" } })  // { count, sum, avg, min, max }

The same grammar serves list, watch, count and stats, and a relay's when and where. Server-managed fields are refused. A where outside the grammar is 400 invalid_filter, and the message names the field and the operator.

In a live collection with fixed fields, a field's default is read by eq, ne, in and nin; the ordered operators, contains, startsWith and exists read the stored value only.

  • count({ where?, search? }) answers how many records this person could list, under the same safety rule, scope and filters, without paging them.
  • list({ count: true }) adds the total to a page: total, exact up to 10,000, or past that totalAtLeast: 10000 (show "10,000+"). It counts under the same safety rule, filters and search as the page, reads at most 10,001 records, rides the first page only (a page fetched with cursor carries none), and is refused on a since delta (400 invalid_count).
  • list({ author: "me" }) or list({ author: userId }) lists one person's records on shared, community, public_read and direct, inside the reader's own scope, so it only ever returns fewer records. On private, addressed and admin_write it is 403 author_not_visible. ownerUserId stays refused in where; author is the filter for it.
  • stats(field, { where?, search? }) reduces one numeric field over the same records. count is how many held a number there (a record whose field is missing or not a number is skipped). sum, avg, min and max are null when none did. A sum past 253 is answered as a string.
  • Neither pages its results, so since, sort, cursor, limit and expand are refused (400 invalid_count / 400 invalid_stats).
  • A stats field name starts with a letter and uses only letters, digits and underscores; anything else is 400 invalid_stats.
  • Both share list's read rate limit, and each call counts as one read for a secret key.

The grammar has no or group and no sort by a data field; search is a case-insensitive substring match over stored text; stats reduces one field per call.

Was this page helpful?

Last updated