Skip to main content

Client SDK

Database operations from the client (browser, mobile, game engine). All operations go through Access Rules.

Security & Triggers
  • Access Rules — Control who can read, write, and delete your data
  • DB Triggers — Run server-side code automatically on data changes

Setup

import { createClient } from '@edgebase/web';

const client = createClient('https://my-app.edgebase.dev');

Use client.db(namespace, id?) to select a DB block, then .table(name) to access a table:

client.db('app')                    // single-instance DB block
client.db('workspace', 'ws-456') // workspace-isolated DB block
client.db('user', userId) // per-user DB block

Single-instance block names are just config keys. Older docs may show shared, but app, catalog, or any other descriptive name works the same way.

TypeScript Generics

Define an interface for your table and pass it as a type parameter to table<T>(). All operations will be fully typed:

interface Post {
id: string;
title: string;
content: string;
status: 'draft' | 'published' | 'archived';
views: number;
createdAt: string;
updatedAt: string;
}

const posts = client.db('app').table<Post>('posts');

// All return types are now typed as Post
const post = await posts.getOne('post-id'); // Post
const result = await posts.getList(); // ListResult<Post>
const first = await posts.getFirst(); // Post | null

// Insert/update data is typed as Partial<Post>
await posts.insert({ title: 'Hello', content: '...', status: 'draft' });
await posts.update('post-id', { status: 'published' });

// Filters and query builder are also typed
const published = await posts
.where('status', '==', 'published')
.orderBy('createdAt', 'desc')
.limit(10)
.getList(); // ListResult<Post>

Error Handling

All SDK methods throw on failure — there is no { data, error } return pattern.

Error Structure

PropertyTypeDescription
statusnumberHTTP status code
messagestringHuman-readable error message
dataRecord<string, { code, message }>Per-field validation errors (optional)

Common Error Codes

CodeNameWhen
400Validation ErrorSchema validation failed, batch limit exceeded
401UnauthorizedMissing or expired auth token
403ForbiddenAccess Rules denied the operation
404Not FoundRecord or table doesn't exist
429Rate LimitedToo many requests (see Quotas)
import { EdgeBaseError } from '@edgebase/core';

try {
await client.db('app').table('posts').getOne('nonexistent');
} catch (error) {
if (error instanceof EdgeBaseError) {
console.error(error.status); // 404
console.error(error.message); // "Not found."
console.error(error.data); // undefined (or field errors for 400)
}
}

Insert

const post = await client.db('app').table('posts').insert({
title: 'Hello World',
content: 'My first post.',
status: 'published',
});
// post.id → "0192d3a4-..." (UUID v7, auto-generated)
insert vs upsert

insert() throws an error on duplicate ID (UNIQUE constraint). Use upsert() if you want to update the existing record instead of failing.

Upsert

Insert a new record, or update it if a record with the same ID (or unique field) already exists:

const result = await client.db('app').table('posts').upsert({
id: 'post-001',
title: 'Hello World',
status: 'published',
});
// result.action → "inserted" or "updated"

conflictTarget

By default, upsert matches on id. Use conflictTarget to match on a different unique field:

const result = await client.db('app').table('categories').upsert(
{ name: 'Tech', slug: 'tech', description: 'Technology articles' },
{ conflictTarget: 'slug' }
);

Read

Get a single record

const post = await client.db('app').table('posts').getOne('record-id');
doc() Pattern

JS, Python, Dart, Swift, Kotlin, Java SDKs also support the doc() pattern for single-record operations:

const ref = client.db('app').table('posts').doc('record-id');
await ref.get(); // Same as getOne('record-id')
await ref.update({...}); // Same as update('record-id', {...})
await ref.delete(); // Same as delete('record-id')
ref.onSnapshot(callback); // Realtime subscription for this document

Get First Match

Retrieve the first record matching query conditions. Returns null if no records match.

const user = await client.db('app').table('users')
.where('email', '==', 'june@example.com')
.getFirst();
// user → T | null

