Core concept

Data model

How records are stored, what the server adds to them, and the three features that make schemaless storage safe to build on: versions, parents and batches.

Two places data lives

The distinction is ownership, and it decides which credential can reach it.

User recordsApp-wide records
Owned byOne end userThe application itself
Typical useNotes, orders, preferences, uploadsCatalogues, announcements, feature flags, pricing
Reachable withPublishable or secret keySecret key only
ScopingPinned to the signed-in session for publishable callersNo per-user scoping — every user sees the same rows

What a record looks like

Your payload, plus the fields the server maintains.

Json
{
  "id": "cmsq8f2k10001na9x…",
  "dataCategory": "orders",
  "version": 3,
  "createdAt": "2026-08-12T10:04:11.284Z",
  "updatedAt": "2026-08-12T11:20:55.019Z",

  "title": "Order #1042",
  "status": "paid",
  "total": 40
}

You write only the payload:

1await authix.addUserData({
2  userId,
3  dataCategory: "orders",
4  data: { title: "Order #1042", status: "paid", total: 40 },
5});

Reserved field names

The server merges its own fields into the same object, so avoid id, dataCategory, version, createdAt, updatedAt and parentId as payload keys — yours would collide with theirs.

Categories

A namespace, not a schema.

dataCategory groups related records — orders, notes, settings. It is lower-cased by the server, set once on create, and used to filter reads. Records in the same category do not have to share a shape, which is the point of a schemaless store: adding a field is a deploy, not a migration.

The trade you are making

Nothing validates that every orders record has a total. That freedom is why this is fast to build on, and it is also why a typo in a field name becomes a silent bug. Validate payloads in your own code — a schema library at the boundary costs very little and catches exactly this.

Versions

Every write bumps a counter, and you can require it to match.

Without a version, the last write wins and the other change disappears with no error. That is fine for a record only one device touches, and wrong for anything else — two phones, a phone and a web tab, a background sync.

1try {
2  await authix.updateUserData({
3    userId,
4    dataId,
5    data: { status: "shipped" },
6    version: record.version,      // the version you read
7  });
8} catch (error) {
9  if (error.code === "VERSION_CONFLICT") {
10    // Someone else wrote first. Nothing was overwritten.
11    console.log("now at version", error.currentVersion);
12  }
13}

Pass the version you read and a competing change is rejected with a 409 instead of silently overwritten. Nothing is written, and the error carries the current version so you can re-read and retry.

Parent records

A soft relation, for shapes that are naturally nested.

Javascript
1// Create the order
2const order = await authix.addUserData({
3  userId,
4  dataCategory: "orders",
5  data: { total: 40 },
6});
7
8// Then its line items, parented to it
9await authix.addUserData({
10  userId,
11  dataCategory: "line-items",
12  parentId: order.data.id,
13  data: { sku: "A-1", qty: 2 },
14});
15
16// Read the children of one order in a single call
17const items = await authix.getUserData({
18  userId,
19  parentId: order.data.id,
20});

parentId gives you order → line-items without a join table, and deleting a parent removes its children. It is deliberately not a full relational model: there are no foreign keys across users, no cascading updates, and no query planner joining three levels for you.

Batches

When two records must change together, or neither should.

1await authix.batch({
2  userId,
3  operations: [
4    { type: "create", dataCategory: "orders", total: 40 },
5    { type: "update", id: stockId, version: 3, remaining: 9 },
6    { type: "delete", id: draftId },
7  ],
8});

All or nothing

Every operation commits, or none do. A version mismatch or a missing record aborts the whole batch — so an order can never be recorded without the stock deduction that belongs with it. Up to 50 operations per call.

What this model is not good at

Worth knowing before you build on it.

  • Multi-entity joins. There is no query that returns orders with their customer and their line items in one shot. Fetch and assemble, or denormalise on write.
  • Aggregate reporting. No GROUP BY, no SUM across a category. Compute totals as you write them, or export and aggregate elsewhere.
  • Enforced referential integrity beyond parentId. Nothing stops a record pointing at an id that no longer exists.
  • Schema-level constraints. No uniqueness on a payload field, no required columns. Enforce these in your code.

Related