Skip to content

0018. Plugins and extension points

  • Status: Accepted
  • Date: 2026-10-27

Shops and integrators want OpenKedai to do things the core project will never ship: sync to an accounting package, award loyalty points, print a survey QR on receipts, ask for a car plate at a drive-through. If the only way to add these is to change the core codebase, every shop with a need forks, and forks drift. The human asked for the server to be as flexible and extensible as possible, with plugins that add features without touching the core (in the spirit of ADR 0014). A future hosted fork (ADR 0013) needs the same seams to stay thin.

Three constraints shape what a plugin may do:

  • Offline-first (ADR 0002): taking, paying and printing an order never wait on the network, so nothing on the server can sit on that path.
  • Devices compute totals (ADR 0007): a device calculates totals with packages/core while offline. A server-side plugin that changed a price, tax, discount or rounding would disagree with what the till already printed.
  • App stores do not allow an app to download and run new code, and the POS must work offline, so plugin code cannot run on devices.

Two layers, one mechanism.

  1. In-process server plugins for self-hosters. The operator installs a plugin by putting it in the plugins folder (or naming a package in an environment variable). Through @openkedai/plugin-sdk a plugin can:

    • handle events after they are stored, including its own;
    • add HTTP routes under /api/ext/<plugin id>/, each with an access level;
    • declare settings as a list of simple fields, so any front end can render the form;
    • keep data in its own SQLite file per tenant;
    • define its own event types, ext.<plugin id>.<name>, validated by a schema it supplies;
    • run scheduled jobs;
    • contribute declarative items that the POS app shows (below).

    Plugins are trusted code running with the server’s permissions, like WordPress or Strapi plugins. The operator decides what to install; a hosted version only allows plugins it has vetted.

  2. Outgoing webhooks and a token-scoped API for everyone else: integrations in any language, hosted or self-hosted. These are designed for now and built in a later milestone. They are off by default and only send data when the owner switches them on.

Event-log consumers. Each plugin’s event handlers, and later each webhook, read the tenant’s event log from a saved position (cursor). A handler that throws is retried with backoff and never blocks storing or syncing sales; the plugin is marked as failing until it recovers. After a restart or a fixed bug, the plugin catches up from where it stopped. Delivery is at least once, in storage order, so handlers must tolerate seeing an event twice. Delivery is woken by new events, never by polling.

What plugins cannot do. They cannot change prices, tax, discounts or rounding, or edit or delete events. Anything that changes money goes through settings and rule data (ADR 0006, ADR 0014), which every device applies the same way offline. Plugin events never belong to an order (orderId stays empty; they refer to orders in their payload), so order and shift projections never see them.

Declarative items for the POS app. A plugin can contribute data the app knows how to use, and the app caches it so it works offline:

  • tenders: extra payment types (for example “GrabPay”, “Touch ’n Go”), each mapped to a built-in method, optionally asking for a reference;
  • receipt blocks: text or a QR code at the foot of the receipt, with a few placeholders such as the receipt number;
  • order fields: extra fields taken with an order (for example a car plate or a pager number), optionally printed on the receipt or kitchen ticket.

The same shapes can later be defined by the owner directly, without a plugin.

The hub’s own features use the same API. Sync, pairing and discovery (M6b), and later backup, e-invoicing and payment providers, are built as modules on the plugin kernel where practical, so the API stays honest and a hosted fork becomes the core plus private plugins.

Licensing. @openkedai/plugin-sdk is MIT, with no runtime dependencies. LICENSE adds a section 7 additional permission (a plugin exception) so plugins that use only the plugin interface may be distributed under any license. Like the CLA, the exception’s wording must be reviewed by a lawyer before the repository is published.

Compatibility. The SDK exports PLUGIN_API_VERSION. A plugin declares the version it was built for. The server loads a plugin only when the major versions match, and reports any plugin it skips.

  • The server grows a plugin kernel: a loader, consumers, a route mount, settings, storage, jobs, and the contributions endpoint. M6 is split: M6a builds the kernel, M6b builds the hub on it.
  • The event store accepts extension events alongside the core events. The core order and shift types stay closed, so exhaustive checks still catch missing core cases.
  • A broken plugin can still harm the server: it can use memory, block the event loop or read files. That is the price of in-process plugins. The resource budget is measured without plugins.
  • The plugin API is a public contract. Breaking it needs a new major PLUGIN_API_VERSION and a note in the changelog.
  • Payment payloads, orders and receipts grow optional fields for tenders, order fields and receipt blocks. Older events stay valid.

For AI tools: llms.txt ·llms-full.txtopenkedai.com