Internally calls .limit(1).getList() and returns the first item. No special server endpoint needed.

List records

const result = await client.db('app').table('posts')
.orderBy('createdAt', 'desc')
.limit(20)
.getList();

// result.items → Post[]
// result.total → 150
// result.page → 1

Update

await client.db('app').table('posts').update('record-id', {
title: 'Updated Title',
status: 'published',
});

Field Operators

import { increment, deleteField } from '@edgebase/core';

await client.db('app').table('posts').update('record-id', {
views: increment(1), // Atomic increment
tempField: deleteField(), // Set to NULL
});

Delete

await client.db('app').table('posts').delete('record-id');

Queries

Filtering

Use where() to filter records. Multiple where() calls are combined with AND. Available operators: ==, !=, >, <, >=, <=, contains, in, not in.

OR Conditions

Use .or() to combine conditions with OR logic. Conditions inside .or() are joined with OR, while multiple .where() calls remain AND. A maximum of 5 conditions are allowed inside a single .or() group.

// OR across different fields
const results = await client.db('app').table('posts')
.or(q => q.where('status', '==', 'draft').where('authorId', '==', userId))
.getList();

// AND + OR combined
const results = await client.db('app').table('posts')
.where('createdAt', '>', '2025-01-01')
.or(q => q.where('status', '==', 'draft').where('status', '==', 'archived'))
.getList();

For same-field OR, the in operator is more efficient: where('status', 'in', ['draft', 'review']).

// Simple filter
const published = await client.db('app').table('posts')
.where('status', '==', 'published')
.getList();

// Multiple filters (AND)
const myPosts = await client.db('app').table('posts')
.where('authorId', '==', currentUser.id)
.where('status', '==', 'published')
.getList();

// Contains (partial text match)
const results = await client.db('app').table('posts')
.where('title', 'contains', 'tutorial')
.getList();

// In (match any of the values)
const featured = await client.db('app').table('posts')
.where('status', 'in', ['published', 'featured'])
.getList();

Sorting

// Single sort
const latest = await client.db('app').table('posts')
.orderBy('createdAt', 'desc')
.getList();

// Multi-sort
const sorted = await client.db('app').table('posts')
.orderBy('status', 'asc')
.orderBy('createdAt', 'desc')
.getList();

Pagination

Offset Pagination

// Using offset
const page2 = await client.db('app').table('posts')
.limit(20)
.offset(20)
.getList();

// Using page (alias for offset-based pagination)
const page3 = await client.db('app').table('posts')
.page(3)
.limit(20)
.getList();

// Response: { items: [...], total: 150, page: 3, perPage: 20 }
note

page(n) and after(cursor)/before(cursor) are mutually exclusive. You cannot use both in the same query.

Cursor Pagination

For better performance with large datasets, use cursor pagination with UUID v7 keys:

const firstPage = await client.db('app').table('posts')
.limit(20)
.getList();

// Next page using cursor (forward)
const nextPage = await client.db('app').table('posts')
.limit(20)
.after(firstPage.items[firstPage.items.length - 1].id)
.getList();

// Previous page using cursor (backward)
const prevPage = await client.db('app').table('posts')
.limit(20)
.before(nextPage.items[0].id)
.getList();

// Cursor response format:
// { items: [...], cursor: "last-item-id", hasMore: true }

Count

Get the count of records without fetching them:

const total = await client.db('app').table('posts').count();
// total → 150

// With filter
const published = await client.db('app').table('posts')
.where('status', '==', 'published')
.count();

Batch Operations

insertMany

Insert multiple records in a single atomic transaction (all-or-nothing):

const posts = await client.db('app').table('posts').insertMany([
{ title: 'Post 1', status: 'published' },
{ title: 'Post 2', status: 'draft' },
{ title: 'Post 3', status: 'published' },
]);
// All succeed or all fail (single transaction)

updateMany

Update all records matching a filter condition:

