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| Param | Type | Required | Description |
|---|---|---|---|
| user_id | str | Yes | Who owns the record. |
| data_category | str | Yes | A namespace, not a schema — 'notes', 'orders'. Lower-cased by the server and fixed on create. |
| data | dict | Yes | Any JSON-serialisable payload. |
| parent_id | str | No | Parents this record to another, giving order → line-items without a join table. |
Returns: dict — the created record
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| Param | Type | Required | Description |
|---|---|---|---|
| user_id | str | Yes | Whose records to list. |
| category | str | No | Restrict to one category. |
| parent_id | str | No | Only the children of this record. |
| limit | int | No | 1–100. Defaults to 20; larger values are clamped. |
| cursor | str | No | next_cursor from the previous page. |
Returns: Page
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]| Param | Type | Required | Description |
|---|---|---|---|
| user_id | str | Yes | Whose records to walk. |
| category | str | No | Restrict to one category. |
| limit | int | No | Page size while iterating. Larger pages mean fewer round trips. |
| max_pages | int | No | Safety valve for a dataset of unknown size. |
Returns: Iterator[dict]
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
| Param | Type | Required | Description |
|---|---|---|---|
| user_id | str | Yes | Who owns it. |
| data_id | str | Yes | The record id. |
Returns: dict
1record = authix.get_single_user_data(user_id=user_id, data_id=data_id)
2print(record["data"]["version"]) # needed for a safe updateThrows: 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| Param | Type | Required | Description |
|---|---|---|---|
| user_id | str | Yes | Whose records to search. |
| q | str | No | Free-text match across the record's text. |
| category | str | No | Restrict to one category. |
| keys | dict | No | Exact-match filter on payload fields, e.g. {'status': 'paid'}. |
| limit | int | No | 1–100. |
| cursor | str | No | For the next page. |
Returns: Page
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| Param | Type | Required | Description |
|---|---|---|---|
| user_id | str | Yes | Who owns the record. |
| data_id | str | Yes | Which record. |
| data | dict | Yes | Fields to merge in. |
| version | int | No | The version you read. Omit and the last write silently wins. |
Returns: dict
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
| Param | Type | Required | Description |
|---|---|---|---|
| user_id | str | Yes | Who owns it. |
| data_id | str | Yes | Which record. |
Returns: dict
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
| Param | Type | Required | Description |
|---|---|---|---|
| user_id | str | Yes | Who owns them. |
| data_ids | list[str] | Yes | Up to 100 ids. |
Returns: dict
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
| Param | Type | Required | Description |
|---|---|---|---|
| user_id | str | Yes | Whose records the operations apply to. |
| operations | list[dict] | Yes | Up to 50 operations, each with type 'create', 'update' or 'delete'. |
Returns: dict
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
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
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