Reference
JavaScript client
The dabadata JavaScript client is a small library with no dependencies that wraps /v1/mutate, /v1/db and the live socket. It runs in browsers and in any runtime with a global fetch and WebSocket; elsewhere, pass your own through the options.
Writing a client for another platform (Dart, Swift, Kotlin)? Everything this client does on the wire, from token refresh timings to the realtime transport's recovery, is specified on Wire protocol, with a conformance suite that checks a port case by case.
Load it
The hosted service serves the client, ready to use, next to its types:
| URL | What |
|---|---|
https://dabadata.com/client/dist/dabadata.js | ES module: createClient, DabadataError. |
https://dabadata.com/client/dist/dabadata.iife.js | Plain script tag: sets window.dabadata = { createClient, DabadataError }. |
https://dabadata.com/client/dist/dabadata.d.ts | The TypeScript types of the ES module. |
https://dabadata.com/client/v0.2.0/dabadata.js | The same files, pinned to one released version (dabadata.iife.js and dabadata.d.ts too). See Versions and changes. |
/client/dist/ is always the newest client. For a production app, load a pinned version from /client/v<version>/: its files never change, so what you tested is what your users run. Both answer any origin (CORS), so the import works from your own site.
As an ES module:
<script type="module">
import { createClient, DabadataError } from "https://dabadata.com/client/dist/dabadata.js";
const db = createClient({ url: "https://dabadata.com", key: "pk_test_EXAMPLEexample0000000000", token: "<user token>" });
</script>With a plain script tag:
<script src="https://dabadata.com/client/dist/dabadata.iife.js?v=5bce207b"></script>
<script>
const db = dabadata.createClient({ url: "https://dabadata.com", key: "pk_test_EXAMPLEexample0000000000", token: "<user token>" });
</script>In a TypeScript or bundled project (Vite, React, Vue ...), save dabadata.js and dabadata.d.ts side by side in your source, for example as src/lib/dabadata.js and src/lib/dabadata.d.ts, and import ./lib/dabadata.js: TypeScript picks the types up from the file next to it. A server of your own serves the same files at /client/dist/ once the client is built (The client, built from source).
Migrating from livedb
dabadata was called livedb before. Pages written then keep working: the same bundles are also served as livedb.js and livedb.iife.js, the script tag also sets window.livedb, LiveDbError is another name of DabadataError, and a session stored under the old livedb.auth: key is moved to the new one. Hosted users' tokens issued under the old issuer name livedb are still accepted, so that name is reserved in auth.issuers like hosted. Switch to the new names when you next touch the page.
createClient(options)
function createClient(options: ClientOptions): DabadataClient| Option | Type | Default | Meaning |
|---|---|---|---|
url | string | required | Server base URL: https://dabadata.com, or your own server's address. A trailing slash is fine. |
key | string | null | none | The environment's publishable key (pk_live_... or pk_test_...), sent with every call. Needed on a hosted project for anonymous calls, provider JWTs and db.auth; leave it out on a self-hosted server's default project. |
storage | "local" | "memory" | "local" | Where db.auth keeps the signed-in user's tokens: localStorage or memory only. See Where the session is kept. |
token | string | null | none | A user (access) token from POST /v1/users/token. Leave it out for anonymous calls, or to use db.auth (and a stored session). |
refreshToken | string | null | none | The refresh token from the same call. With it the client renews the access token by itself (below). |
expiresAt | string | number | null | none | When token expires: the ISO string from the server, or milliseconds since the epoch. |
onTokens | (tokens: TokenSet) => void | none | Called with every new pair after a refresh, so the app can persist it. |
refreshUrl | string | <url>/v1/users/refresh | Where refreshes are sent. |
reconnectDelays | number[] | [1000, 2000, 4000, 8000, 10000] | Milliseconds to wait before each reconnect attempt. The last one repeats. |
pingInterval | number | 25000 | Milliseconds between pings on the socket. |
fetch | FetchLike | globalThis.fetch | Override for tests or runtimes without a global fetch. |
WebSocket | typeof WebSocket | globalThis.WebSocket | Override for tests or runtimes without a global WebSocket. |
The client belongs to one person at a time. With db.auth it switches by itself: a sign-in or sign-out reconnects every subscription as the new user. With tokens you pass in yourself, close() the old client when someone else signs in, or the person signs out, and create a new one.
Tokens and refreshing
User tokens live 1 hour. Give the client the whole pair your server minted - token, expiresAt and refreshToken - and it keeps itself signed in:
- Ahead of time. It refreshes 60 seconds before
expiresAt(never sooner than 5 seconds from when it schedules the refresh). - On a 401. When any HTTP call answers 401, it refreshes once and retries the call once.
- On the socket. When a socket closes before it opened while the access token is expired, or its expiry is unknown, it refreshes (once per streak of failed connects) and the reconnect uses the new token. An open socket is kept across a refresh; the next reconnect uses the new token.
- One at a time. Refreshes that would overlap share one request.
- Every new pair goes to
onTokens, so you can store it for the next visit. The old refresh token is dead the moment it is used, so always keep the newest one. - When a refresh fails with 401 (the refresh token expired, was used already or was revoked), the client drops the refresh token and the original error surfaces. Send the person back through your sign-in to get a new pair.
const saved = JSON.parse(localStorage.getItem("dabadata.tokens") || "null");
const db = createClient({
url: "https://dabadata.com",
key: "pk_live_EXAMPLEexample0000000000",
token: saved?.token,
expiresAt: saved?.expiresAt,
refreshToken: saved?.refreshToken,
// Keep the newest pair: the refresh token it replaces no longer works.
onTokens(tokens) {
localStorage.setItem("dabadata.tokens", JSON.stringify(tokens));
},
});TokenSet is the shape both /v1/users/token and /v1/users/refresh answer with: { token, expiresAt, refreshToken, refreshExpiresAt }. Treat the refresh token like a password; see the Security note on Identity and tokens. Without a refresh token the client uses the access token until it expires; after that, calls answer 401.
With a hosted project
Give the client the environment's publishable key as key. It sends it as X-Api-Key on every HTTP call (refreshes included) and as ?key= on the socket, so the server knows which project every call is for:
const db = createClient({
url: "https://dabadata.com",
key: "pk_live_EXAMPLEexample0000000000", // safe in a browser: it grants nothing by itself
token, expiresAt, refreshToken, // a signed-in user; leave out for anonymous
});Anonymous calls and provider JWTs (Google, Clerk, Auth0, ...) carry nothing that names your project, so they need the key. A token dabadata minted - from your backend, or from db.auth - knows its environment and works with or without it; with it, the two must belong to the same environment. Send the key on every client of a hosted project: it costs nothing and is what db.auth needs. Leave key out only on a self-hosted server's default project. See How a call finds its project.
The console's Keys → Snippets tab builds this code from your real keys for plain HTML, Vite / React / Vue, a Node backend, WordPress and curl.
db.auth
When the environment hosts your users' accounts, db.auth signs them up, in and out. It needs key. A sign-in stores the token pair, schedules the refresh, calls onTokens, and reconnects every open subscription as the new user, so their onRows run again with what that user may see. Signing out reconnects them as anonymous.
| Method | Resolves | Does |
|---|---|---|
signUp({ email, password, name?, redirectUrl? }) | SignUpResult | Creates the account. With email confirmation on (the default) a link is mailed: { user: null, verify: true, signupSecret }. Without, the person is signed in: { user, verify: false, signupSecret: null }. |
signIn({ email, password }) | AuthUser | Signs in. Rejects with code invalid_login, email_unverified, user_disabled or rate_limited. |
signInWithLink({ email, redirectUrl? }) | void | Mails a 15-minute sign-in link. |
providers() | AuthProviders | { providers, page }: the sign-in providers whose browser flow is on, in the server's order (["google", "github"]), and page, the hosted pages' address (https://dabadata.com/auth/prj_..._test/). Ids this client does not know are left out. |
signInWithProvider(provider, { redirectUrl? }) | void | Leaves the page for the provider, through <page><provider>?redirect=. redirectUrl must be one of the project's redirectUrls; the default is the current URL without its fragment. Call completeLink() on the page the person comes back to. Rejects with code provider_not_enabled when the provider is not in providers(), and unsupported outside a browser. |
signInWithIdToken({ provider, idToken, nonce?, name? }) | AuthUser | Signs in with an ID token your app got from Google or Apple itself (native apps and your own buttons), like signIn. provider is "google" or "apple"; nonce is the raw nonce when the token carries one; name is used only when a user is created. Rejects with code invalid_token, provider_not_enabled, email_missing, email_unverified, signup_closed, user_disabled, on_signup_failed, validation or rate_limited. |
completeLink({ url?, password?, signupSecret? }) | AuthUser | null | Finishes the link the page was opened with (url, default location.href): the hosted pages' redirect (#token=&refreshToken=...) or an app-mode link (#token=...&type=verify|magic|reset|invite). Clears the fragment. Null when there is no #token. |
resetPassword({ email, redirectUrl? }) | void | Mails a 1-hour reset link. |
signOut({ everywhere? }) | void | Revokes this session's tokens (or, with everywhere, all of the user's), forgets them locally and reconnects as anonymous. The local sign-out happens even if the server cannot be reached. |
user({ force? }) | AuthUser | null | The signed-in user, cached; force asks the server. Null when signed out or when the token is no longer valid. |
update({ name?, password?, currentPassword? }) | AuthUser | Changes the signed-in user's name (null clears it) and / or password. A new password needs currentPassword when the user has one (wrong: code invalid_login, status 403), signs every other session out, and the new token pair the server answers is stored like a sign-in, so this page stays signed in. Fires "update". |
onAuthChange(cb) | () => void | Calls cb(user, event) with event "signIn", "signOut", "refresh" or "update". Returns the function that stops it. |
invite({ email, name?, role?, redirectUrl? }), invites(), cancelInvite(id) | void, Invite[], void | Invites from the app, under the project's auth.users.invite rule (Invite people from your app). |
admin.listUsers({ query?, limit?, after? }) | UserList | For users who pass the project's auth.users.manage rule (App admins): { users, next }, newest first; each user with email, role, disabled, lastSignInAt, hasPassword, providers (the social providers linked to them). |
admin.setRole(id, role), admin.setName(id, name), admin.disable(id), admin.enable(id) | ManagedUser | Changes one user. Rejects with code manage_not_allowed, role_not_allowed (outside auth.users.manageRoles) or self_not_allowed (your own role, disabling yourself). |
admin.remove(id) | void | Deletes the user and signs them out everywhere. |
- Links. A reset or invite link sets a password: call
completeLink({ password }). Without one it rejects withcode: "password_required"and leaves the link in the URL, so you can show a password field and call it again. A dead link (link_invalid) is removed from the URL. - The sign-up secret.
signUpkeeps the answer'ssignupSecretfor 24 hours andcompleteLinksends it with the confirmation link. Only then does the password chosen at sign-up survive; a confirmation opened elsewhere signs the person in without it (why). An app that carries the secret itself passescompleteLink({ signupSecret }). - Refreshes of a
db.authsession are stored and fire"refresh". A refresh the server refuses ends the session: it is cleared and"signOut"fires.
const db = createClient({ url: "https://dabadata.com", key: "pk_test_EXAMPLEexample0000000000" });
db.auth.onAuthChange((user, event) => console.log(event, user?.email ?? "signed out"));
// Back from a mailed link or the hosted pages? Finish signing in.
await db.auth.completeLink();
if (!(await db.auth.user())) {
await db.auth.signIn({ email: "[email protected]", password: "correct horse battery" });
}// A "Continue with ..." button for each provider that is on.
const { providers } = await db.auth.providers();
for (const p of providers) {
const b = document.createElement("button");
b.textContent = `Continue with ${p}`;
b.onclick = () => db.auth.signInWithProvider(p, { redirectUrl: "https://tasks.example.com/" });
document.body.append(b);
}
// Back on https://tasks.example.com/ the completeLink() call above signs the person in.A complete page - sign up, links, sign in, sign out and a live list - is on Sign-in for your app's users; one with provider buttons is on Sign in with Google, Apple and more.
Where the session is kept
The storage option says where db.auth keeps the signed-in user's token pair:
"local"(the default):localStorage, one entry per server URL and key (dabadata.auth:<url>:<key>). The nextcreateClienton the page - after a reload, in another tab - picks the session up, as long as it is given neithertokennorrefreshTokenand the refresh token has not expired (an expired entry is removed). WherelocalStorageis missing or blocked, the client falls back to memory."memory": nothing is written; the session ends with the page. The sign-up secret is then kept in memory too, so the confirmation link must be completed by the same page.
Only db.auth sessions are stored. Tokens you pass to createClient yourself are yours to keep, with onTokens.
insert
insert<R = Row>(table: string, values: Record<string, unknown>): Promise<R>Inserts one row and resolves with it as stored - with its id, defaults, filled-in userId columns and timestamps. Like every write, it resolves with only { id } when your read rule does not let you read the row, and without the columns your column read rules hide.
const task = await db.insert("tasks", { title: "Order chairs", due: "2026-10-20" });
console.log(task.id, task.owner, task.done); // "5f1c...", "bob", falseupdate
update<R = Row>(table: string, id: string, values: Record<string, unknown>): Promise<R>Changes the given columns of one row and resolves with the row after the change.
const done = await db.update("tasks", task.id, { done: true });delete
delete(table: string, id: string): Promise<{ id: string }>Deletes one row and resolves with its id.
await db.delete("tasks", task.id);action
action<R = Row>(name: string, params?: Record<string, unknown>): Promise<{ results: { row: R | null }[]; traceId: number; traceIds: number[]; ai?: ActionAiResult }>Runs one of the definition's actions (POST /v1/actions/<name>): its steps run in one transaction with the server's rights, if you pass its allow rule. results holds one { row } per step, null where your read rule does not let you see the row.
try {
await db.action("rsvp", { event: eventId });
} catch (e) {
if (e.status === 409 && e.body.index === 0) showFull(); // step 0, the capacity check, did not hold
else if (e.status === 403) console.log(e.explanation); // "action rsvp: denied, anonymous is not authenticated"
else throw e;
}A refusal rejects with a DabadataError: 403 forbidden (the allow rule, with the explanation), 400 bad params (e.body.messages), 404 a missing row, 409 conflict with e.body.index naming the step.
An action that calls a model also resolves with ai: { status: "done", text, stopReason, usage, traceId } when the call was waited for, { status: "failed", error, traceId } when it failed after the steps committed, or { status: "queued", id } when the answer is written to a row in the background (subscribe to the row to see it arrive). Over the action's limits it rejects with 429 e.code rate_limited or ai_busy (e.body.retryAfter seconds), and an action with no steps whose call failed rejects with 502 ai_failed.
query
query<R = Row>(table: string, options?: QueryOptions): Promise<R[]>One read of the rows the caller may see.
| Option | Type | Meaning |
|---|---|---|
where | Record<string, unknown> | Column -> value (equality), or operators: $eq $ne $gt $gte $lt $lte $in $nin $like $ilike $null, and for json columns $contains (an array holding a value, an object holding a sub-object); $and / $or nest filters. See the protocol. |
orderBy | string | string[] | A declared column or created_at, or several: "due,-created_at" or ["due", "-created_at"] (- = descending). Default: created_at, newest first. |
desc | boolean | Descending order for a single orderBy column. |
limit | number | Default 100, at most 1000. |
const open = await db.query("tasks", { where: { done: false }, orderBy: "due", limit: 50 });
const soon = await db.query("events", {
where: { starts_at: { $gte: new Date().toISOString() }, tags: { $contains: "music" } },
orderBy: ["starts_at", "-created_at"],
});There is no single-row method. To fetch one row by id, call GET /v1/db/:table/:id with the same token.
view and viewRow
view<R = Row>(name: string, options?: ViewOptions): Promise<{ rows: R[]; traceId: number }>
viewRow<R = Row>(name: string, id: string, options?: { params?: Record<string, unknown> }): Promise<{ row: R; traceId: number }>Reads a declared view (GET /v1/views/<name>): the rows it lets you read, in your role's shape, computed fields included. viewRow reads one row of it by id.
| Option | Type | Meaning |
|---|---|---|
params | Record<string, unknown> | The view's params, of the types it declares. |
where | Where | A filter, as for query, on the view's filter columns only. |
orderBy, desc | string | string[], boolean | A sort on the view's sort columns. Default: the view's own. |
limit | number | Clamped to the view's max. |
const { rows } = await db.view("open_questions", { params: { round: roundId }, orderBy: "reveal_at" });
const { row } = await db.viewRow("open_questions", rows[0].id, { params: { round: roundId } });A view with joins nests each joined row under the join's name: row.question.text. JoinedRow types it, and a column the joined table's column rules hide from you is absent from the nested row as well:
import type { JoinedRow, Row } from "./lib/dabadata.js";
const { rows } = await db.view<JoinedRow<{ question: Row }>>("my_answers");
for (const a of rows) console.log(a.choice, a.question.text, a.question.answer ?? "(not revealed yet)");A refusal rejects with a DabadataError: 404 an unknown view (or a row not in it), 403 forbidden from the view's allow rule (with the explanation), 400 bad params or a where / orderBy on columns the view does not declare (e.body.messages).
subscribe
subscribe<R = Row>(target: string | { view: string; params?: Record<string, unknown> }, options?: SubscribeOptions, handlers?: SubscribeHandlers<R>): SubscriptionOpens the socket if needed, subscribes, and calls your handlers. target is a table name, or { view, params } for a view. options takes a where map, as for query (for a view: on its filter columns).
| Handler | Called with | When |
|---|---|---|
onRows(rows) | R[] | The full result set: first, again after every reconnect, and whenever the server resets the subscription (a definition change, a time flip of a whole rule, a membership change, a joined row that may let rows in, too many changes at once). Treat it as a reset. |
onChange(change) | { op, row } | Every change you may see, including rows that enter or leave your view by time with no write. op is "insert", "update" or "delete"; for deletes row may be only { id }. An update carries the whole visible row: replace the row you hold, do not merge. |
onError(error) | DabadataError | The subscription was refused (code: "forbidden", status 403) or failed (status 0, e.g. an unknown table), or there is no WebSocket to use. |
onStatus(status) | ConnectionStatus | The socket's status changed. Also called right away if the socket already exists. |
It returns a Subscription: { id, table, view?, close() } - for a view, table and view are the view's name. close() ends this subscription only; the socket stays open for the others.
const tasks = new Map();
const sub = db.subscribe("tasks", { where: { done: false } }, {
onRows(rows) {
tasks.clear();
for (const row of rows) tasks.set(row.id, row);
render();
},
onChange({ op, row }) {
if (op === "delete") tasks.delete(row.id);
else tasks.set(row.id, row); // insert, or update of a row that may be new to us
render();
},
onError(err) {
console.warn(err.explanation ?? err.message);
},
onStatus(status) {
document.body.dataset.connection = status;
},
});
// later
sub.close();A handler that throws does not break the socket; the error is rethrown on its own tick so it still shows up in the console.
db.storage
Files in the project's storage buckets. Uploads go straight to object storage through a signed URL; the server checks the bucket's rules, sizes and types first.
const file = await db.storage.upload("avatars", input.files[0], {
name: "me.png", // default: the File's name
type: "image/png", // default: the Blob's type
onProgress: ({ loaded, total }) => console.log(loaded / total),
}); // -> the row of files_avatars
const src = await db.storage.url("avatars", file.id); // public URL, or a 5-minute signed URL
const { url, expiresAt, expiresIn, publicUrl } = await db.storage.signedUrl("docs", id);
const stop = db.storage.bindImage(img, "photos", row.photo); // sets src; a fresh URL when the old one expired
const stopLink = db.storage.watchUrl("docs", id, (url) => (link.href = url)); // renews before expiry
await db.storage.remove("avatars", file.id); // -> { id }
const rows = await db.storage.list("avatars", { where: { owner: "u1" }, orderBy: "size", desc: true, limit: 20 });
db.subscribe("files_avatars", {}, { onChange: (change) => console.log(change) });onProgress reports as the bytes go out when the runtime has XMLHttpRequest (browsers); otherwise only at the start and the end. A refused call rejects with a DabadataError carrying the rule's explanation; a failed upload PUT has code: "upload_failed".
A signed URL lives 5 minutes. On a page that stays open, use bindImage for an <img> or <video> and watchUrl for anything else; both return stop() and take { onError }. See Private files on pages that stay open.
attachTo: { table, id, column } then sets a file column of that row to the new file (an update, with the row's rules); the file it replaces is deleted by the server. When the update is refused, the thrown DabadataError carries the uploaded file as file.
token, setToken and refresh
readonly token: string | null
setToken(token: string | null, expiresAt?: string | number | null): void
refresh(): Promise<TokenSet>db.tokenis the access token in use now: the one adb.authsign-in stored, or the one you passed. It changes after every refresh, so read it when you need it rather than keeping a copy. Use it to call a route the client has no method for, with the same key:fetch(url, { headers: { "X-Api-Key": key, Authorization: `Bearer ${db.token}` } }).db.setToken(token, expiresAt?)swaps the access token (a new one from your own backend, ornullto go anonymous) and reschedules the refresh. An open socket keeps the identity it opened with; the next connect uses the new token. It does not touch the refresh token or a storeddb.authsession - for those usedb.auth.db.refresh()refreshes now (overlapping calls share one request) and resolves the new pair.
transport, presence and publish
readonly transport: "inproc" | "centrifugo" | null
presence(table: string): Promise<PresenceEntry[]> // PresenceEntry = { id, name?, role }
publish(table: string, data: unknown): Promise<void>
onMessage<T>(table: string, cb: (data: T, from: PresenceEntry | null) => void): () => voiddb.transportis how live changes arrive:"inproc"(the server's own/v1/livesocket) or"centrifugo"(the realtime tier, on dabadata.com). It isnulluntil the client has asked the server (GET /v1/meta, once, on the first connect); the optionrealtimeofcreateClientcan fix it instead.db.presence(table)lists who is looking at a table now: one entry per user connected to its live lane (people who may read it). Realtime tier only (GET /v1/rt/presence).db.publish(table, data)sends an ephemeral message - typing, a cursor, "someone is editing" - to everyone on the table's live lane;db.onMessage(table, cb)receives them (this client's own too) and returns the function that stops it. Messages are never stored and never change data. Only people who may read the table get them. Realtime tier only: on a server without it,publishrejects withcode: "unsupported".
const stop = db.onMessage("tasks", (data, from) => showTyping(from?.name, data.taskId));
input.addEventListener("input", () => db.publish("tasks", { typing: true, taskId }));
console.log((await db.presence("tasks")).map((p) => p.name ?? p.id));status and close
readonly status: ConnectionStatus
close(): voiddb.status is the socket's current status. db.close() closes the socket and every subscription, cancels any pending reconnect and sets the status to "closed". Subscribing again later opens a new socket.
Errors
Every failed call rejects with a DabadataError, which extends Error:
| Field | Type | Meaning |
|---|---|---|
name | "DabadataError" | |
message | string | The explanation if there is one, else the server's message(s), else the code. |
status | number | HTTP status; 0 for network and socket errors. |
code | string? | The body's error: "forbidden", "unauthorized", "validation", "not found", and on hosted projects "plan_limit", "rate_limited", "project_deleted", ... |
explanation | string? | The rule engine's sentence, on 403s and refused subscriptions. |
traceId | number | string? | The call's row in the trace. |
body | unknown | The parsed response body, for anything else you need. |
try {
await db.delete("tasks", id);
} catch (err) {
if (err instanceof DabadataError && err.code === "forbidden") {
showToast(err.explanation); // delete tasks: denied, role "viewer" not in ["admin"]
} else {
throw err;
}
}Connection status
| Status | Meaning |
|---|---|
"closed" | No socket: before the first subscribe, or after close(). |
"connecting" | The first connection is being opened. |
"open" | Connected; subscriptions are live. |
"reconnecting" | The connection dropped; waiting for the next attempt or making it. |
The full reconnect behaviour - back-off, re-subscribing, dead-link detection - is on Live subscriptions.
Typed tables
Generate TypeScript types from your definition and pass them to createClient: a wrong table name, a misspelled column or a value of the wrong type becomes a compile error in your editor instead of a 400 from the server, and rows come back typed.
dabadata types # -> dabadata-types.ts, for the environment you use
# or, with any key and token of the environment (not anonymous):
curl -o src/dabadata-types.ts https://dabadata.com/v1/schema/types -H "Authorization: Bearer $DABADATA_SECRET_KEY"import { createClient } from "./lib/dabadata.js";
import type { Database } from "./dabadata-types.js";
const db = createClient<Database>({ url: "https://dabadata.com", key: "pk_live_..." });
const task = await db.insert("tasks", { title: "Buy milk" }); // task.title: string, task.done: boolean
await db.update("tasks", task.id, { done: true });
const open = await db.query("tasks", { where: { done: false }, orderBy: ["-created_at"] });
await db.action("rsvp", { event: "evt_1" });
db.insert("taks", { title: "x" }); // error: "taks" is not a table
db.insert("tasks", { titel: "x" }); // error: did you mean "title"?
db.insert("tasks", { title: 1 }); // error: title is a string- The file has a
row, aninsertand anupdateshape per table, the rows and params of your views, and the params of your actions. Required columns are required ininsert; optional ones, columns with a default anduserIdcolumns are not. A column with a column read rule may be absent from a row (?). - Types follow the column types: text, ids, files, dates and datetimes are strings;
intandfloatnumbers;jsonisJson; a location is{ lat, lng }. - Typed
wherekeys must be columns of the row;orderBytakes a column,"-column"or a list of them (not a comma string). - Run
dabadata typesagain after changing the definition: an outdated file shows up as compile errors, which is the point. Without<Database>the client is exactly as before. - The server still checks everything; the types only move the mistakes earlier. Swift, Kotlin and Dart types will come from the same definition when those SDKs exist.
Types
Every type is in /client/dist/dabadata.d.ts, served next to the bundle. In a TypeScript project without a checkout of dabadata, save both files side by side (for example src/lib/dabadata.js and src/lib/dabadata.d.ts) and import ./lib/dabadata.js; TypeScript reads the types from the file next to it:
curl -o src/lib/dabadata.js https://dabadata.com/client/dist/dabadata.js
curl -o src/lib/dabadata.d.ts https://dabadata.com/client/dist/dabadata.d.tsimport { createClient, DabadataError, type AuthUser, type Row } from "./lib/dabadata.js";The main ones:
type Row = {
id: string;
created_at?: string;
updated_at?: string;
created_by?: string;
[column: string]: unknown;
};
type Where = Record<string, unknown>;
interface QueryOptions { where?: Where; orderBy?: string; desc?: boolean; limit?: number }
interface SubscribeOptions { where?: Where }
interface ViewOptions { params?: Record<string, unknown>; where?: Where; orderBy?: string | string[]; desc?: boolean; limit?: number }
interface ViewTarget { view: string; params?: Record<string, unknown> }
type ConnectionStatus = "connecting" | "open" | "reconnecting" | "closed";
interface Change<R = Row> { op: "insert" | "update" | "delete"; row: R }
interface SubscribeHandlers<R = Row> {
onRows?(rows: R[]): void;
onChange?(change: Change<R>): void;
onError?(error: DabadataError): void;
onStatus?(status: ConnectionStatus): void;
}
interface Subscription { readonly id: string; readonly table: string; readonly view?: string; close(): void }
interface TokenSet { token: string; expiresAt: string; refreshToken: string; refreshExpiresAt: string }
type AuthStorage = "local" | "memory";
interface AuthUser { id: string; email: string; name: string | null; role: string; emailVerified: boolean; hasPassword: boolean; createdAt: string }
type AuthChangeEvent = "signIn" | "signOut" | "refresh" | "update";
interface ManagedUser extends AuthUser { disabled: boolean; lastSignInAt: string | null; hasPassword: boolean; providers: AuthProvider[] }
interface UserList { users: ManagedUser[]; next: string | null }
interface SignUpResult { user: AuthUser | null; verify: boolean; signupSecret: string | null }
type AuthProvider = "google" | "apple" | "microsoft" | "github" | "gitlab" | "discord" | "linkedin" | "slack";
type IdTokenProvider = "google" | "apple";
interface AuthProviders { providers: AuthProvider[]; page: string }
interface AuthApi {
signUp(input: { email: string; password: string; name?: string; redirectUrl?: string }): Promise<SignUpResult>;
signIn(input: { email: string; password: string }): Promise<AuthUser>;
signInWithLink(input: { email: string; redirectUrl?: string }): Promise<void>;
providers(): Promise<AuthProviders>;
signInWithProvider(provider: AuthProvider, input?: { redirectUrl?: string }): Promise<void>;
signInWithIdToken(input: { provider: IdTokenProvider; idToken: string; nonce?: string; name?: string }): Promise<AuthUser>;
completeLink(input?: { url?: string; password?: string; signupSecret?: string }): Promise<AuthUser | null>;
resetPassword(input: { email: string; redirectUrl?: string }): Promise<void>;
signOut(input?: { everywhere?: boolean }): Promise<void>;
user(input?: { force?: boolean }): Promise<AuthUser | null>;
onAuthChange(cb: (user: AuthUser | null, event: AuthChangeEvent) => void): () => void;
update(input: { name?: string | null; password?: string; currentPassword?: string }): Promise<AuthUser>;
invite(input: { email: string; name?: string; role?: string; redirectUrl?: string }): Promise<void>;
invites(): Promise<Invite[]>;
cancelInvite(id: string): Promise<void>;
readonly admin: {
listUsers(input?: { query?: string; limit?: number; after?: string }): Promise<UserList>;
setRole(id: string, role: string): Promise<ManagedUser>;
setName(id: string, name: string | null): Promise<ManagedUser>;
disable(id: string): Promise<ManagedUser>;
enable(id: string): Promise<ManagedUser>;
remove(id: string): Promise<void>;
};
}
type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;Versions and changes
Each released version is served at https://dabadata.com/client/v<version>/ - dabadata.js, dabadata.iife.js and dabadata.d.ts - with Cache-Control: public, max-age=31536000, immutable. A released file never changes and is never removed; a fix is a new version. /client/dist/ follows the newest version and changes with it.
- Version numbers. While the client is
0.x, the last number goes up for fixes and additions (0.2.0->0.2.1: safe to take), and the middle one for a change that can break a working app (0.2.x->0.3.0: read its entry first). - The API stays compatible with released clients. Server changes add fields and routes; an older pinned client keeps working against the current API. If a change ever has to break an older client, its entry here says so and names the first version that works.
- Pinning and testing. Load a pinned URL, run your tests against it, then change the version number in one place to upgrade. To vendor the files instead,
curlthem from the pinned folder.
0.2.0 - 19 October 2026
The first pinned release: the client as documented on this page.
- New:
db.storage.bindImageanddb.storage.watchUrlkeep private files showing on pages that stay open longer than a signed URL lives (Private files on pages that stay open). - New:
db.storage.signedUrlreturnsexpiresIn(seconds) when the server sends it. - New:
createClient<Database>()with the typesdabadata typeswrites from your definition (Typed tables). - Breaking changes: none.