const result = await client.db('app').table('posts')
.where('status', '==', 'draft')
.updateMany({ status: 'archived' });
// result.totalProcessed → 42, result.totalSucceeded → 42

deleteMany

Delete all records matching a filter condition:

const result = await client.db('app').table('posts')
.where('status', '==', 'archived')
.deleteMany();
// result.totalProcessed → 15, result.totalSucceeded → 15

upsertMany

Batch upsert — insert or update multiple records atomically:

await client.db('app').table('settings').upsertMany([
{ id: 'theme', value: 'dark' },
{ id: 'lang', value: 'ko' },
]);

You can also upsert by a unique field using conflictTarget:

await client.db('app').table('categories').upsertMany(
[
{ name: 'Tech', slug: 'tech' },
{ name: 'Science', slug: 'science' },
],
{ conflictTarget: 'slug' }
);
Transaction Behavior
  • Maximum 500 items per server batch call. The REST API returns 400 if a single request exceeds 500 items.
  • SDK auto-chunking: When insertMany receives more than 500 items, the SDK automatically splits them into 500-item chunks and sends them sequentially. Each chunk is an independent transaction, so partial failures are possible.
  • insertMany (≤ 500) — All-or-nothing (single transaction)
  • updateMany / deleteMany — Each batch is an independent transaction
  • upsertMany (≤ 500) — All-or-nothing (single transaction)
Go SDK

The Go SDK does not yet support updateMany, deleteMany, or upsertMany. Use individual Update/Delete calls in a loop, or handle batch operations via App Functions.


Realtime

Subscribe to changes in real time using onSnapshot:

Table Subscription

const unsubscribe = client.db('app').table('posts')
.where('status', '==', 'published')
.orderBy('createdAt', 'desc')
.limit(20)
.onSnapshot((event) => {
if (event.type === 'added') {
console.log('New:', event.data);
} else if (event.type === 'modified') {
console.log('Updated:', event.data);
} else if (event.type === 'removed') {
console.log('Deleted:', event.docId);
}
});

unsubscribe(); // Stop listening

Event Structure

PropertyTypeDescription
type'added' | 'modified' | 'removed'Change type
tablestringTable name
docIdstringRecord ID
dataT | nullRecord data (null for removed)
timestampstringISO 8601 timestamp

Error Handling

const removeErrorHandler = client.realtime.onError((error) => {
console.error(error.code, error.message);
// Possible codes: AUTH_TIMEOUT, AUTH_FAILED, CHANNEL_ACCESS_DENIED, ...
});

Reconnection

Auto-reconnect is enabled by default with exponential backoff (max 30s). After reconnection, all subscriptions are automatically restored.

For more details, see Realtime Subscriptions, Broadcast, and Presence.


Search across text fields using FTS5. Requires fts to be enabled on the table in your config:

// edgebase.config.ts
posts: {
schema: { /* ... */ },
fts: ['title', 'content'], // Enable FTS on these fields
}
const results = await client.db('app').table('posts')
.search('typescript tutorial')
.limit(20)
.getList();

// results.items → ranked by relevance
// results.items[0].highlight → { title: "...<mark>TypeScript</mark> <mark>Tutorial</mark>..." }

.search() can be combined with .where(), .orderBy(), and .limit() like any other query. Uses trigram tokenizer for CJK language support.

For more details, see Advanced — Full-Text Search.


Quotas & Limits

Rate Limits (per IP, 60-second window)

CategoryLimit
Database (CRUD)100 requests/min
Storage50 requests/min
Functions50 requests/min
Auth30 requests/min
Sign In10 requests/min
Sign Up10 requests/min

When exceeded, the server returns 429 Too Many Requests.

Operation Limits

LimitValue
Batch size (insertMany, upsertMany)500 items per request
SDK auto-chunkingSplits >500 items into sequential 500-item chunks
OR conditions per query5 max
Default page size20
Storage file list1,000 files max per request
Realtime server filters5 conditions max
Presence state size1 KB max
WebSocket pending connections5 per IP