App Functions
EdgeBase App Functions let you run server-side code in response to events. Define functions triggered by database changes (insert, update, delete), expose custom HTTP endpoints, run scheduled tasks with cron expressions, or hook into authentication events to enforce business logic. Functions have full access to the Admin SDK for database operations, storage, push notifications, and more.
The context.admin surface maps to the same server-side capabilities exposed by all Admin SDKs.
File-System Routing
App Functions use file-system routing by default. Each .ts file in the functions/ directory becomes an HTTP endpoint under /api/functions/*:
functions/
hello.ts -> /api/functions/hello
users/index.ts -> /api/functions/users
users/[userId].ts -> /api/functions/users/:userId
users/[userId]/profile.ts -> /api/functions/users/:userId/profile
(internal)/sync.ts -> /api/functions/sync (parentheses stripped)
Named Exports = HTTP Methods
Export named constants (GET, POST, PUT, PATCH, DELETE) to handle specific HTTP methods:
// functions/users/[userId].ts
import { defineFunction } from '@edgebase/shared';
export const GET = defineFunction(async ({ params, admin }) => {
const user = await admin.db('shared').table('users').get(params.userId);
return Response.json(user);
});
export const DELETE = defineFunction(async ({ params, admin }) => {
await admin.db('shared').table('users').delete(params.userId);
return Response.json({ deleted: true });
});
Dynamic Routes
Use [param] in file or directory names to capture URL segments. The captured values are available in context.params:
// functions/workspaces/[wsId]/docs/[docId].ts
// URL: /api/functions/workspaces/ws-123/docs/doc-456
export const GET = defineFunction(async ({ params, admin }) => {
// params.wsId = 'ws-123'
// params.docId = 'doc-456'
return admin.db('workspace', params.wsId).table('documents').get(params.docId);
});
Optional trigger.path Override
If you need a cleaner public route than the file path provides, use a default export with trigger.path:
// functions/reports/top-authors.ts
export default defineFunction({
trigger: { type: 'http', method: 'GET', path: '/analytics/top-authors' },
handler: async ({ admin }) => {
return admin.sql('shared', undefined, 'SELECT 1');
},
});
That function is served at GET /api/functions/analytics/top-authors.
Trigger Types
DB Trigger
Fire after database insert, update, or delete operations. Runs asynchronously via waitUntil().
HTTP Trigger
Expose custom endpoints under /api/functions/* with optional captcha protection.
Schedule (Cron)
Run on a schedule — weekly reports, daily cleanups, periodic syncs.
Auth Hooks
Hook into beforeSignUp, afterSignIn, onTokenRefresh and more. Block or modify auth flows.
Defining a Function
HTTP Functions (Named Exports)
For HTTP endpoints, export named constants matching HTTP methods:
// functions/send-email.ts -> POST /api/functions/send-email
import { defineFunction, FunctionError } from '@edgebase/shared';
export const POST = defineFunction(async ({ auth, admin, request }) => {
if (!auth) throw new FunctionError('unauthenticated', 'Login required');
const body = await request.json();
await admin.db('shared').table('emails').insert({
to: body.to,
subject: body.subject,
userId: auth.id,
});
return Response.json({ sent: true });
});
Trigger Functions (Default Export)
For DB triggers, cron schedules, and auth hooks, use the default export with a trigger config:
// functions/onPostCreated.ts
import { defineFunction } from '@edgebase/shared';
export default defineFunction({
trigger: { type: 'db', table: 'posts', event: 'insert' },
handler: async ({ data, auth, admin }) => {
// data.after -> the newly created post
// auth -> current user info
// admin -> server SDK instance (full access)
await admin.db('shared').table('activity').insert({
type: 'new_post',
postId: data.after.id,
userId: auth?.id,
});
},
});
Function Context
Every function receives these context objects:
| Context | Description |
|---|---|
data | Trigger-specific data (DB event, HTTP request, etc.) |
admin | Admin SDK instance — admin.db('shared').table(), admin.sql(), admin.auth, admin.broadcast(), admin.functions.call() |
auth | Current user (if authenticated) |
params | Dynamic route parameters from [param] segments (HTTP functions only) |
request | The incoming HTTP Request object |
storage | File storage API (optional, only if R2 binding exists) |
analytics | Analytics Engine adapter (optional, only if ANALYTICS binding exists) |
pluginConfig | Plugin-specific configuration (optional, from config.plugins section) |
DB Trigger
Fires after database CUD operations:
export default defineFunction({
trigger: { type: 'db', table: 'orders', event: 'update' },
handler: async ({ data }) => {
console.log('Before:', data.before);
console.log('After:', data.after);
},
});
DB triggers execute asynchronously (context.waitUntil()) and do not block API responses.
HTTP Trigger
Expose custom HTTP endpoints via file-system routing. The file path determines the default URL, and named exports determine the HTTP method:
// functions/stripe-webhook.ts -> POST /api/functions/stripe-webhook
export const POST = defineFunction(async ({ request, admin }) => {
const body = await request.json();
await admin.db('shared').table('payments').insert({ stripeId: body.id });
return Response.json({ received: true });
});
Multiple methods can be defined in the same file:
// functions/users.ts
export const GET = defineFunction(async ({ admin }) => {
const { items } = await admin.db('shared').table('users').list();
return Response.json({ items });
});
export const POST = defineFunction(async ({ request, admin }) => {
const body = await request.json();
const user = await admin.db('shared').table('users').insert(body);
return Response.json(user);
});
You can also override the default route with trigger.path:
export default defineFunction({
trigger: { type: 'http', method: 'POST', path: '/webhooks/stripe' },
handler: async ({ request, admin }) => {
const body = await request.json();
await admin.db('shared').table('payments').insert({ stripeId: body.id });
return Response.json({ received: true });
},
});
Options
| Option | Type | Default | Description |
|---|---|---|---|
captcha | boolean | false | Require captcha (Turnstile) verification before the handler runs |
Captcha-protected HTTP function:
// functions/contact.ts
export const POST = defineFunction(async ({ request, admin }) => {
const body = await request.json();
await admin.db('shared').table('inquiries').insert({ email: body.email, message: body.message });
return Response.json({ ok: true });
});
POST.captcha = true; // Requires a valid captcha token
When captcha: true, the middleware rejects requests without a valid token (403). See Captcha Guide for full details.
Schedule Trigger (Cron)
Run on a schedule:
export default defineFunction({
trigger: { type: 'schedule', cron: '0 9 * * MON' },
handler: async ({ admin }) => {
const count = await admin.sql(
'reports',
"SELECT COUNT(*) as total FROM reports WHERE createdAt > date('now', '-7 days')",
);
console.log(`Weekly report count: ${count[0].total}`);
},
});
Auth Hooks
Hook into authentication events:
export default defineFunction({
trigger: { type: 'auth', event: 'beforeSignUp' },
handler: async ({ data }) => {
const domain = data.email.split('@')[1];
if (domain !== 'company.com') {
throw new Error('Only company emails allowed');
}
return { role: 'employee' };
},
});
| Event | Timing | Can Block? |
|---|---|---|
beforeSignUp | Before signup | Yes |
afterSignUp | After signup | No |
beforeSignIn | Before login | Yes |
afterSignIn | After login | No |
beforePasswordReset | Before password reset | Yes |
onTokenRefresh | On token refresh | Yes |
Blocking hooks have a 5-second timeout. If exceeded, the hook is skipped and the operation continues.