Skip to content

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

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: string | number): Promise<T | null>

Fetches a row by primary key, or null if it doesn’t exist.

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: 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: string | number): Promise<number>

Deletes a row by primary key. Resolves to the number of rows removed (0 or 1).

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.

TypeStored asJS value
textTEXTstring
integerINTEGERnumber (safe integer)
realREALnumber
booleanINTEGER (0/1)boolean
timestampINTEGER (epoch ms)number or ISO string
jsonTEXTany JSON-serializable value

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

StatusMeaning
400Invalid input — unknown column, bad operator/type, or a malformed filter.
401No app key — pass apiKey to the Client.
402Free-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).
403The credential lacks the matching db:read / db:write scope, or the table is backend-only and you presented a publishable key.
404Table or row not found.
409Constraint violation (e.g. a unique column), or an unsupported (destructive) schema migration.
507Table at its single-instance capacity (sharding required).