Skip to main content

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.


Admin Surface Parity

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:

ContextDescription
dataTrigger-specific data (DB event, HTTP request, etc.)
adminAdmin SDK instance — admin.db('shared').table(), admin.sql(), admin.auth, admin.broadcast(), admin.functions.call()
authCurrent user (if authenticated)
paramsDynamic route parameters from [param] segments (HTTP functions only)
requestThe incoming HTTP Request object
storageFile storage API (optional, only if R2 binding exists)
analyticsAnalytics Engine adapter (optional, only if ANALYTICS binding exists)
pluginConfigPlugin-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

OptionTypeDefaultDescription
captchabooleanfalseRequire 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' };
},
});
EventTimingCan Block?
beforeSignUpBefore signupYes
afterSignUpAfter signupNo
beforeSignInBefore loginYes
afterSignInAfter loginNo
beforePasswordResetBefore password resetYes
onTokenRefreshOn token refreshYes

Blocking hooks have a 5-second timeout. If exceeded, the hook is skipped and the operation continues.