User data

Records owned by one end user. Create, read, search, update and delete — plus the two features that make a schemaless store safe to build on: versions and batches.

Every list is bounded

There is no “fetch everything” call. A list returns 20 records by default and 100 at most, with a cursor for the next page. A larger limit is clamped, not rejected. Use the iter_* generators when you genuinely need every record.

add_user_dataSession cookie

POST /api/users/:id/data

authix.add_user_data(
    *, user_id: str, data_category: str, data: dict,
    parent_id: str | None = None,
) -> dict
ParamTypeRequiredDescription
user_idstrYesWho owns the record.
data_categorystrYesA namespace, not a schema — 'notes', 'orders'. Lower-cased by the server and fixed on create.
datadictYesAny JSON-serialisable payload.
parent_idstrNoParents this record to another, giving order → line-items without a join table.

Returns: dict — the created record

Python
1record = authix.add_user_data(
2    user_id=user_id,
3    data_category="orders",
4    data={"title": "Order #1042", "status": "paid", "total": 40},
5)
6
7print(record["data"]["id"])

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

get_user_dataSession cookie

GET /api/users/:id/data

authix.get_user_data(
    *, user_id: str, category: str | None = None,
    parent_id: str | None = None,
    limit: int | None = None, cursor: str | None = None,
) -> Page
ParamTypeRequiredDescription
user_idstrYesWhose records to list.
categorystrNoRestrict to one category.
parent_idstrNoOnly the children of this record.
limitintNo1–100. Defaults to 20; larger values are clamped.
cursorstrNonext_cursor from the previous page.

Returns: Page

Python
1page = authix.get_user_data(user_id=user_id, category="orders", limit=50)
2
3print(len(page), page.has_more)

Newest first, ordered by (created_at, id) — which is what makes the cursor stable while records are being inserted.

iter_user_dataSession cookie

GET /api/users/:id/data (paged)

authix.iter_user_data(
    *, user_id: str, category: str | None = None,
    limit: int | None = None, max_pages: int | None = None,
) -> Iterator[dict]
ParamTypeRequiredDescription
user_idstrYesWhose records to walk.
categorystrNoRestrict to one category.
limitintNoPage size while iterating. Larger pages mean fewer round trips.
max_pagesintNoSafety valve for a dataset of unknown size.

Returns: Iterator[dict]

Python
1for record in authix.iter_user_data(user_id=user_id, category="orders"):
2    process(record)

A generator: records are yielded as pages arrive, so memory stays flat. It also stops when either has_more is false or next_cursor is missing — hand-rolled loops that only check has_more can refetch page one forever.

get_single_user_dataSession cookie

GET /api/users/:id/data/:dataId

authix.get_single_user_data(*, user_id: str, data_id: str) -> dict
ParamTypeRequiredDescription
user_idstrYesWho owns it.
data_idstrYesThe record id.

Returns: dict

Python
1record = authix.get_single_user_data(user_id=user_id, data_id=data_id)
2print(record["data"]["version"])   # needed for a safe update

Throws: NotFoundError (404) if the record does not exist or is not owned by this user.

search_in_user_dataSession cookie

GET /api/users/:id/data/search

authix.search_in_user_data(
    *, user_id: str, q: str | None = None, category: str | None = None,
    keys: dict | None = None,
    limit: int | None = None, cursor: str | None = None,
) -> Page
ParamTypeRequiredDescription
user_idstrYesWhose records to search.
qstrNoFree-text match across the record's text.
categorystrNoRestrict to one category.
keysdictNoExact-match filter on payload fields, e.g. {'status': 'paid'}.
limitintNo1–100.
cursorstrNoFor the next page.

Returns: Page

Python
1page = authix.search_in_user_data(
2    user_id=user_id,
3    category="orders",
4    keys={"status": "paid"},
5    limit=100,
6)

A keys filter that returns 12 rows beats paging 4,000 to find them. Filter before you reach for a bigger page.

update_user_dataSession cookie

PUT /api/users/:id/data/:dataId

