For developers
Extend it without
asking permission.
One command scaffolds a shippable plugin package. One environment variable loads it into any install. The API surface is typed, versioned, and refuses incompatibility with a sentence instead of a stack trace.
Sixty seconds, honestly measured
A plugin is a file
the host imports.
Scaffold the shippable shape
A standalone npm package: factory entry, its own build, zero runtime imports from the host — the host hands your factory its own helpers, so your bundle runs inside any install regardless of versions.
npx astrobaas plugin package acme-stamps
cd acme-stamps && npm install && npm run buildLoad it with one variable
A path or a package name. Activation is separate, so an operator installs once and switches modules in the admin. A module that fails to load is reported with its reason — on boot, and by the deep health check a deploy script asserts.
ASTROBAAS_PLUGINS=./acme-stamps/dist/index.mjs \
ASTROBAAS_PLUGINS_ACTIVATE=acme-stamps npm run devPersist from a route, import nothing
Handlers receive a store already namespaced to your plugin. This exact route runs in the platform test suite — two calls count 1, then 2.
handler: async ({ store }) => {
const n = ((await store.get('counters', 'hits'))?.n ?? 0) + 1;
await store.put('counters', 'hits', { n });
return new Response(JSON.stringify({ ok: true, hits: n }));
}Declare what you need, get refused politely
An incompatible module is refused at load — “requires core ^9.0.0, but this host provides plugin API 1.0.0” — instead of loading and failing somewhere deep at request time.
requiresCore: '^1.0.0'The surface
What a plugin
can put its hands on.
- API routes
- Under /api/plugin/<id>/ with declared access: public, staff or admin — enforced by the middleware before your handler runs. A route may declare csrf exemptions only inside its own namespace; a plugin cannot widen a core gate. Core paths always win: a plugin claiming /api/settings/get simply does not get it.
- Admin screens
- Server-rendered pages under /admin/plugin/<id>/, in the sidebar automatically, admin-only unless you declare roles. Authorisation rides the same longest-prefix table as core screens.
- Storage
- A namespaced store handed to your route handlers as ctx.store and to your migrations — one plugin cannot read another’s records, as a property of the API rather than a rule to remember. Migrations run at bootstrap, stamped per step.
- Payments
- Gateways and manual methods through two hooks. A provider is only offered when its declared env vars exist, and a manual method can never shadow a gateway id — a shop cannot accept as unpaid what it believes was charged.
- Content types
- Register custom collections with schemas; they get REST endpoints, visibility rules and admin CRUD without you writing routes.
- Themes
- Design tokens with a dark palette, slot overrides for the public views, whole-page patterns, and a customizer the admin already knows how to drive.
Hooks, the honest sample
Twenty-one hooks.
Here are the interesting five.
- post_content
- (html, post) => htmlPost body HTML. Re-sanitized after filters — you cannot inject script, by construction.
- product_price
- (cents, product, { qty }) => centsFinal unit price at checkout. Sales rules, volume discounts, member pricing. Integer cents.
- order_line_extras
- (result, { line, product }) => resultThe vertical’s hook: validate a line, refuse the order, or freeze extra data onto it. How the optical module checks prescriptions.
- payment_providers
- (providers) => providersContribute a payment gateway. A forged webhook answers 401 if your error class ends in VerificationError.
- commerce_schema
- (schema, { name }) => schemaPublish a machine-readable form schema so a storefront renders your domain without hard-coding it.
- … and 16 more
- PLUGIN_HOOKS in astrobaas/coreAll 21 documented, with payload contracts and the reduce-order rules that keep a refusal from being overwritten.