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.

/api/admin/v1/databases
GET, POST, DELETE
The documented database methods. PUT and PATCH return the framework's 405 response.
/api/admin/v1/telemetry
GET
Reads a snapshot. POST, PUT, PATCH, and DELETE return the framework's 405 response.

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:

bash
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.

bash
1docker compose up -d --force-recreate app

Keys 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

json
1{"dir":"prod","filename":"app.db"}
dir
string · required
A folder relative to the database root. Use "" for the root or "prod/eu" for nested folders.
filename
string · required
One nonempty filename. No extension is added. A name such as app.db is enough.

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.

POST/api/admin/v1/databases

Create 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.

bash
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"}'
json
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.

GET/api/admin/v1/databases

List available service databases, including subfolders.

bash
1curl 'https://z8data.com/api/admin/v1/databases' -H 'Authorization: Bearer <admin-key>'
json
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.

GET/api/admin/v1/databases?dir=prod&filename=app.db

Check that one database file exists at a safe path.

bash
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>'
json
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.

DELETE/api/admin/v1/databases?dir=prod&filename=app.db

Delete one database and its SQLite sidecar files.

bash
1curl -X DELETE 'https://z8data.com/api/admin/v1/databases?dir=prod&filename=app.db' \
2  -H 'Authorization: Bearer <admin-key>'
json
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.

GET/api/admin/v1/telemetry

Read a current snapshot of host stats, worker queue counters, and DB query traffic.

bash
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:

json
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:

sampledAt
string
UTC ISO timestamp taken at the start of the sample.
host.cpuCount
number
Available CPU parallelism reported by Node os.availableParallelism().
host.cpuPercent
number · percent
Busy CPU time across the CPUs reported by the OS, between this sample and the previous sample. Clamped to 0–100; 0 when no time delta is available.
host.memory.totalBytes
number · bytes
Total system memory reported by Node os.totalmem().
host.memory.usedPercent
number · percent
100 × (total memory − free memory) / total memory. Zero when total memory is zero.
host.disk.totalBytes
number or null · bytes
Total capacity of the filesystem holding the app data directory. Null if filesystem stats cannot be read.
host.disk.usedPercent
number or null · percent
Used filesystem blocks as a percentage of total blocks. Zero for zero capacity, or null if filesystem stats cannot be read.
database.queuedTasks
number
Tasks currently waiting in the worker queue. Running tasks are not included.
database.totalQueueWaitMs
number · milliseconds
Total wait time for tasks that started since the coordinator was created. Rejected or expired tasks that never started do not add to it.
database.startedTasks
number
Tasks sent to workers since the coordinator was created, including tasks that later fail. Includes DB batches, Admin file work, and internal lifecycle tasks.
database.limits.pendingTasks
number · 1000
The admission limit for accepted tasks, including running tasks. It is not the current pending count.
database.limits.taskWaitMs
number · 5000
The maximum queue wait before a task starts. It is not a SQL execution timeout.
traffic.totals.incomingRequests
number
POST requests reaching the DB query handler since Telemetry state was created, including rejected auth, JSON, path, and SQL requests. Excludes OPTIONS, Admin requests, health, and docs.

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:

{"error":"Invalid API key"}
401
Missing or invalid key. Includes WWW-Authenticate: Bearer and Cache-Control: no-store.
{"error":"API key does not allow this operation"}
403
A valid DB key was used. Includes Cache-Control: no-store.
{"error":"Failed to read telemetry"}
500
Snapshot collection failed. This error response does not set an explicit Cache-Control header.

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.

GET/api/health

Public process health response. No API key is required.

bash
1curl 'https://z8data.com/api/health'
json
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

json
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.

INVALID_REQUEST
400
Invalid JSON, request fields, statement input, parameters, or more than one SQL statement in an item.
INVALID_DATABASE_PATH
400
Invalid database identity or an unsafe file path.
UNSUPPORTED_RESULT_VALUE
400
A result cannot use the supported JSON value formats. The batch is rolled back.
SQLITE_ERROR*, SQLITE_ABORT*, SQLITE_RANGE, SQLITE_MISMATCH, SQLITE_TOOBIG
400
SQLite could not run the caller's statement.
UNAUTHORIZED
401
Missing or invalid key. The response includes WWW-Authenticate: Bearer.
FORBIDDEN
403
A valid key has the wrong API role.
SQLITE_AUTH*, SQLITE_READONLY*
403
SQLite denied SQL under the key or connection rules.
DATABASE_NOT_FOUND
404
The requested database or parent folder does not exist.
DATABASE_EXISTS
409
Admin creation would replace an existing database or SQLite sidecar.
SQLITE_CONSTRAINT*
409
A constraint failed, such as UNIQUE, NOT NULL, CHECK, or a foreign key.
SQLITE_BUSY*, SQLITE_LOCKED*
503
SQLite database access is busy.
DATABASE_UNAVAILABLE
503
Creation, deletion, or failed deletion cleanup blocks new work for this database.
WORKER_QUEUE_FULL
500
The pending task count or byte budget would be exceeded.
WORKER_QUEUE_TIMEOUT
500
A task waited too long to start.
INTERNAL_ERROR
500
Invalid server key settings, storage, protection setup, rollback, or an unexpected server failure.
Other SQLite codes
500
A SQLite engine or storage failure, such as SQLITE_IOERR, SQLITE_FULL, or SQLITE_CORRUPT.

* 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.