authix.update_user_data(
    *, user_id: str, data_id: str, data: dict,
    version: int | None = None,
) -> dict
ParamTypeRequiredDescription
user_idstrYesWho owns the record.
data_idstrYesWhich record.
datadictYesFields to merge in.
versionintNoThe version you read. Omit and the last write silently wins.

Returns: dict

Python
1authix.update_user_data(
2    user_id=user_id,
3    data_id=data_id,
4    data={"status": "shipped"},
5    version=record["version"],
6)

Throws: VersionConflictError (409) if someone wrote first. Nothing is saved, and the exception carries current_version.

Passing version turns a silent overwrite into an error you can act on. Omit it only for records a single device touches.

delete_user_dataSession cookie

DELETE /api/users/:id/data/:dataId

authix.delete_user_data(*, user_id: str, data_id: str) -> dict
ParamTypeRequiredDescription
user_idstrYesWho owns it.
data_idstrYesWhich record.

Returns: dict

Python
1authix.delete_user_data(user_id=user_id, data_id=data_id)

Also deletes any records parented to this one. Permanent.

delete_many_user_dataSession cookie

POST /api/users/:id/data/delete-many

authix.delete_many_user_data(*, user_id: str, data_ids: list[str]) -> dict
ParamTypeRequiredDescription
user_idstrYesWho owns them.
data_idslist[str]YesUp to 100 ids.

Returns: dict

Python
1authix.delete_many_user_data(user_id=user_id, data_ids=stale_ids[:100])

One transaction: all of them go, or none do.

batchSession cookie

POST /api/users/:id/data/batch

authix.batch(*, user_id: str, operations: list[dict]) -> dict
ParamTypeRequiredDescription
user_idstrYesWhose records the operations apply to.
operationslist[dict]YesUp to 50 operations, each with type 'create', 'update' or 'delete'.

Returns: dict

Python
1authix.batch(
2    user_id=user_id,
3    operations=[
4        {"type": "create", "dataCategory": "orders", "total": 40},
5        {"type": "update", "id": stock_id, "version": 3, "remaining": 9},
6        {"type": "delete", "id": draft_id},
7    ],
8)

Throws: VersionConflictError or NotFoundError aborts the whole batch — nothing is applied.

This is the answer to 'these two records must change together, or neither should'. An order can never be recorded without the stock deduction that belongs with it.

Walking a dataset

Python
1# One page at a time — you hold the cursor.
2page = authix.get_user_data(user_id=user_id, category="notes", limit=50)
3
4for note in page:          # a Page iterates its own records
5    print(note["title"])
6
7if page.has_more:
8    nxt = authix.get_user_data(
9        user_id=user_id, category="notes", limit=50, cursor=page.next_cursor,
10    )
11
12# Or let the SDK follow the cursor. iter_* is a generator, so memory stays
13# flat no matter how many records exist.
14for note in authix.iter_user_data(user_id=user_id, category="notes"):
15    process(note)
16
17# Bound it when the size is unknown.
18for note in authix.iter_user_data(user_id=user_id, max_pages=10):
19    process(note)

Retrying a version conflict

Python
1from neuctra_authix import VersionConflictError
2
3
4def update_with_retry(authix, user_id, data_id, change, attempts=3):
5    """Re-read, reapply, retry.
6
7    Resending the original payload with the new version would overwrite the
8    other writer's change — which is exactly what the version check exists to
9    prevent. The change has to be recomputed from the record just re-read.
10    """
11    for _ in range(attempts):
12        current = authix.get_single_user_data(user_id=user_id, data_id=data_id)
13        record = current["data"]
14
15        try:
16            return authix.update_user_data(
17                user_id=user_id,
18                data_id=data_id,
19                data=change(record),
20                version=record["version"],
21            )
22        except VersionConflictError:
23            continue  # someone wrote first; loop and reapply
24
25    raise RuntimeError(f"gave up after {attempts} conflicting writes")

Reapply, do not replay

A 409 is the one error that is routinely correct to retry — but only after re-reading. Resending the original payload with the new version number overwrites the other writer's change, which is precisely what the version check existed to prevent.

Two or three attempts is plenty. Beyond that the record is genuinely contended, and the right answer is usually a batch or a rethink of the write path.

Related