Skip to main content

Config Reference

Complete reference for edgebase.config.ts.

Preferred Grammar

EdgeBase now prefers a shared access + handlers config grammar for product surfaces such as DB, storage, realtime, push, auth, and rooms.

  • Use access for allow/deny decisions.
  • Use handlers.hooks for interception points such as before*, after*, enrich, or delivery hooks.
  • Plugin-defined tables are merged into their target DB block before materialization, so they inherit the same grammar as first-party tables.
export default defineConfig({
databases: {
shared: {
tables: {
posts: {
access: {
read: () => true,
insert: (auth) => auth !== null,
},
handlers: {
hooks: {
beforeInsert: async (_auth, data) => ({ ...data, status: 'draft' }),
},
},
},
},
},
},
push: {
access: {
send: (auth) => auth !== null,
},
handlers: {
hooks: {
beforeSend: async (_auth, input) => input,
afterSend: async (_auth, input, output) => {
console.log(input.kind, output.sent);
},
},
},
},
auth: {
access: {
signIn: (_input, ctx) => !!ctx.auth,
},
handlers: {
hooks: {
enrich: async () => ({ tenantRole: 'member' }),
},
email: {
onSend: async () => undefined,
},
sms: {
onSend: async () => undefined,
},
},
},
rooms: {
game: {
access: {
metadata: (auth) => !!auth,
join: (auth) => !!auth,
action: (auth) => !!auth,
},
handlers: {
lifecycle: {
onJoin: (sender, room) => {
room.setPlayerState(sender.userId, () => ({ hp: 100 }));
},
},
actions: {
MOVE: (payload, room) => {
room.setSharedState((state) => ({ ...state, position: payload }));
},
},
},
},
},
});

Full Example

import { defineConfig } from '@edgebase/shared';

