Autonnel v0.1.0

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.

  • registration drives the card on the payment page, the settings form (text, password and textarea fields; secret: true values are never sent back to the browser) and the public checkout config. A gateway that returns { redirect: true } from getPublicConfig gets a pay button that sends the buyer to the hosted payment page.
  • Hosted-page payments finish through GET /api/shop/payment/return/<slug> and POST /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, so payment.captured is published exactly once.
  • Implement chargeOffSession to support one-click upsells. It receives the vaultRef stored 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.

  • onConversion fires on every captured payment (kind: 'purchase') and after each Stripe one-click upsell (kind: 'upsell'). conversionId is stable across redelivery, so use it as the idempotency key. visitedUrls lists 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.
  • jobHandlers run 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/plugins runs them on demand.
  • runtime.cache is 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

  1. Make the package’s default export an AutonnelPlugin.
  2. Add autonnel as a peer dependency; do not bundle it.
  3. Add the autonnel-plugin keyword.
  4. Document required config keys and tables in the README.
  5. 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.