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']);