export default defineConfig({
// ─── Release Mode ──────────────────────────────────────
release: false, // Set to true before production deployment

// ─── Databases ──────────────────────────────────────────
databases: {
shared: {
tables: {
posts: {
schema: {
title: { type: 'string', required: true, min: 1, max: 200 },
content: { type: 'text' },
status: { type: 'string', enum: ['draft', 'published'], default: 'draft' },
views: { type: 'number', default: 0 },
authorId: { type: 'string', references: 'users' },
tags: { type: 'json' },
published: { type: 'datetime' },
featured: { type: 'boolean', default: false },
},
indexes: [
{ fields: ['status'] },
{ fields: ['authorId', 'createdAt'] },
{ fields: ['status', 'views'], unique: false },
],
fts: ['title', 'content'],
access: {
read(auth, row) { return true },
insert(auth) { return auth !== null },
update(auth, row) { return auth !== null && auth.id === row.authorId },
delete(auth, row) { return auth !== null && auth.id === row.authorId },
},
},
},
},
},

// ─── Authentication ───────────────────────────────────
auth: {
allowedOAuthProviders: ['google', 'github', 'apple', 'discord'],
anonymousAuth: true,
allowedRedirectUrls: [
'https://app.example.com/auth/*',
'http://localhost:3000/auth/*',
],
anonymousRetentionDays: 30,
// Delete Isolated DO data when a user is deleted (DECISIONS #118)
cleanupOrphanData: false,
},

// ─── Storage ──────────────────────────────────────────
storage: {
buckets: {
default: {
access: {
read(auth, file) { return true },
// maxFileSize/allowedMimeTypes removed — enforce in write rule instead:
write(auth, file) { return auth !== null && file.size < 50 * 1024 * 1024 && /^image\//.test(file.contentType) },
delete(auth, file) { return auth !== null && auth.id === file.uploadedBy },
},
},
},
},

// ─── Captcha (Bot Protection) ────────────────────────
// captcha: true, // Auto-provision via Cloudflare deploy
captcha: { // Manual keys (self-hosting / Docker)
siteKey: '0x4AAAAAAA...', // Turnstile dashboard → siteKey
secretKey: '0x4AAAAAAA...', // Turnstile dashboard → secretKey
failMode: 'open', // 'open' (default) | 'closed'
siteverifyTimeout: 3000, // ms (default: 3000)
},

// ─── Rate Limiting ────────────────────────────────────
rateLimiting: {
global: { requests: 10000000, window: '60s' },
db: {
requests: 100,
window: '60s',
binding: { limit: 250, period: 60, namespaceId: '2002' },
},
storage: { requests: 50, window: '60s' },
functions: { requests: 50, window: '60s' },
auth: { requests: 30, window: '60s' },
authSignin: { requests: 10, window: '1m' },
authSignup: { requests: 10, window: '60s' },
events: { requests: 100, window: '60s' },
},

// ─── CORS ─────────────────────────────────────────────
cors: {
origin: ['https://my-app.com', 'http://localhost:3000'],
methods: ['GET', 'POST', 'PATCH', 'DELETE'],
credentials: true,
},

// ─── Realtime ─────────────────────────────────────────
realtime: {
namespaces: {
presence: {
access: {
subscribe(auth) { return auth !== null },
},
},
'public': {
access: {
subscribe() { return true },
},
},
game: {
access: {
subscribe(auth) { return auth !== null },
},
},
},
},

// ─── Service Keys ─────────────────────────────────────
// Consumed by all Admin SDKs.
serviceKeys: {
keys: [
{
kid: 'backend',
tier: 'root',
scopes: ['*'],
secretSource: 'dashboard',
secretRef: 'SERVICE_KEY_BACKEND',
constraints: { env: ['prod'], ipCidr: ['10.0.0.0/8'] },
},
],
},

// ─── Functions ────────────────────────────────────────
functions: {
// Blocking hook timeout is 5 seconds (fixed, not configurable)
},

// ─── Cloudflare Deploy Escape Hatches ────────────────
cloudflare: {
extraCrons: ['15 * * * *'], // Additional Wrangler cron triggers
},

// Note:
// - deploy replaces the managed [triggers] set from config
// - extraCrons wakes scheduled() but does not target a specific App Function

// ─── API ──────────────────────────────────────────────
api: {
schemaEndpoint: 'authenticated', // true | false | 'authenticated'
},

// ─── Email ────────────────────────────────────────────
email: {
provider: 'resend',
apiKey: '...',
from: 'noreply@my-app.com',
},

// ─── KV (User-defined namespaces) ────────────────────
kv: {
cache: {
binding: 'CACHE_KV',
rules: { read(auth) { return true }, write(auth) { return auth !== null } },
},
},

// ─── D1 (User-defined databases) ────────────────────
d1: {
analytics: { binding: 'ANALYTICS_DB' },
},

// ─── Vectorize (Vector search indexes) ───────────────
vectorize: {
embeddings: { dimensions: 1536, metric: 'cosine' },
},
});

rateLimiting is an abuse-protection configuration, not a strict global quota system.

FieldMeaning
requestsSoft-limit request count
windowSoft-limit window (60s, 5m, 1h, ...)
bindingOptional Cloudflare binding override for built-in groups only

Notes:

  • Built-in groups are global, db, storage, functions, auth, authSignin, authSignup, events
  • binding is applied by edgebase dev / edgebase deploy when the CLI generates the temporary wrangler.toml
  • Custom groups do not get generated Cloudflare bindings automatically

Section Reference

SectionDescription
releaseRelease mode — true enables deny-by-default, false (default) allows without rules
databasesDB blocks (shared, namespace:{id}...) with tables, schemas, rules
authOAuth providers, anonymous auth settings
captchaCaptcha (bot protection) settingstrue for auto-provision, or { siteKey, secretKey } for manual
storageBucket definitions, size/type limits, rules
serviceKeysService Key definitions, scopes, constraints
rateLimitingRequest limits per time window
corsCross-origin request settings
realtimeChannel access rules
roomsRoom namespaces with access, lifecycle/action/timer handlers, and public.* release opt-ins
pushFCM config plus server-side delivery access and hooks
functionsFunction settings (blocking hook timeout is 5 seconds, fixed)
cloudflareDeploy-time Cloudflare escape hatches such as additional managed cron triggers
apiAPI endpoint configuration
emailEmail provider settings
pluginsBuild-time plugin instances; plugin tables inherit DB config grammar after merge
kvUser-defined KV namespace bindings (server-only, #121)
d1User-defined D1 database bindings (server-only, #121)
vectorizeVectorize index settings — dimensions, metric (server-only, #121)