This feature is in beta. Core behavior is stable, but some APIs or configuration may change before general availability.
Database
EdgeBase Database defaults to SQLite on D1 and Durable Objects, but single-instance DB blocks can also switch to Neon or PostgreSQL through Hyperdrive-backed bindings. You get CRUD, queries, full-text search, access rules, schema validation, and automatic migrations without managing a separate data layer by hand.
- Access Rules: Database Access Rules gate table reads, writes, deletes, and DB block access.
- Hooks: Table Hooks validate writes, enrich reads, and run inline side effects.
- Triggers: Database Triggers run App Function handlers after insert, update, or delete events.
- Handlers: Inline database behavior lives under
databases[namespace].tables[name].handlers.hooksinedgebase.config.ts.
Key Features
CRUD + Batch
Insert, update, delete, upsert — single or batch
Queries
Filter (where), sort (orderBy), paginate (offset or cursor-based), OR conditions
Schema Validation
Declarative schema with type checking, required fields, min/max constraints
Full-Text Search
FTS5 with trigram tokenizer — works with CJK and all languages
Subscriptions
onSnapshot subscriptions with server-side filtering and automatic delta sync
Access Rules
Deny-by-default, per-operation rules with auth and resource context
Table Hooks
Validate writes, enrich reads, and run inline side effects inside the active DB backend
Database Triggers
Run server-side code automatically on insert, update, or delete
Migrations
Lazy migration engine — automatic for additions, explicit SQL for destructive changes
Multi-Tenancy
DB blocks — single-instance or dynamic (per-ID isolation) with any name you choose
DB Blocks
EdgeBase uses DB blocks to route SQLite storage. There are two types — single-instance (one DB, no instance ID) and dynamic (instance: true, one DB per ID). The block name is a config key you choose freely; the examples below use app, user, and workspace, but any name works:
- JavaScript
- Dart/Flutter
- Swift
- Kotlin
- Java
- C#
- C++
client.db('app') // One single-instance DB block (D1 by default)
client.db('user', userId) // Per-user isolated DB block
client.db('workspace', 'ws-456') // Per-workspace isolated DB block
client.db('app'); // One single-instance DB block (D1 by default)
client.db('user', userId); // Per-user isolated DB block
client.db('workspace', 'ws-456'); // Per-workspace isolated DB block
client.db("app") // One single-instance DB block (D1 by default)
client.db("user", userId) // Per-user isolated DB block
client.db("workspace", "ws-456") // Per-workspace isolated DB block
client.db("app") // One single-instance DB block (D1 by default)
client.db("user", userId) // Per-user isolated DB block
client.db("workspace", "ws-456") // Per-workspace isolated DB block
client.db("app"); // One single-instance DB block (D1 by default)
client.db("user", userId); // Per-user isolated DB block
client.db("workspace", "ws-456"); // Per-workspace isolated DB block
client.Db("app"); // One single-instance DB block (D1 by default)
client.Db("user", userId); // Per-user isolated DB block
client.Db("workspace", "ws-456"); // Per-workspace isolated DB block
client.db("app"); // One single-instance DB block (D1 by default)
client.db("user", userId); // Per-user isolated DB block
client.db("workspace", "ws-456"); // Per-workspace isolated DB block
Each DB block is its own backing database. Tables inside the same block can JOIN each other because they share one storage engine. Single-instance blocks default to D1 for globally shared data, can explicitly use Durable Objects for single-instance SQLite, or can switch to Neon/PostgreSQL when you want centralized PostgreSQL semantics. Dynamic blocks scale out on Durable Objects — one isolated SQLite per instance ID.
If you're working locally, you can create the DB block from the Admin Dashboard at Database -> Tables -> + DB, or define it manually in config. See Create Database, Defining Tables, and Admin Dashboard Schema Editor.
Quick Example
// Define schema
app: {
tables: {
posts: {
schema: {
title: { type: 'string', required: true, max: 200 },
content: { type: 'text' },
status: { type: 'string', default: 'draft' },
},
access: {
read() { return true },
insert(auth) { return auth !== null },
update(auth, row) { return auth !== null && auth.id === row.authorId },
delete(auth) { return auth !== null && auth.role === 'admin' },
},
},
},
}
Assume client is already initialized with your platform SDK.
- JavaScript
- Dart/Flutter
- Swift
- Kotlin
- Java
- C#
- C++
const post = await client.db('app').table('posts').insert({
title: 'Hello World',
content: 'My first post.',
});
final post = await client.db('app').table('posts').insert({
'title': 'Hello World',
'content': 'My first post.',
});
let post = try await client.db("app").table("posts").insert([
"title": "Hello World",
"content": "My first post."
])
val post = client.db("app").table("posts").insert(mapOf(
"title" to "Hello World",
"content" to "My first post."
))
Map<String, Object> post = client.db("app").table("posts").insert(Map.of(
"title", "Hello World",
"content", "My first post."
));
var post = await client.Db("app").Table("posts").InsertAsync(new() {
["title"] = "Hello World",
["content"] = "My first post.",
});
auto post = client.db("app").table("posts").insert(R"({
"title": "Hello World",
"content": "My first post."
})");
Server-side database operations, raw SQL, and DB block access are available across all Admin SDKs.
Next Steps
CRUD operations, queries, batch, live subscriptions
Server-side operations with Service Key (bypasses rules)
Type mapping, auto fields, migrations, destructive change detection
Deny-by-default access control for tables
Validate writes, enrich reads, and run inline side effects
Server-side code on data changes
Upsert, full-text search
Choose Client SDK vs Admin SDK for each database capability