SQLite: Key-Value Store
Section titled “SQLite: Key-Value Store”Use these endpoints to list the keys in a SQLite key-value store. The store runs in one of three modes: regular (a single .db file), directory mode (a directory of files), or time-travel via at_timestamp. The response envelope differs by mode; the per-field descriptions note which mode each field applies to.
List keys
Section titled “List keys”GET /api/v1/sqlite/kv
Section titled “GET /api/v1/sqlite/kv”List all keys in the KV store with optional filtering and pagination.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
db | query | string | Yes | Database file path or directory |
table | query | string | No | Custom table name. Default: "kv_store" |
prefix | query | string | No | Filter keys by prefix |
limit | query | integer | No | Maximum number of results. Values above 1000 are treated as 1000; a negative value is rejected with 400. 0 returns an empty page, except with at_timestamp, where it means no limit. Default: 100 |
offset | query | integer | No | Skip N results for pagination. Honoured for both the regular listing and the at_timestamp listing; results are ordered by key in both cases. A negative value is rejected with 400. Default: 0 |
after | query | string | No | Cursor: list only keys that sort after this one (byte order, exclusive). Pass the previous page’s next_after; a key written or deleted elsewhere between two pages never makes another key be skipped or repeated. The value is used as given, so any string works, including a key that no longer exists. Combined with offset greater than 0 or with at_timestamp it is rejected with 400 |
at_timestamp | query | integer | No | Unix timestamp for time-travel LIST: returns the keys as they stood at that moment, in a different response envelope. 0 or omitted lists the current keys; a time in the future is rejected with 400. A directory (directory mode) is rejected with 400 in this mode |
timeout | query | integer | No | Deadline for this request, in whole seconds, clamped to [1, 300]. Once it passes, a long operation stops at its next checkpoint rather than being cut off mid-step; a step already running, such as a filesystem scan or a wait for another writer, finishes first. A request that has not finished by then answers 503 REQUEST_TIMEOUT, except that a write which has already committed still returns its success. A value that is not a whole number is ignored and the default applies. This is a server-side deadline, not a client transport timeout. Default: 30 |
Response
Section titled “Response”Keys listed. The envelope depends on the mode: regular, directory mode (db names a directory) or at_timestamp; each field says which modes carry it.
{ "success": true, "count": 2, "items": [ { "key": "user:1:profile", "content_type": "application/json", "size": 256, "created_at": 1700000000, "updated_at": 1700000100, "etag": "a1b2c3d4e5f6" }, { "key": "user:2:profile", "content_type": "application/json", "size": 312, "created_at": 1700000200, "updated_at": 1700000300, "etag": "f6e5d4c3b2a1" } ], "limit": 100, "offset": 0, "has_more": false, "next_after": null, "change_cursor": "0/42"}| Field | Type | Mode | Description |
|---|---|---|---|
success | boolean | all | Always true on a 200 |
count | integer | all | Number of entries in items |
items | array | all | The page of keys. null in regular mode for an empty page |
items[].key | string | all | The key |
items[].content_type | string | all | The content type. In directory mode, derived from the file extension; in at_timestamp mode, the value the entry had at that timestamp |
items[].size | integer | regular, directory | Size of the stored value; in directory mode, the file size in bytes |
items[].created_at | integer | regular, directory | Creation time, Unix seconds; in directory mode, the file modification time |
items[].updated_at | integer | regular, directory | Last update time, Unix seconds; in directory mode, the file modification time |
items[].etag | string | regular | The entry’s current ETag, the same value GET /kv/{key} sends; usable in If-Match |
items[].expire_at | integer | regular | Expiry time, Unix seconds. Present only when the key has a TTL |
items[].existed_at_timestamp | boolean | at_timestamp | Always true: only keys that existed at the timestamp are listed |
items[].op_number | integer | at_timestamp | History operation number of the write that set the value seen at the timestamp; 0 when the key has no recorded history |
items[].size_estimate | integer | at_timestamp | Approximate size: the size of the recorded history entry for that write, or of the current value when the current value is reported. Not the exact size of the value |
offset | integer | all | The offset applied, as requested |
limit | integer | regular, directory | The page size applied, after the 1000 cap. Absent in at_timestamp mode |
has_more | boolean | regular, directory | True when at least one more key follows this page |
next_after | string | regular, directory | Cursor for the next page: pass it as after. The last key this page covered, or null when no key follows. With limit=0 it echoes after, null when after was not given |
change_cursor | string | regular | Change-feed cursor read in the same snapshot as items: pass it as since to GET /changes or /changes/stream to receive every write made after this page was read. null when the page was read without versions (the database was busy) |
scan_errors | integer | regular | Rows that could not be read and were left out of items. Present only when at least one row was left out |
directory_mode | boolean | directory | Always true when present |
total | integer | directory | Number of keys matching prefix before pagination |
timestamp | integer | at_timestamp | The requested at_timestamp |
has_gaps | boolean | at_timestamp | True when gap_keys is non-empty |
gap_keys | array | at_timestamp | Keys left out of items because a write made with history=false leaves their state at the timestamp unknown. null when has_gaps is false |
candidate_truncated | boolean | at_timestamp | True when the candidate-key scan stopped at its internal cap, so keys may be missing from items without appearing in gap_keys; narrow the request with prefix to get a complete listing |
Invalid request parameters.
{ "code": "INVALID_DB_PATH", "error": "The db path must be an absolute path under /hoody/databases"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INVALID_PARAMETERS | Invalid request parameters | A parameter or the request body is missing, malformed or out of range. This is the default code for a 400 that has no more specific one. | Check parameter types and values against the API specification; the error text names the problem |
INVALID_DB_PATH | Invalid database path | The db/path value is empty, relative, outside /hoody/databases (unless the server allows any absolute path), contains ? or #, escapes via a symlink, or names something that is not a regular file | Provide an absolute path under /hoody/databases, or a bare name / ./name shorthand (resolved to /hoody/databases/*.db) |
DATABASE_NOT_FOUND | Database file does not exist | The database file does not exist and this request does not create it | Check the path, create the database first, or pass create_db_if_missing=true where the operation supports it |
INVALID_SQLITE_HEADER | Not a valid SQLite database | The file exists but is empty or does not start with the SQLite header | Point db at a SQLite database file |
PATH_IS_DIRECTORY | Path is a directory | Expected a database file but the path is a directory. Only reading, writing, deleting and checking a single key and listing keys (without at_timestamp) accept a directory, given as an absolute path (directory mode) | Use a .db file path |
db is a directory outside /hoody/databases, or reaches outside it through a symlink (not sent when the server allows any absolute path).
{ "code": "FORBIDDEN", "error": "Path is outside the permitted database directory"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
FORBIDDEN | Forbidden | The request is not allowed: a path that escapes the database directory, or a request refused by the Source IP Guard (Hoody Kit programs are reached through their URLs only) | Use a path inside the permitted directory, and reach the program through its URL |
Internal server error.
{ "code": "DATABASE_ERROR", "error": "I/O error reading the database file"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INTERNAL_ERROR | Internal server error | An unexpected server-side failure. Default code for a 500 that has no more specific one; details are in the server log | Retry later; report it if it persists |
DATABASE_ERROR | Database fault | SQLite failed for a reason that is not the request: I/O error, corruption, disk full, a file that cannot be opened. The message is sanitised; details are in the server log | Check the server log, disk space and the database file; the database may need repair |
Database busy, request deadline exceeded, or server shutting down (code DATABASE_BUSY, REQUEST_TIMEOUT or SERVICE_UNAVAILABLE).
The response includes a Retry-After header (always 1).
{ "code": "DATABASE_BUSY", "error": "The database is locked by another writer"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
SERVICE_UNAVAILABLE | Service unavailable | The service could not complete the request right now. Default code for a 503 that has no more specific one | Retry after a short delay |
REQUEST_TIMEOUT | Request deadline exceeded | The request did not finish before its deadline (the timeout parameter, default 30 s); nothing was committed | Retry, or raise timeout (max 300) |
DATABASE_BUSY | Database busy | The database was locked by another writer or in use for longer than the request could wait; nothing was changed | Retry after a short delay |
SDK and cURL
Section titled “SDK and cURL”import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-sqlite-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
const response = await client.sqlite.kv.list({ db: './app.db', prefix: 'user:', limit: 50 });curl -X GET "https://{projectId}-{containerId}-sqlite-1.{server}.containers.hoody.com/api/v1/sqlite/kv?db=./app.db&prefix=user:&limit=50" \ -H "Authorization: Bearer $HOODY_TOKEN"