App Data Management
Shared records that belong to the application rather than to any one user — announcements, catalogues, feature flags, pricing tables. Seven methods, all scoped by the configured appId.
1// App data belongs to the app, not to any user.
2final created = await authix.addAppData(
3 const AddAppDataParams(
4 dataCategory: 'announcements',
5 data: {'title': 'We are live', 'pinned': true},
6 ),
7);
8
9created.id; // 'ck…'
10created.version; // 0
11created.data!['title']; // 'We are live'API key only
App-wide data is readable and writable by every user of your app, so these routes are account-level: an API key or an admin dashboard session. Called with an API key they return InsufficientScopeError. Put them behind your own backend and expose only what your app needs.
The DataItem type
App-wide records and user records share one shape. Pass DataItem.fromJson as itemParser to get a typed id, version and dataCategory plus an indexer for the payload.
| Member | Type | Meaning |
|---|---|---|
| id | String | Record ID. |
| dataCategory | String | The category it was filed under. |
| version | int | Bumped on every write. Pass it back on update for optimistic concurrency. |
| createdAt / updatedAt | DateTime? | Parsed timestamps, when the API sent them. |
| parentId | String? | Parent record, for nested shapes. |
| operator [] | dynamic | Read any payload field: item['title']. |
| payload | Map<String, dynamic> | The payload with the identity fields stripped out. |
| fields | Map<String, dynamic> | The full decoded record, identity fields included. |
1final page = await authix.getAppData<DataItem>(
2 const GetAppDataParams(category: 'announcements'),
3 itemParser: DataItem.fromJson,
4);
5
6for (final item in page) {
7 item.id; // typed String
8 item.version; // typed int — keep it to update safely
9 item.dataCategory; // 'announcements'
10 item['title']; // any payload field, via the [] operator
11 item.payload; // the payload without the identity fields
12 item.fields; // the full decoded record as a Map
13}Methods
addAppData
POST /app/:appId/data/:dataCategory
Future<DataItemResponse> addAppData(AddAppDataParams params)
| Param | Type | Required | Description |
|---|---|---|---|
| dataCategory | String | Yes | Category namespace — travels in the URL, not the body. |
| data | Map<String, dynamic> | Yes | The record body. |
Returns: DataItemResponse
1final created = await authix.addAppData(
2 const AddAppDataParams(
3 dataCategory: 'announcements',
4 data: {
5 'title': 'Version 2 is out',
6 'body': 'Dark mode, faster sync.',
7 'publishedAt': '2026-08-08',
8 },
9 ),
10);Throws: ConfigurationError when dataCategory is empty. PermissionDeniedError when the plan's app-data storage limit is reached.
Your map is sent as the body verbatim — no envelope, and no appId is merged in, because the app id is already in the path. In v1 both were, and ended up stored inside the record.
getAppData
GET /app/:appId/app-data[/:category]
Future<Page<T>> getAppData<T>(GetAppDataParams params, {ItemParser<T>? itemParser})| Param | Type | Required | Description |
|---|---|---|---|
| category | String? | No | Restrict to one category. Omit it to list every category. |
| limit | int? | No | Page size. Defaults to 20, capped at 100. |
| cursor | String? | No | nextCursor from the previous page. |
| itemParser | ItemParser<T>? | No | Maps each record into your own model. |
Returns: Page<T>
1// One category
2final news = await authix.getAppData(
3 const GetAppDataParams(category: 'announcements', limit: 50),
4);
5
6// Everything
7final all = await authix.getAppData(const GetAppDataParams());Cursor-paginated, newest first. In v1 this returned the whole list — an app with a large catalogue would load all of it into memory.
iterateAppData
GET /app/:appId/app-data (repeated)
Stream<T> iterateAppData<T>(GetAppDataParams params, {ItemParser<T>? itemParser, int? maxPages})| Param | Type | Required | Description |
|---|---|---|---|
| params | GetAppDataParams | Yes | Same filters as getAppData. Any cursor you set is ignored. |
| itemParser | ItemParser<T>? | No | Maps each record into your own model. |
| maxPages | int? | No | Stop after this many pages. |
Returns: Stream<T>
1await for (final item in authix.iterateAppData(
2 const GetAppDataParams(category: 'catalogue'),
3)) {
4 print(item['sku']);
5}Follows the cursor for you and yields records as they arrive.
getSingleAppData
GET /app/:appId/data/:dataId
Future<DataItemResponse> getSingleAppData({required String dataId})| Param | Type | Required | Description |
|---|---|---|---|
| dataId | String | Yes | Record ID. |
Returns: DataItemResponse
1final item = await authix.getSingleAppData(dataId: 'app_data_123');
2print(item.data!['title']);Throws: ConfigurationError when dataId is empty. NotFoundError when the record does not exist.
searchInAppData
POST /app/:appId/app-data/search
Future<Page<T>> searchInAppData<T>(SearchAppDataParams params, {ItemParser<T>? itemParser})| Param | Type | Required | Description |
|---|---|---|---|
| q | String? | No | Substring match across the whole record. |
| category | String? | No | Restrict to one category. |
| keys | Map<String, dynamic>? | No | Exact field match, e.g. {'pinned': true}. |
| limit | int? | No | Page size. Defaults to 20, capped at 100. |
| cursor | String? | No | nextCursor from the previous page. |
Returns: Page<T>
1final results = await authix.searchInAppData(
2 const SearchAppDataParams(
3 category: 'announcements',
4 q: 'dark mode',
5 keys: {'pinned': true},
6 ),
7);Every argument is optional — calling it with an empty params object searches all app data. Both filters are index-backed in Postgres.
updateAppData
PATCH /app/:appId/data/:dataId
Future<DataItemResponse> updateAppData(UpdateAppDataParams params)
| Param | Type | Required | Description |
|---|---|---|---|
| dataId | String | Yes | Record to update. |
| data | Map<String, dynamic> | Yes | Fields to write. Fields you omit are left alone. |
| version | int? | No | The version you last read. Omit it and the write always wins. |
Returns: DataItemResponse
1await authix.updateAppData(
2 const UpdateAppDataParams(
3 dataId: 'app_data_123',
4 data: {'title': 'Version 2.1 is out'},
5 version: 4,
6 ),
7);Throws: ConfigurationError when dataId is empty. VersionConflictError when version no longer matches.
Uses PATCH — the only method in the SDK that does. It is a partial update.
deleteAppData
DELETE /app/:appId/data/:dataId
Future<Map<String, dynamic>> deleteAppData(DeleteAppDataParams params)
| Param | Type | Required | Description |
|---|---|---|---|
| dataId | String | Yes | Record to delete. |
Returns: Map<String, dynamic>
1await authix.deleteAppData(
2 const DeleteAppDataParams(dataId: 'app_data_123'),
3);Permanent.
App data vs user data
| User data | App data | |
|---|---|---|
| Scoped by | userId (pinned to the session for ak_ keys) | appId from your config |
| Reachable with | API or API key | API key only |
| Listing | Cursor-paginated Page | Cursor-paginated Page |
| Update verb | PUT | PATCH |
| Category lives in | Request body | URL path |
| Atomic batches | batch() | Not available |
Related