Using Plugins
Installation
Plugins are standard npm packages. Install them like any other dependency:
npm install @edgebase/plugin-stripe
Configuration
Import the plugin's factory function and add it to the plugins array in edgebase.config.ts:
import { defineConfig } from '@edgebase/shared';
import { stripePlugin } from '@edgebase/plugin-stripe';
export default defineConfig({
plugins: [
stripePlugin({
secretKey: process.env.STRIPE_SECRET_KEY!,
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
currency: 'usd',
}),
],
});
Each plugin factory accepts a typed config object. The types come from the plugin package itself — your IDE will autocomplete available options.
Plugin authors can also expose a serializable manifest from definePlugin(). The CLI reads it for descriptions, docs links, and configTemplate output.
definePlugin() also stamps the current public pluginApiVersion onto the resolved plugin instance. If the plugin was built against a different contract, deploy fails fast instead of loading a partially incompatible plugin.
Store sensitive values (API keys, secrets) in .env.development / .env.release and reference them via process.env when building edgebase.config.ts, just like the rest of your server configuration.
Multiple Plugins
Add multiple plugins to the array. They are processed in order:
import { defineConfig } from '@edgebase/shared';
import { stripePlugin } from '@edgebase/plugin-stripe';
import { analyticsPlugin } from '@edgebase/plugin-analytics';
import { emailPlugin } from '@edgebase/plugin-email';
export default defineConfig({
plugins: [
stripePlugin({ secretKey: process.env.STRIPE_SECRET_KEY! }),
analyticsPlugin({ trackPageViews: true }),
emailPlugin({ provider: 'resend', apiKey: process.env.RESEND_API_KEY! }),
],
});
When multiple plugins register the same auth hook (e.g. afterSignUp), they all execute in the order they appear in the plugins array.
Deploying
Plugins are bundled at build time — no special deployment steps:
npx edgebase deploy
The CLI automatically:
- Evaluates
edgebase.config.ts(resolves plugin factories) - Merges plugin tables into your DB blocks (namespaced)
- Registers plugin functions and auth hooks
- Bundles everything into a single Worker
- Runs plugin migrations (if plugin version changed)
Managing Plugins
List Installed Plugins
npx edgebase plugins list
Shows all configured plugins with their manifest metadata, injected tables, functions, hooks, and config template:
📦 2 plugin(s) configured:
• plugin-stripe v0.1.0 api v1
Stripe billing for EdgeBase
Tables: customers, invoices, subscriptions
Functions: create-checkout, handle-webhook, sync-charges
Hooks: onTokenRefresh
Config Template:
{
"stripeSecretKey": "sk_test_CHANGE_ME"
}
• plugin-analytics
Functions: track-event
Hooks: afterSignIn
Removing a Plugin
- Remove it from
edgebase.config.ts - Uninstall the package:
npm uninstall @edgebase/plugin-stripe - Redeploy:
npx edgebase deploy - Remove plugin-owned data:
npx edgebase plugins cleanup @edgebase/plugin-stripe
plugins cleanup deletes namespaced plugin tables plus plugin migration metadata stored in the internal control-plane D1 (CONTROL_DB). On Cloudflare Edge, pass --account-id and --api-token if the plugin ever wrote to dynamic Durable Object instances; otherwise the CLI can only clean currently known namespaces.
The command refuses to run while the plugin is still configured in edgebase.config.ts, which prevents accidentally wiping active plugin data.
Using Plugin Client SDKs
Some plugins provide optional client SDK wrappers for typed frontend access:
// Client-side (e.g. in a React app)
import { createClient } from '@edgebase/web';
import { createStripePlugin } from '@edgebase/plugin-stripe/client';
const client = createClient({ baseUrl: 'https://your-project.edgebase.app' });
const stripe = createStripePlugin(client);
// Typed method — calls plugin-stripe/create-checkout under the hood
const session = await stripe.createCheckout({ priceId: 'price_xxx' });
Client SDK extensions use client.functions.call() internally, so they work with any EdgeBase client SDK that supports function calls.
How Plugin Tables Work
Plugin tables behave exactly like your own tables. They:
- Follow the same schema system (types, required fields, defaults)
- Support the same preferred
access + handlersgrammar as first-party tables - Support full-text search and custom indexes
- Are queryable via the Admin SDK
- Are queryable from all Admin SDKs
At build time, EdgeBase merges plugin tables into their target DB block and runs them through the same config materialization path as the rest of the app. That means plugin tables use the same access + handlers grammar without any plugin-specific branching.
Plugins are still trusted server-side code. There is no plugin sandbox or per-plugin binding allowlist today; npm install is treated as an explicit trust decision.
tables: {
customers: {
access: {
read: (auth, row) => auth?.id === row.userId,
insert: (auth) => auth !== null,
},
handlers: {
hooks: {
beforeInsert: async (_auth, data) => ({
...data,
source: 'plugin',
}),
},
},
},
}
The only difference is the namespace prefix:
// In your own code — access plugin tables via admin SDK
const customers = await admin.db('shared').table('plugin-stripe/customers').list();
Plugin handlers access their own tables the same way:
// Inside a plugin function handler
const custs = await ctx.admin.table('plugin-stripe/customers').list({
filter: [['userId', '==', ctx.auth!.id]],
limit: 1,
});
DB Block Targeting
By default, plugin tables go into the shared DB block. Plugins can override this:
export const myPlugin = definePlugin({
name: 'my-plugin',
dbBlock: 'workspace', // Tables go into the 'workspace' DB block
tables: { ... },
});
HTTP Paths
Plugin HTTP functions default to /api/functions/{plugin-name}/{function-name}.
If you want a cleaner public URL, set trigger.path:
functions: {
resolve: {
trigger: { type: 'http', method: 'GET', path: '/s/[code]' },
handler: async (ctx) => {
return Response.redirect('https://example.com', 302);
},
},
}
That function is now available at GET /api/functions/s/:code.
Storage Hooks
Plugins can also hook into storage lifecycle events. These hooks fire for ALL buckets — the plugin receives the bucket name in ctx.file.bucket and can decide whether to act.
Blocking hooks (5s timeout):
beforeUpload— Can reject uploads or inject custom metadatabeforeDelete— Can reject file deletionbeforeDownload— Can reject file download
Non-blocking hooks (via waitUntil):
afterUpload— Process uploaded files (e.g., virus scan, thumbnail generation)afterDelete— Cleanup related dataonMetadataUpdate— React to metadata changes
Presigned URL direct uploads bypass the server entirely and do NOT trigger afterUpload hooks.