Raw SQL
Execute raw SQL queries directly against a collection's Durable Object SQLite database. This gives you full SQL power for complex queries, aggregations, and data operations that go beyond the standard CRUD API.
Endpoint
POST /api/sql
Headers: X-EdgeBase-Service-Key: <key>
Raw SQL requires Service Key authentication. It bypasses access rules entirely, so it is intended for server-side use only.
Raw SQL is available in all Admin SDKs.
Request
{
"namespace": "shared",
"id": "optional-instance-id",
"table": "posts",
"sql": "SELECT * FROM posts WHERE status = ? ORDER BY createdAt DESC LIMIT 10",
"params": ["published"]
}
| Field | Required | Description |
|---|---|---|
namespace | Yes | The database namespace (e.g., shared, workspace) |
id | No | The specific DO instance ID. Omit for the default instance |
table | Yes | The target table name. Used for scope validation with scoped Service Keys |
sql | Yes | The SQL statement to execute. Supports any valid SQLite syntax |
params | No | Array of bind parameters for ? placeholders |
Response
{
"rows": [
{ "id": "01abc...", "title": "Hello World", "status": "published", "createdAt": "2026-01-15T10:00:00Z" }
],
"changes": 0,
"lastRowId": 0
}
| Field | Description |
|---|---|
rows | Array of result objects (for SELECT queries) |
changes | Number of rows affected (for INSERT/UPDATE/DELETE) |
lastRowId | The rowid of the last inserted row |
Parameterized Queries
Always use ? bind parameters to prevent SQL injection:
{
"namespace": "shared",
"table": "posts",
"sql": "SELECT * FROM posts WHERE authorId = ? AND status = ?",
"params": ["user-123", "published"]
}
Never interpolate user input directly into SQL strings. Always use the params array.
Admin SDK Usage
The Admin SDK exposes a top-level sql(...) helper across every Admin SDK.
- TypeScript
- Dart
- Kotlin
- Java
- Scala
- Python
- Go
- PHP
- Rust
- C#
- Ruby
- Elixir
import { createAdminClient } from '@edgebase/admin';
const admin = createAdminClient('https://your-app.example.com', {
serviceKey: process.env.EDGEBASE_SERVICE_KEY!,
});
const rows = await admin.sql(
'shared',
undefined,
'SELECT * FROM posts WHERE status = ? ORDER BY createdAt DESC LIMIT 10',
['published']
);
import 'dart:io';
import 'package:edgebase_admin/edgebase_admin.dart';
final admin = AdminEdgeBase(
'https://your-app.example.com',
serviceKey: Platform.environment['EDGEBASE_SERVICE_KEY']!,
);
final rows = await admin.sql(
'shared',
null,
'SELECT * FROM posts WHERE status = ? ORDER BY createdAt DESC LIMIT 10',
['published'],
);
import dev.edgebase.sdk.admin.AdminEdgeBase
val admin = AdminEdgeBase(
"https://your-app.example.com",
serviceKey = System.getenv("EDGEBASE_SERVICE_KEY") ?: ""
)
val rows = admin.sql(
namespace = "shared",
query = "SELECT * FROM posts WHERE status = ? ORDER BY createdAt DESC LIMIT 10",
params = listOf("published")
)
import dev.edgebase.sdk.admin.*;
import java.util.List;
AdminEdgeBase admin = EdgeBase.admin(
"https://your-app.example.com",
System.getenv("EDGEBASE_SERVICE_KEY")
);
List<Object> rows = admin.sql(
"shared",
"SELECT * FROM posts WHERE status = ? ORDER BY createdAt DESC LIMIT 10",
List.of("published")
);
import dev.edgebase.sdk.scala.admin.AdminEdgeBase
val admin = AdminEdgeBase(
"https://your-app.example.com",
sys.env("EDGEBASE_SERVICE_KEY")
)
val rows = admin.sql(
"shared",
"SELECT * FROM posts WHERE status = ? ORDER BY createdAt DESC LIMIT 10",
Seq("published")
)
import os
from edgebase_admin import AdminClient
admin = AdminClient(
'https://your-app.example.com',
service_key=os.environ['EDGEBASE_SERVICE_KEY'],
)
rows = admin.sql(
'shared',
'SELECT * FROM posts WHERE status = ? ORDER BY createdAt DESC LIMIT 10',
['published'],
)
import (
"context"
"os"
edgebase "github.com/edgebase/sdk-go"
)
ctx := context.Background()
admin := edgebase.NewAdminClient("https://your-app.example.com", os.Getenv("EDGEBASE_SERVICE_KEY"))
rows, _ := admin.SQL(
ctx,
"shared",
"",
"SELECT * FROM posts WHERE status = ? ORDER BY createdAt DESC LIMIT 10",
[]interface{}{"published"},
)
use EdgeBase\Admin\AdminClient;
$admin = new AdminClient('https://your-app.example.com', getenv('EDGEBASE_SERVICE_KEY'));
$rows = $admin->sql(
'shared',
null,
'SELECT * FROM posts WHERE status = ? ORDER BY createdAt DESC LIMIT 10',
['published'],
);
use edgebase_admin::EdgeBase;
use serde_json::json;
let admin = EdgeBase::server(
"https://your-app.example.com",
&std::env::var("EDGEBASE_SERVICE_KEY").unwrap(),
)?;
let rows = admin.sql(
"shared",
None,
"SELECT * FROM posts WHERE status = ? ORDER BY createdAt DESC LIMIT 10",
&["published"],
).await?;
using System;
using EdgeBase.Admin;
var admin = new AdminClient(
"https://your-app.example.com",
Environment.GetEnvironmentVariable("EDGEBASE_SERVICE_KEY")!
);
var rows = await admin.SqlAsync(
"shared",
"SELECT * FROM posts WHERE status = ? ORDER BY createdAt DESC LIMIT 10",
new object[] { "published" }
);
require "edgebase_admin"
admin = EdgebaseAdmin::AdminClient.new(
"https://your-app.example.com",
service_key: ENV.fetch("EDGEBASE_SERVICE_KEY")
)
rows = admin.sql(
"shared",
"SELECT * FROM posts WHERE status = ? ORDER BY createdAt DESC LIMIT 10",
["published"]
)
alias EdgeBaseAdmin
admin =
EdgeBaseAdmin.new("https://your-app.example.com",
service_key: System.fetch_env!("EDGEBASE_SERVICE_KEY")
)
rows =
EdgeBaseAdmin.sql!(admin,
"SELECT * FROM posts WHERE status = ? ORDER BY createdAt DESC LIMIT 10",
namespace: "shared",
params: ["published"]
)
Inside App Functions
// functions/generateReport.ts
import { defineFunction } from '@edgebase/shared';
export default defineFunction({
trigger: { type: 'http', method: 'GET', path: '/report/top-authors' },
handler: async ({ admin }) => {
const result = await admin.sql(
'shared',
undefined,
`SELECT authorId, COUNT(*) as postCount, SUM(views) as totalViews
FROM posts
WHERE status = 'published'
GROUP BY authorId
ORDER BY totalViews DESC
LIMIT 20`
);
return result.rows;
},
});
Use Cases
Complex Aggregations
SELECT
strftime('%Y-%m', createdAt) AS month,
COUNT(*) AS count,
AVG(views) AS avgViews
FROM posts
WHERE status = 'published'
GROUP BY month
ORDER BY month DESC
Multi-Table Joins
SELECT p.title, p.views, u.displayName AS author
FROM posts p
JOIN users u ON p.authorId = u.id
WHERE p.status = 'published'
ORDER BY p.views DESC
LIMIT 10
Full-Text Search with Ranking
SELECT p.*, rank
FROM posts_fts
JOIN posts p ON posts_fts.rowid = p.rowid
WHERE posts_fts MATCH ?
ORDER BY rank
LIMIT 20
Data Migration
UPDATE posts SET category = 'general' WHERE category IS NULL
Limitations
No Cross-DO Queries
Each Durable Object has its own independent SQLite database. A single SQL query runs against one DO instance only. You cannot join data across different namespaces or instance IDs in a single query.
// Each call targets a single DO
const sharedPosts = await admin.sql('shared', undefined, 'SELECT * FROM posts');
const workspaceDocs = await admin.sql('workspace', 'ws-1', 'SELECT * FROM documents');
// These are two separate databases — no cross-join is possible
Security
Raw SQL bypasses all access rules. Access is controlled entirely by the Service Key:
- A root-tier key can execute any SQL on any table
- A scoped key requires the
sql:table:{table}:execscope to match thetablefield in the request
The table field in the request body is used for scope checking only. The SQL statement itself is not parsed or restricted, so a query referencing multiple tables will still execute as long as the scope matches the declared table.
Because raw SQL runs with full database privileges, treat it with the same care as direct database access. Validate inputs, use parameterized queries, and restrict Service Key distribution.