Data API
Data API is a Firebase-style document database service that allows third-party applications to read and write structured JSON data via HTTP requests. Each application can create multiple Collections, each containing multiple Documents.
Overview
| Item | Description |
|---|---|
| Base Path | /data/api/ |
| Authentication | Basic (app mode) or Bearer (user mode) |
| Data Format | JSON |
| Document Size Limit | 512 KB |
| Pagination Limit | Up to 100 items per page |
Authentication
Data API supports two authentication modes, distinguished by the Authorization header:
App Mode (Basic)
Authenticate using the application's client_id and client_secret. App mode has full read/write access to all documents in a collection, regardless of permission settings.
Authorization: Basic base64(client_id:client_secret)⚠️ Security Warning: The
client_secretgrants full administrative access to your application. Never embed it in client-side applications distributed to users (game builds, desktop apps, etc.) — secrets can be extracted through reverse engineering. App mode should only be used in trusted server environments or internal development tools.Recommended Architecture: User-facing clients should use Bearer mode (user OIDC login) combined with Cloud Functions and Permission Groups for business logic.
User Mode (Bearer)
Authenticate using the user's OAuth access_token. User mode access depends on the collection's permission settings.
Authorization: Bearer <access_token>🔒 Access Note: Collections are private by default — users cannot access them via API directly. To allow client-side calls, set the collection's
visibilityto public in the developer dashboard. It is recommended to use cloud functions for data operations.
Replay Protection
All API requests must include the following headers to prevent replay attacks:
| Header | Description |
|---|---|
X-Timestamp | Current Unix timestamp (seconds), must be within ±5 minutes of server time |
X-Nonce | Random unique string, 16-64 alphanumeric characters, different for each request |
Visibility Model
Each collection is controlled by visibility (a bitmask) and publicEditOp (public edit operation) to manage user-mode access.
Collections are private by default — only app mode (Basic) and cloud functions can access them.
visibility Bitmask
| Value | Meaning |
|---|---|
0 | Private (default) — user mode cannot access |
1 | Public read-only — user mode can read documents but not write |
3 | Public read-write — user mode can read and write according to publicEditOp rules |
publicEditOp (Edit Operation)
Only effective when visibility = 3. Controls what write operations user mode is permitted to perform:
| Value | Meaning |
|---|---|
add | Only allow creating/updating documents (PUT / PATCH) |
remove | Only allow deleting documents (DELETE) |
all | Allow creating, updating, and deleting (default) |
App mode (Basic auth) is not restricted by visibility and can always read/write all documents.
Collection Management
Collections are created and managed through the developer management panel and are not exposed as public API endpoints.
Each collection can have an optional description of up to 512 characters for developer notes and purpose labels. Descriptions are visible only through management APIs, the dashboard, and developer tools; they are not returned by the public Data API.
Collection name rules:
- Must start with a letter or underscore
- May contain letters, digits, underscores, and hyphens
- Length: 1–128 characters
- Regex:
/^[a-zA-Z_][a-zA-Z0-9_-]{0,127}$/
Ordered Index Collections
Regular collections keep the legacy behavior: the full document JSON is stored in data_document.data, and the list endpoint can sort by one JSON field. For leaderboards, speedrun boards, price boards, and other high-frequency ordered lists, set the collection data capability to indexed and define one or more ordered indexes in settings.indexes.
Ordered indexes do not store the whole board in one document. The model remains “one ranked entry = one document”. The server projects configured sort fields into the internal data_document_index table. Each document still has the 512 KB document limit, while collection size can grow and ranking queries use the index projection.
Example: a generic partitioned best-score ranking. One collection can hold many independent rankings split by groupId + variant:
{
"indexes": [
{
"name": "best_score",
"kind": "ordered",
"partition": [
{ "path": "groupId", "type": "string" },
{ "path": "variant", "type": "string" }
],
"fields": [
{ "path": "score", "type": "int", "order": "desc" },
{ "path": "completedAt", "type": "int", "order": "asc" }
]
}
],
"writePolicy": {
"mode": "keepBest",
"index": "best_score"
}
}Field reference:
| Field | Description |
|---|---|
indexes[].name | Index name used by query APIs |
indexes[].kind | Currently supports ordered |
indexes[].partition | Optional partition fields. Passing the same partition in queries creates an independent ranking scope |
fields[].path | Sort path. Defaults to document.data; also supports data.xxx, documentId, ownerUid, createdAt, and updatedAt |
fields[].type | int, number, string, or boolean |
fields[].order | asc or desc |
fields[].decimals | Decimal scaling precision for number, 0–6 |
fields[].required | Defaults to true. Set false to place missing values by missing |
writePolicy.mode | overwrite or keepBest |
writePolicy.index | Ordered index used by keepBest to decide whether new data ranks higher |
keepBest applies to PUT / PATCH / $data.set / $data.update / management run scripts. If the new data does not rank ahead of the old document under the configured index, the server keeps the old document and returns kept: true. Single-document edits in the developer panel are treated as maintenance writes and rebuild the index directly.
Normalize important sort values such as time, score, or price as integers when practical to avoid floating-point sort drift.
Management Run Command
The developer management panel includes a Run Command tool for running SQL-like commands or embedded JavaScript scripts against an application's database. This uses the management API and requires a logged-in user with data edit permission for the application. It is not a public Data API endpoint and cannot be called with a Basic app secret or user Bearer token.
Each operation can run in one of two modes:
| Mode | Behavior |
|---|---|
preview | Computes the affected rows and returns before / after changes without writing to the database |
execute | Writes to the database and returns the affected count plus the change list |
Important response fields:
| Field | Description |
|---|---|
affected | Number of matched or modified documents |
result | Command or script return value |
changes | Write diff list with operation, collection, documentId, before, and after |
logs | Captured console.log/info/warn/error output from scripts |
maxRows limits how many documents can be scanned or modified in one run. The default is 100; the maximum is 5000.
Command Mode
Command mode is not full SQL and never executes raw SQL. It supports a safe document-oriented subset:
SELECT * FROM players WHERE level >= 10 LIMIT 20;
UPDATE players SET reviewed = true, season = 'S1' WHERE score > 1000 LIMIT 100;
DELETE FROM players WHERE banned = true LIMIT 50;Supported condition operators:
| Syntax | Meaning |
|---|---|
= | Equals |
!= / <> | Not equals |
> / >= | Greater than / greater than or equal |
< / <= | Less than / less than or equal |
Field names point to top-level fields in document data by default. You can also use documentId, ownerUid, or nested paths such as data.profile.level. UPDATE SET can only modify document data fields and cannot change documentId.
JS Script Mode
Scripts run in a controlled VM and can use the built-in db object and console. Return values must be returned with return:
const docs = await db.select('players', {
where: { level: { $gte: 10 } },
limit: 100
})
for (const doc of docs) {
await db.update('players', {
where: { documentId: doc.documentId },
patch: { reviewed: true }
})
}
console.log('reviewed', docs.length)
return { reviewed: docs.length }Available methods:
| Method | Description |
|---|---|
db.select(collection, { where, limit, offset, sortBy, sortOrder }) | Reads documents and returns documentId, ownerUid, data, and timestamps |
db.update(collection, { where, patch, limit }) | Shallow merge update; patch also supports $set, $unset, $inc, $push, and $pull |
db.delete(collection, { where, limit }) | Deletes matched documents |
db.set(collection, documentId, data, { ownerUid }) | Creates or replaces one document |
db.indexTop(collection, indexName, { limit, offset, ownerUid, ownerUids, partition }) | Reads the top ordered-index entries or a filtered ranking |
db.indexAround(collection, indexName, documentId, { before, after, partition }) | Reads the ranking window around a document |
Script where objects support { field: value } and { field: { $gt: 1 } }. Operators include $eq, $ne, $gt, $gte, $lt, $lte, and $in. Field path rules are the same as command mode.
API Endpoints
List Documents
GET /data/api/{collectionName}Query Parameters:
| Parameter | Type | Description |
|---|---|---|
ownerUid | string | Optional. Filter by owner |
limit | number | Optional. Items per page, default 50, max 100 |
offset | number | Optional. Offset, default 0 |
sortBy | string | Optional. Sort by a field in data |
sortOrder | string | Optional. Sort direction: asc or desc |
where | string | Optional. JSON filter string (e.g. {"level":{"$gt":5}}) |
Response:
{
"documents": [
{
"documentId": "player_001",
"ownerUid": "R10001",
"createdAt": "1713168000",
"updatedAt": "1713168000"
}
],
"total": 42,
"limit": 50,
"offset": 0
}Note:
listdoes not return thedatafield to save bandwidth. Thedatafield would include all document JSON content and can be large. UseGET /data/api/{collectionName}/{documentId}to retrieve the full document.
List Top Entries by Ordered Index
GET /data/api/{collectionName}/indexes/{indexName}/topQuery Parameters:
| Parameter | Type | Description |
|---|---|---|
limit | number | Optional. Default 50, max 100 |
offset | number | Optional. Pagination offset |
ownerUid | string | Optional. Filter to one owner |
ownerUids | string | Optional. Comma-separated owner UID list, useful for friend rankings |
partition | JSON string | Optional partition query, such as {"groupId":"item_001","variant":"hard"} |
The response includes full data and rank:
{
"indexName": "best_score",
"documents": [
{
"rank": 1,
"documentId": "uid_001",
"ownerUid": "uid_001",
"data": {
"score": 1000000,
"label": "Entry 001",
"completedAt": 1713168000
},
"createdAt": "1713168000",
"updatedAt": "1713168000"
}
],
"total": 1,
"limit": 50,
"offset": 0
}Get My or a Document's Ranking Window
GET /data/api/{collectionName}/indexes/{indexName}/me?before=5&after=5&partition={"groupId":"item_001","variant":"hard"}
GET /data/api/{collectionName}/indexes/{indexName}/around/{documentId}?before=5&after=5&partition={"groupId":"item_001","variant":"hard"}me requires Bearer user authentication and finds the best indexed document for the current user's uid. around/{documentId} works when the target document ID is known. The response includes the target rank, target document, and the surrounding documents window.
Get Document
GET /data/api/{collectionName}/{documentId}Response:
{
"document": {
"documentId": "player_001",
"ownerUid": "R10001",
"data": { "score": 9800, "level": 15 },
"createdAt": "2026-03-10 12:00:00",
"updatedAt": "2026-03-15 08:30:00"
}
}
document.datais your application JSON payload. The outerdocumentobject carries metadata such as document ID, owner, and timestamps.
Create or Replace Document
Uses PUT semantics — replaces the document if it exists, creates it if it doesn't.
PUT /data/api/{collectionName}/{documentId}
Content-Type: application/jsonRequest Body:
{
"data": { "score": 9800, "level": 15 },
"ownerUid": "R10001"
}| Field | Type | Description |
|---|---|---|
data | object | Required. Document content (any JSON object) |
ownerUid | string | Optional. Only available in app mode to specify document owner. In user mode, automatically set to the current user |
Response:
{
"status": "ok",
"documentId": "player_001",
"created": true
}Document ID rules:
- May contain letters, digits, underscores, hyphens, colons, and dots
- Length: 1–128 characters
- Regex:
/^[a-zA-Z0-9_\-:.]{1,128}$/
Note: PUT writes the whole document. If the document already exists,
datais replaced by the new object from the request body; old fields that are not present in the newdataare removed. PUT does not fail just because a business field is missing.
Partial Update Document
Uses PATCH semantics — performs a shallow merge on the document data.
PATCH /data/api/{collectionName}/{documentId}
Content-Type: application/jsonRequest Body:
{
"data": { "level": 16 }
}Only the specified fields are updated; other fields remain unchanged. For example, the original document's score will not be affected.
If PATCH data contains a top-level field that does not exist yet, that field is added automatically. If the document itself does not exist, the API returns 404 not_found. The shallow merge only applies to top-level fields; "profile.name" is not interpreted as a nested path.
Response:
{
"status": "ok",
"documentId": "player_001"
}Delete Document
DELETE /data/api/{collectionName}/{documentId}Response:
{
"status": "ok"
}Error Responses
All errors are returned in JSON format with error and error_description fields:
{
"error": "not_found",
"error_description": "Document not found"
}Common error codes:
| HTTP Status | error | Description |
|---|---|---|
| 400 | invalid_document_id | Invalid document ID format |
| 400 | payload_too_large | Document size exceeds 512 KB limit |
| 401 | unauthorized | Missing Authorization header |
| 401 | invalid_client | Basic auth failed (invalid client_id or secret) |
| 401 | invalid_token | Bearer token is invalid or expired |
| 403 | forbidden | Insufficient permissions (e.g., user accessing a private collection, or writing to a read-only collection) |
| 403 | service_not_enabled | Data service not enabled for this application |
| 404 | not_found | Collection or document not found |
Examples
cURL — App Mode Write
# Write a document
curl -X PUT https://api.starecho.net/data/api/player_data/player_001 \
-H "Authorization: Basic $(echo -n 'your_client_id:your_client_secret' | base64)" \
-H "Content-Type: application/json" \
-d '{"data": {"score": 9800, "level": 15}, "ownerUid": "R10001"}'
# Read a document
curl https://api.starecho.net/data/api/player_data/player_001 \
-H "Authorization: Basic $(echo -n 'your_client_id:your_client_secret' | base64)"cURL — User Mode Read/Write
# User writes their own document
curl -X PUT https://api.starecho.net/data/api/player_data/my_settings \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{"data": {"theme": "dark", "language": "zh-CN"}}'
# User reads their own document
curl https://api.starecho.net/data/api/player_data/my_settings \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..."JavaScript (Fetch API)
const CLIENT_ID = 'your_client_id'
const CLIENT_SECRET = 'your_client_secret'
const BASE_URL = 'https://api.starecho.net/data/api'
// Write a document
await fetch(`${BASE_URL}/player_data/player_001`, {
method: 'PUT',
headers: {
'Authorization': `Basic ${btoa(`${CLIENT_ID}:${CLIENT_SECRET}`)}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
data: { score: 9800, level: 15 },
ownerUid: 'R10001',
}),
})
// List documents
const res = await fetch(`${BASE_URL}/player_data?limit=20&offset=0`, {
headers: {
'Authorization': `Basic ${btoa(`${CLIENT_ID}:${CLIENT_SECRET}`)}`,
},
})
const { documents, total } = await res.json()