Writing a plugin
The AutonnelPlugin contract: payment gateways, conversion callbacks, background jobs, crons, plugin tables and settings panels.
A plugin extends Autonnel without forking the core. It is a package whose default export is an AutonnelPlugin object. The contract is experimental: pin your plugin to a specific Autonnel minor version.
The contract
import type { AutonnelPlugin } from 'autonnel';
export interface AutonnelPlugin {
name: string; // unique id, e.g. "@example/plugin-hello"
version: string;
authProvider?: AuthProvider;
hooks?: AutonnelHooks; // onOrderCreated, ...
onConversion?: (conversion: PluginConversion, runtime: PluginRuntime) => Promise<void>;
jobHandlers?: Record<string, PluginJobHandler>; // kinds start with "plugin."
crons?: PluginCron[]; // names start with "plugin."
settingsPanels?: PluginSettingsPanel[]; // rendered at /settings/plugins/<id>
paymentProviders?: Record<string, PluginPaymentProvider>;
schemaExtension?: string; // extra Prisma models / enums
routes?: { pattern: string; entrypoint: string }[];
configKeys?: { key: string; envFallback: string }[];
}
Plugins load in the order of the plugins array. Hooks, crons, routes and config keys accumulate across plugins; the last authProvider wins; a duplicate payment slug or cron name is rejected at registration.
Minimal example
import type { AutonnelPlugin } from 'autonnel';
const plugin: AutonnelPlugin = {
name: '@example/plugin-hello',
version: '0.1.0',
hooks: {
onOrderCreated: async (ctx, order) => {
console.log('order created', order.orderNumber);
},
},
configKeys: [{ key: 'hello.greeting', envFallback: 'HELLO_GREETING' }],
};
export default plugin;
Registering plugins
// src/plugins.ts
import helloPlugin from '@example/plugin-hello';
export function createPlugins() {
return [helloPlugin];
}
// astro.config.mjs
autonnel({ plugins: createPlugins(), pluginsModule: new URL('./src/plugins.ts', import.meta.url) });
plugins only reaches the build. pluginsModule makes the production server register the same list at startup, on Node and on Cloudflare Workers (including the scheduled path). Without it, hooks, panels, jobs and crons work only in astro dev. The autonnel/cloudflare (defineAutonnelCloudflareConfig) and autonnel/node (defineAutonnelNodeConfig) presets take the same options.
Payment gateways
paymentProviders maps an upper-case slug (for example ACME) to { registration, create(config, runtime) }. STRIPE and PAYPAL are reserved.
registrationdrives the card on the payment page, the settings form (text,passwordandtextareafields;secret: truevalues are never sent back to the browser) and the public checkout config. A gateway that returns{ redirect: true }fromgetPublicConfiggets a pay button that sends the buyer to the hosted payment page.- Hosted-page payments finish through
GET /api/shop/payment/return/<slug>andPOST /api/shop/payment/webhook/<slug>. The return, the webhook and a reconcile cron (every 10 minutes, oldest first, canceled after 24 hours) share one completion path, sopayment.capturedis published exactly once. - Implement
chargeOffSessionto support one-click upsells. It receives thevaultRefstored by the first capture.
See the payments overview for how a plugin gateway looks to the merchant.
Conversions, jobs and crons
Core hands every callback a PluginRuntime with getConfig, enqueueJob, an SSRF-guarded fetch, model, cache, paymentConfig and logger.
onConversionfires on every captured payment (kind: 'purchase') and after each Stripe one-click upsell (kind: 'upsell').conversionIdis stable across redelivery, so use it as the idempotency key.visitedUrlslists the buyer’s tracked pages, newest first, for reading click ids. Do network calls in a job: a throw here makes the whole delivery retry.jobHandlersrun on the core job queue.runtime.enqueueJob(kind, idempotencyKey, payload)dedupes on the key; a throw retries with backoff, up to 12 attempts.crons({ name, intervalMs, run }) run once per 5-minute tick, locked and isolated from each other. The floor is 5 minutes and a failed run is not retried, so enqueue a job when the work must succeed.POST /api/cron/pluginsruns them on demand.runtime.cacheis a plugin and tenant scoped key-value cache with no atomic operations. Use it only for data you can rebuild, such as access tokens.runtime.paymentConfig(slug)returns the decrypted config of a gateway the plugin registered, for jobs and crons that run outside a payment request.
Plugin tables
schemaExtension holds Prisma model and enum blocks. Every model needs a tenantId String field, must not reuse a core model or table name, and must not reference a core model (store the core id as a plain column). runtime.model('Name') returns a tenant-scoped delegate for the plugin’s own models only.
The deployment then composes and pushes the schema:
npx autonnel schema:compose --plugins src/plugins.ts --out prisma/schema
npx prisma generate
npx prisma db push
Settings panels
settingsPanels renders a configured-card form at /settings/plugins/<id> with a Settings menu entry. Fields (text, url, toggle) are stored together as one JSON value under configKey; url fields must be https. Jobs whose kind is in jobKinds appear on the page as a delivery log. Panel fields are sent to the browser, so do not put secrets in them.
Publishing
- Make the package’s default export an
AutonnelPlugin. - Add
autonnelas a peer dependency; do not bundle it. - Add the
autonnel-pluginkeyword. - Document required config keys and tables in the README.
- Ship route entrypoints inside the package and reference them with bare specifiers, such as
@your/plugin/pages/foo.astro.
Keep entry files small and load heavy dependencies with dynamic import() so Workers bundles stay small.