client.db
client.db is your app’s scalable database. You define tables (columns +
indexes) in the Muhkoo portal; each table is backed
by its own isolated SQLite instance and reachable through a sanitized
REST gateway. This namespace wraps that gateway with a typed, ergonomic surface.
Unlike client.kv (per-user, encrypted-at-rest key/value), the
database is app-scoped, queryable, and shared across your app’s users —
authorized by the app key (the appId + environment are resolved server-side
from the key, so a key can only ever reach its own app’s tables). A table marked
backend-only rejects the browser-safe publishable
key and is reachable only with a secret key or a scoped access token.
table(name)
Section titled “table(name)”table<T extends Record<string, unknown>>(name: string): DbTable<T>Returns a handle to a table. The optional type parameter gives the row operations a typed shape:
interface Todo { _id: number; title: string; done: boolean; rank: number }const todos = client.db.table<Todo>("todos");If no column is marked as the primary key in the portal, an auto-incrementing
_id integer is added for you. Identifiers (table, column, index names) match
[A-Za-z_][A-Za-z0-9_]{0,62}; _id and anything starting _muhkoo_ are reserved.
Table methods
Section titled “Table methods”insert(values)
Section titled “insert(values)”insert(values: Partial<T>): Promise<{ row: T; id: unknown }>Inserts a row. Returns the created row and its primary-key value. Every field is validated against the table’s schema (type-checked, NOT NULL enforced); unknown columns are rejected.
const { id } = await todos.insert({ title: "Buy milk", done: false, rank: 1 });get(id)
Section titled “get(id)”get(id: string | number): Promise<T | null>Fetches a row by primary key, or null if it doesn’t exist.
query(query?)
Section titled “query(query?)”query(query?: DbQuery): Promise<{ rows: T[]; nextCursor: string | null }>Lists rows with filters, ordering, and keyset pagination. Pass the
nextCursor from a previous result back as cursor to fetch the next page.
const { rows, nextCursor } = await todos.query({ where: [{ column: "done", op: "eq", value: false }], orderBy: { column: "rank", dir: "asc" }, limit: 50,});interface DbQuery { select?: string[]; // columns to return; omit for all where?: DbWhereCondition[]; // AND-combined orderBy?: { column: string; dir?: "asc" | "desc" }; limit?: number; // clamped server-side to 100 cursor?: string; // a previous result's nextCursor}
interface DbWhereCondition { column: string; op: DbFilterOp; value: unknown;}Operators (DbFilterOp): eq, neq, gt, gte, lt, lte, in
(value is an array), like (raw SQL LIKE pattern on a text column),
likeStartsWith / likeContains (the server builds and escapes the pattern for
you). Range operators (gt/gte/lt/lte) apply to integer, real, and
timestamp columns; the like family applies to text columns.
update(id, values)
Section titled “update(id, values)”update(id: string | number, values: Partial<T>): Promise<{ row: T }>Updates a row by primary key and resolves to the updated row. You can’t change the primary key.
delete(id)
Section titled “delete(id)”delete(id: string | number): Promise<number>Deletes a row by primary key. Resolves to the number of rows removed (0 or 1).
Offline behavior
Section titled “Offline behavior”When offline sync is on (the default in browsers), the table
handle caches rows and query results locally and queues writes while the network
is down. Reads fall back to the cache; insert returns a provisional client id
until the write syncs, and update / delete apply optimistically. A response
from the server (any HTTP error) is never swallowed: only a failure to reach the
server triggers the offline path.
Column types
Section titled “Column types”| Type | Stored as | JS value |
|---|---|---|
text | TEXT | string |
integer | INTEGER | number (safe integer) |
real | REAL | number |
boolean | INTEGER (0/1) | boolean |
timestamp | INTEGER (epoch ms) | number or ISO string |
json | TEXT | any JSON-serializable value |
Safety
Section titled “Safety”The REST gateway is the only way to reach the database — your front end never
talks to the database directly. All input is bound as parameters (never string-concatenated),
column and operator names are validated against the table’s schema, and there is
no raw-SQL passthrough. Results are bounded (limit is capped; pagination is
keyset, not offset).
Errors
Section titled “Errors”| Status | Meaning |
|---|---|
400 | Invalid input — unknown column, bad operator/type, or a malformed filter. |
401 | No app key — pass apiKey to the Client. |
402 | Free-tier database quota exceeded (rows read/written or storage). A write refused because the app takes paid accounts only, or its owner’s payment is past due, also answers 402; the SDK throws PaymentRequiredError for those (check err.code). |
403 | The credential lacks the matching db:read / db:write scope, or the table is backend-only and you presented a publishable key. |
404 | Table or row not found. |
409 | Constraint violation (e.g. a unique column), or an unsupported (destructive) schema migration. |
507 | Table at its single-instance capacity (sharding required). |