Data
A JSON document store, like Cloud Firestore. Documents live in collections, which you create on the dashboard's Data page. Each collection has an access mode instead of security rules:
| Mode | Who reads | Who writes |
|---|---|---|
owner_only (default) |
Each user their own documents | Each user their own |
authenticated_read |
Every signed-in user | Each user their own |
public_read |
Anyone using the app, even before sign-in | Signed-in users, their own |
server_only |
Only your backend (Admin API) | Only your backend |
A document belongs to the user who created it, or to the app when your backend created it. Your backend can read and write everything through the Admin API, and the dashboard's data browser can too.
Documents
A document is {"id", "fields", "owner", "version", "created_at", "updated_at"}. fields is any JSON
object up to 1 MiB; owner is the owner's uid (null for the app's own documents); version goes up
with every write and is also the ETag header.
| Call | Does |
|---|---|
POST /data/{collection} with {"fields": {…}} |
Creates a document with a new id (201) |
PUT /data/{collection}/{id} with {"fields": {…}} |
Creates (201) or replaces (200) the document with this id |
PATCH /data/{collection}/{id} with {"fields": {…}} |
Merges: nested objects merge key by key, anything else replaces |
GET /data/{collection}/{id} |
Gets one document (404 if missing or not readable) |
GET /data/{collection} |
Lists documents you may read, newest first, in pages |
DELETE /data/{collection}/{id} |
Deletes it (204) |
Ids are 1–128 letters, digits, _ and -. Writing another user's document answers 403 permission-denied.
// Flutter
final created = await session.authorized('POST', '/data/orders', body: {
'fields': {'item': 'Soup', 'price': 5.5, 'status': 'new'},
});
final id = created['data']['id'];
await session.authorized('PATCH', '/data/orders/$id', body: {'fields': {'status': 'paid'}});
// React Native
const { data: order } = await authorized('POST', '/data/orders', { fields: { item: 'Soup', price: 5.5, status: 'new' } });
await authorized('PATCH', `/data/orders/${order.id}`, { fields: { status: 'paid' } });
// iOS
let created = try await session.authorized("POST", "/data/orders", body: ["fields": ["item": "Soup", "price": 5.5, "status": "new"]])
let id = (created["data"] as! [String: Any])["id"] as! String
_ = try await session.authorized("PATCH", "/data/orders/\(id)", body: ["fields": ["status": "paid"]])
// Android
val fields = JSONObject().put("item", "Soup").put("price", 5.5).put("status", "new")
val id = session.authorized("POST", "/data/orders", JSONObject().put("fields", fields)).getJSONObject("data").getString("id")
session.authorized("PATCH", "/data/orders/$id", JSONObject().put("fields", JSONObject().put("status", "paid")))
// Web
const { data: order } = await authorized('POST', '/data/orders', { fields: { item: 'Soup', price: 5.5, status: 'new' } });
await authorized('PATCH', `/data/orders/${order.id}`, { fields: { status: 'paid' } });
Numbers keep their kind: 5.0 comes back as 5.0, so typed clients read a double.
Safe updates
To change a document only if nobody else changed it since you read it, send its version:
If-Match: "3". If it changed, the answer is 412 data/version-mismatch: read it again and retry.
If-None-Match: * on PUT only creates, never replaces (412 data/already-exists).
Queries
POST /data/{collection}/query finds documents you may read:
{
"where": [["status", "==", "new"], ["price", ">=", 5], ["tags", "array-contains", "vegan"]],
"order_by": {"field": "price", "direction": "desc", "type": "number"},
"limit": 20
}
- Filters must all match; at most 10. Operators:
==,!=,<,<=,>,>=,in(a list of up to 30 values) andarray-contains. Fields are dot-separated paths such asaddress.city. - Values match only fields of the same kind:
"5"doesn't match5. Strings compare case-sensitively.!=skips documents that don't have the field. order_bysorts by one field of onetype(stringornumber; taken from a filter on the same field when there is one); documents without such a value are left out. Without it, newest first.- The answer is a page like the list, with
meta.next_cursor: send it back as"cursor"with the same query for the next page.
let cursor: string | null = null;
do {
const page = await authorized('POST', '/data/orders/query', { where: [['status', '==', 'new']], cursor });
page.data.forEach(show);
cursor = page.meta.next_cursor;
} while (cursor);
Limits and errors
| Code | When |
|---|---|
404 not-found |
The collection or document doesn't exist, or you may not read it |
401 auth/invalid-token |
The collection needs a signed-in user |
403 permission-denied |
Not your document, or a server_only collection |
412 data/version-mismatch, data/already-exists |
See safe updates |
413 data/document-too-large |
Over 1 MiB |
422 validation/failed |
A bad body or query; see fields |
When a user deletes their account, their documents in owner_only collections are deleted; their
documents in shared collections stay and become the app's (owner null).