Skip to main content

Admin SDK

Database operations from the server using a Service Key. All operations bypass Access Rules.

Language Coverage

The server-side database API is available in all Admin SDKs.

Setup

import { createAdminClient } from '@edgebase/admin';

const admin = createAdminClient('https://my-app.edgebase.dev', {
serviceKey: process.env.EDGEBASE_SERVICE_KEY,
});

Inside App Functions, the URL and Service Key are detected from environment variables automatically:

const admin = createAdminClient();
No Per-Category Rate Limits

Admin SDK requests authenticated with a Service Key bypass EdgeBase's app-level rate limits entirely, including global. This makes Admin SDK suitable for high-throughput server-to-server operations.

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

admin.db('app')                    // single-instance DB block
admin.db('workspace', 'ws-456') // workspace-isolated DB block
admin.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 = admin.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' });

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
401UnauthorizedInvalid Service Key
403ForbiddenOperation not permitted
404Not FoundRecord or table doesn't exist
import { EdgeBaseError } from '@edgebase/core';

try {
await admin.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 admin.db('app').table('posts').insert({
title: 'Hello World',
content: 'My first post.',
status: 'published',
});
// post.id → "0192d3a4-..." (UUID v7, auto-generated)

Read

Get a single record

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

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

const ref = admin.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 admin.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 admin.db('app').table('posts')
.orderBy('createdAt', 'desc')
.limit(20)
.getList();

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

Update

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

Field Operators

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

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

Delete

await admin.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 admin.db('app').table('posts')
.or(q => q.where('status', '==', 'draft').where('authorId', '==', userId))
.getList();

// AND + OR combined
const results = await admin.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 admin.db('app').table('posts')
.where('status', '==', 'published')
.getList();

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

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

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

Sorting

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

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

Pagination

Offset Pagination

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

// Using page (alias for offset-based pagination)
const page3 = await admin.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 admin.db('app').table('posts')
.limit(20)
.getList();

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

// Previous page using cursor (backward)
const prevPage = await admin.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 admin.db('app').table('posts').count();
// total → 150

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

Batch Operations

insertMany

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

const posts = await admin.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:

Go Coverage

The Go Admin SDK documents insertMany, but updateMany and deleteMany are not exposed on the current Go TableRef yet, so those tabs are intentionally omitted here.

const result = await admin.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 admin.db('app').table('posts')
.where('status', '==', 'archived')
.deleteMany();
// result.totalProcessed → 15, result.totalSucceeded → 15

upsertMany

Batch upsert — insert or update multiple records atomically:

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

You can also upsert by a unique field using conflictTarget:

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


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


Raw SQL (App Functions Only)

Inside App Functions, you can execute raw SQL using admin.sql():

// functions/analytics.ts
import { defineFunction } from '@edgebase/shared';

export default defineFunction({
trigger: { type: 'http', path: '/api/functions/analytics-top-authors', method: 'GET' },
handler: async (context) => {
const topAuthors = await context.admin.sql(
'posts',
'SELECT authorId, COUNT(*) as postCount FROM posts WHERE status = ? GROUP BY authorId ORDER BY postCount DESC LIMIT ?',
['published', 10]
);
return Response.json(topAuthors);
},
});
Alternative API

The db.sql tagged template is also available as an alternative syntax. Both forms are fully supported:

// Tagged template
const rows = await context.admin.db('app').sql`SELECT * FROM posts WHERE status = ${'published'}`;

// Parameterized (recommended for dynamic queries)
const rows = await context.admin.sql('posts', 'SELECT * FROM posts WHERE status = ?', ['published']);