REST API
Admin API
Manage database files and read server Telemetry with the admin key.
Authentication and HTTP
The base URL is https://z8data.com. Use your own service URL for another deployment. Send Authorization: Bearer <admin-key> on every Admin request. The token is case-sensitive. The Bearer scheme is not case-sensitive. Use one space before the token, with no extra text, spaces, commas, or second key.
The admin key can create, list, get, and delete database files and read Telemetry. DB keys cannot use Admin routes. The admin key cannot query SQL. Use the DB API with a DB read/write key to create tables and change data. Both DB keys cover every database; there are no user or per-database keys.
Authentication runs before JSON parsing or database path access. Missing or invalid keys return 401 with WWW-Authenticate: Bearer. A valid key with the wrong role returns 403. Keys are never returned by the APIs or shown on the website.
Database success and handled error responses use JSON and Cache-Control: no-store. Admin routes do not set CORS allow headers. Call them from a server-side client or the same origin. See the Telemetry section for its separate error format and cache headers.
Next.js also provides OPTIONS and HEAD for these GET routes. OPTIONS needs no key and returns 204 with an Allow header: DELETE, GET, HEAD, OPTIONS, POST for databases, or GET, HEAD, OPTIONS for Telemetry and health. It sets no CORS allow headers or explicit Cache-Control header.
HEAD uses the GET handler with no response body. On the database route, authentication runs first, then an authorized HEAD returns 400 because the database handler only accepts GET, POST, and DELETE. Telemetry HEAD checks the admin key and collects or reuses a snapshot. Health HEAD returns 200. Use GET for the documented read operations. Framework 405 responses do not use the database JSON error format.
Server key settings and rotation
Set these three server-only variables in .env. The values below are placeholders:
1Z8DATA_ADMIN_KEY=<admin-key>
2Z8DATA_WRITE_KEY=<db-read-write-key>
3Z8DATA_READ_KEY=<db-read-only-key>All three values must be nonempty and different. Whitespace and commas are invalid. The service reads the values from the process environment; local Next.js loads .env and Docker Compose passes it through env_file. There are no hard-coded fallback keys. The .env file stays tracked by this project.
To rotate a key, edit .env and restart the local app. For Docker, recreate the app container with the command below. A later key change does not need an image rebuild. A plain container restart keeps its old environment.
1docker compose up -d --force-recreate appKeys are read when the server module loads and validated on the first protected request. Missing, duplicate, or malformed settings block access to all protected routes. Database routes return 500 INTERNAL_ERROR. Telemetry raises a server error before its handler runs; this configuration failure does not have a stable API response body. The public health route does not validate keys.
Database identity and URL fields
1{"dir":"prod","filename":"app.db"}The pair identifies one database. There is no generated ID or per-database key. Both strings must contain valid Unicode. Names are not trimmed or repaired. File name case follows the server filesystem.
Use / between folder names. Absolute paths, leading or trailing slashes, empty path parts, ., .., backslashes, colons, and NUL characters are rejected. A filename cannot contain a slash. Names ending in -wal, -shm, or -journal are reserved, with any letter case.
Paths must stay inside the database root. Symbolic links in database paths and hard links to database or sidecar files are rejected. Text such as ?mode=ro inside a filename stays filename text; it is not a SQLite connection option.
POST accepts exactly these two JSON fields. GET-one and DELETE use exactly one dir and one filename query field. Their order does not matter. Unknown fields, repeated fields, or only one identity field return 400 INVALID_REQUEST. List uses no query fields at all.
URL-encode query values once. The server decodes them once. For a root database, use ?dir=&filename=app.db. A plus sign in a URL query means a space; encode a literal plus as %2B. Use curl --data-urlencode for names with special characters. JSON strings are not URL-decoded.
Database response objects have only dir and filename. They include no generated ID, schema, size, timestamps, key, account data, or absolute server path. Get and list check file paths; they do not run an integrity check or inspect table schemas.
/api/admin/v1/databasesCreate one empty database and any missing parent folders.
Send Content-Type: application/json, the exact identity body, and no URL query fields. SQL and schema fields are not accepted.
The handler parses the body as JSON without checking the Content-Type header. Empty or invalid JSON returns 400 INVALID_REQUEST after authentication.
1curl -X POST 'https://z8data.com/api/admin/v1/databases' \
2 -H 'Authorization: Bearer <admin-key>' \
3 -H 'Content-Type: application/json' \
4 -d '{"dir":"prod","filename":"app.db"}'1{"database":{"dir":"prod","filename":"app.db"}}Success returns 201 only after the file is ready with SQLite protections enabled. Use a DB read/write key to create tables after this call.
Creation is exclusive. An existing target or a concurrent create for the same database returns 409 DATABASE_EXISTS. Existing SQLite sidecars also block creation. Unsafe paths or file links return 400 INVALID_DATABASE_PATH. Files are never overwritten.
Creation waits for previously accepted operations for the same identity. During creation, new queries, get requests, and deletes return 503 DATABASE_UNAVAILABLE. If deletion or failed deletion cleanup already blocks the identity, creation also returns 503. A failed create may leave newly made parent folders in place.
/api/admin/v1/databasesList available service databases, including subfolders.
1curl 'https://z8data.com/api/admin/v1/databases' -H 'Authorization: Bearer <admin-key>'1{"databases":[{"dir":"prod","filename":"app.db"}]}Success returns 200. Results are sorted by dir, then filename, using string order. An empty list is {"databases":[]}. There is no pagination or list-size cap. Do not send query fields for filtering or paging.
Listing walks the managed database root without opening or changing database files. It skips sidecars, unsafe names, file links, old storage outside the root, and identities with pending create, delete, or failed deletion cleanup. It checks paths, not SQLite file contents. The list can change after the response as other requests create or delete files.
/api/admin/v1/databases?dir=prod&filename=app.dbCheck that one database file exists at a safe path.
1curl --get 'https://z8data.com/api/admin/v1/databases' \
2 --data-urlencode 'dir=prod' --data-urlencode 'filename=app.db' \
3 -H 'Authorization: Bearer <admin-key>'1{"database":{"dir":"prod","filename":"app.db"}}Success returns 200. A missing file or parent folder returns 404 DATABASE_NOT_FOUND. An unsafe path returns 400 INVALID_DATABASE_PATH. Creation, deletion, or failed deletion cleanup returns 503 DATABASE_UNAVAILABLE. This call does not open SQLite or return rows, schema, or file contents.
/api/admin/v1/databases?dir=prod&filename=app.dbDelete one database and its SQLite sidecar files.
1curl -X DELETE 'https://z8data.com/api/admin/v1/databases?dir=prod&filename=app.db' \
2 -H 'Authorization: Bearer <admin-key>'1{"deleted":true,"database":{"dir":"prod","filename":"app.db"}}Send no body. The service first blocks new work for this database. Work accepted before deletion is allowed to finish or fail. It then closes the database on every worker, clears both role caches, removes the -wal, -shm, and -journal files, and removes the main file last.
Success returns 200 only after removal. Parent folders and other databases stay in place. A missing database returns 404 DATABASE_NOT_FOUND, including a later DELETE after full removal. A concurrent create or delete returns 503 DATABASE_UNAVAILABLE. After deletion succeeds, the identity can be created again. DB queries cannot recreate the file.
If cleanup fails, the response is an error and new work stays blocked for that identity while the app runs. It is also hidden from listing. Fix the cause and retry DELETE to finish cleanup. A successful retry clears the blocked state. Deletion state is held in memory; after a restart, remaining files are checked again.
Storage, concurrency, and shutdown
The database root is .data/databases locally and /var/lib/app/databases in Docker. dir is relative to this root. Old files beside it are not imported, exposed, or deleted. Startup creates no metadata database or application tables. The old log/dataset routes are removed and do not redirect to these APIs.
Use one app process per storage folder. The service owns database creation and deletion. Do not replace or edit files externally while it runs. Filesystem case and Unicode behavior can differ between a Mac and Linux; use consistent names in requests.
Database work shares the worker admission limits: 1,000 pending tasks, 10 MiB of estimated task bytes, and a 5-second wait to start. Admin database requests can return queue errors too. Lifecycle calls can also wait for accepted work and connection cleanup, so the queue wait is not a total HTTP timeout.
Graceful shutdown stops accepting new database work, waits for accepted operations, checkpoints writable connections, and closes workers. Docker keeps database files in its named volume. After startup, data remains and memory counters start again. See the repository README for deployment and volume commands.
/api/admin/v1/telemetryRead a current snapshot of host stats, worker queue counters, and DB query traffic.
1curl 'https://z8data.com/api/admin/v1/telemetry' -H 'Authorization: Bearer <admin-key>'Send the admin key and no body. There are no filters or interval fields. The handler ignores URL query fields. Success returns 200 with the complete shape below. These numbers are examples:
1{
2 "telemetry": {
3 "sampledAt": "2026-09-07T12:00:00.000Z",
4 "host": {
5 "cpuCount": 4,
6 "cpuPercent": 12.5,
7 "memory": {"totalBytes": 8589934592, "usedPercent": 45},
8 "disk": {"totalBytes": 107374182400, "usedPercent": 20}
9 },
10 "database": {
11 "queuedTasks": 0,
12 "totalQueueWaitMs": 120,
13 "startedTasks": 40,
14 "limits": {"pendingTasks": 1000, "taskWaitMs": 5000}
15 },
16 "traffic": {"totals": {"incomingRequests": 25}}
17 }
18}All field paths below are relative to telemetry:
Host values describe what the operating system reports. They are not app-process CPU or memory usage, and disk usage is not the total size of database files. The first CPU sample compares against the time Telemetry state was first created, which may be the first DB query request.
Telemetry is always on. It collects a snapshot when requested, keeps it for 250 ms after collection, and shares an in-progress sample across concurrent requests. A cached response keeps its original sampledAt value and counters. Responses use Cache-Control: no-store; this does not disable the internal snapshot cache.
There is no stored history or backend polling loop. Counters live in memory and reset when the app process restarts. The client chooses its fetch interval. For measurements, compare fetches every 500 ms and 1,000 ms. Use wider intervals when frequent samples are not needed. Average queue wait is totalQueueWaitMs divided by startedTasks when startedTasks is above zero.
The public response contains only the fields above. It does not include SQL text, key values, database names, server paths, result data, pending task bytes, per-worker details, or per-database stats.
Telemetry uses string errors, not the database error object:
A disk stat failure alone is not a 500: both disk fields become null. Invalid server key configuration is the separate server-error case described in key settings.
/api/healthPublic process health response. No API key is required.
1curl 'https://z8data.com/api/health'1{"status":"ok"}The route returns 200 JSON with Cache-Control: no-store. It does not open databases, check disk writes, validate keys, or collect Telemetry. Query fields are ignored. It shows that the HTTP handler can respond, not that a protected database operation will succeed.
Database errors
1{"error":{"code":"DATABASE_NOT_FOUND","message":"Database not found","statementIndex":null}}DB query and Admin database errors have exactly these three fields inside error. Use code to handle an error. Messages use fixed text and omit SQL, keys, parameter values, stack traces, and server file paths. There are no partial results.
statementIndex starts at zero. It identifies the failed statement, including invalid statement input. It is null for request, auth, path, queue, database open, transaction begin or commit, and Admin errors. A rollback failure also returns a null index with INTERNAL_ERROR.
* above includes the primary code and its extended codes. A known extended code is kept, such as SQLITE_CONSTRAINT_UNIQUE. An unknown extended code falls back to its primary code. Telemetry uses the separate string error format shown on the Admin API page.