Skip to main content

Raw SQL

Execute raw SQL queries directly against a configured EdgeBase database namespace. Depending on the namespace provider, the same /api/sql endpoint routes to Durable Object SQLite, Cloudflare D1, or PostgreSQL/Neon.

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.

Language Coverage

Raw SQL is available in all Admin SDKs.

Request

{
"namespace": "shared",
"id": "optional-instance-id",
"sql": "SELECT * FROM posts WHERE status = ? ORDER BY createdAt DESC LIMIT 10",
"params": ["published"]
}
FieldRequiredDescription
namespaceYesThe database namespace (e.g., shared, workspace)
idNoDynamic database instance ID. Omit for single-instance namespaces and non-DO providers
sqlYesThe SQL statement to execute
paramsNoArray of bind parameters for ? placeholders

namespace must match a database block declared in edgebase.config.ts. For dynamic Durable Object namespaces, pass the instance id you want to target.

Response

{
"rows": [
{ "id": "01abc...", "title": "Hello World", "status": "published", "createdAt": "2026-01-15T10:00:00Z" }
],
"items": [
{ "id": "01abc...", "title": "Hello World", "status": "published", "createdAt": "2026-01-15T10:00:00Z" }
],
"results": [
{ "id": "01abc...", "title": "Hello World", "status": "published", "createdAt": "2026-01-15T10:00:00Z" }
]
}
FieldDescription
rowsCanonical array of row objects
itemsAlias of rows for SDK compatibility
resultsAlias of rows for SDK compatibility
columnsOptional PostgreSQL column metadata
rowCountOptional PostgreSQL affected-row count

Execution Targets

EdgeBase selects the backing engine from your database namespace configuration:

  • Durable Object SQLite: default for managed EdgeBase database namespaces
  • D1: namespaces configured to route through Cloudflare D1
  • PostgreSQL: namespaces configured with provider: 'postgres' (legacy provider: 'neon' configs still work)

Parameterized Queries

Always use ? bind parameters to prevent SQL injection:

{
"namespace": "shared",
"sql": "SELECT * FROM posts WHERE authorId = ? AND status = ?",
"params": ["user-123", "published"]
}
danger

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.

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

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}:exec scope to match the table field 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.

caution

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.