Packages
| Package | Used in | Role |
|---|---|---|
@pancake-pos/ui-extensions |
extensions/, page/ |
Types for pos and pos-* elements, the defineExtension helper, the client for iframe pages (/page) |
@pancake-pos/ui |
page/ |
Theme and POS-styled components for iframe pages |
@pancake-pos/app-cli |
Dev machine, CI | The pos-app command: sign in, try in POS, check, build, upload |
@pancake-pos/eslint-plugin |
Editor, pos-app check |
Code rules for apps; editors show problems as you type |
create-pos-app |
Once | Creates a starter project with the packages above and an AGENTS.md |
Package versions follow api_version: package 2026.1.x is for "api_version": "2026-01". The CLI reports an error when they do not match.
SDK and the POS runtime
Code running in the sandbox has two parts, provided by two different parties:
Worker inside the app sandbox
┌─────────────────────────────────────────────────────┐
│ App bundle (built and uploaded by the shop) │
│ extension modules + Preact or plain JS │
├─────────────────────────────────────────────────────┤
│ POS runtime (provided by POS, not in the bundle) │
│ minimal DOM · pos-* elements · global pos object │
│ postMessage channel · app token · Open API calls │
└─────────────────────────────────────────────────────┘- The POS runtime is loaded into the worker before the app's modules. Because it is not part of the bundle, POS can ship security fixes or improvements without apps redeploying, and app modules stay small.
@pancake-pos/ui-extensionsmostly provides types and helpers inextensions/; it adds almost no code to the bundle.- Iframe pages have no preloaded runtime: the app imports
connect()from@pancake-pos/ui-extensions/page, and this small client is bundled with the page.
What the SDK and runtime handle for you
| Concern | Details |
|---|---|
| Messaging | postMessage between the sandbox and POS, sender verification, commands and events in both directions |
| Authentication | Fetches the app token, renews it before it expires, refreshes and retries once on 401 |
| Open API calls | pos.api.*, adds the Authorization header, blocks methods outside access.methods, turns errors into PosApiError |
| Worker UI | Minimal DOM, pos-* element definitions, batching changes sent to POS, delivering POS events to the worker |
| Context | Updates pos.data, emits data-change events |
| Iframe pages | Syncs the URL with POS, setTitle, setPrimaryAction |
| Development mode | Loads the bundle from localhost, reloads extensions and pages after every build |
Installation
Projects created with create-pos-app already have everything. To add the SDK to an existing project:
npm install @pancake-pos/ui-extensions preact
npm install -D @pancake-pos/app-cli typescript eslint @pancake-pos/eslint-plugin
npm install @pancake-pos/ui react react-dom # only if you have an iframe pageTypeScript
The tsconfig.json at the project root covers extensions/:
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "Bundler",
"jsx": "react-jsx",
"jsxImportSource": "preact",
"strict": true,
"types": ["@pancake-pos/ui-extensions"]
},
"include": ["extensions"]
}typesdeclares the globalposobject and thepos-*elements for JSX (properties, events,event.detail).- The
page/folder has its ownpage/tsconfig.jsonwith"jsxImportSource": "react", generated by the template; do not merge it into the root tsconfig. extensions/supports Preact (recommended, small) and plain JS using the runtime's DOM API. React is not supported inextensions/yet: React 18 turns array/object properties into strings and cannot listen topos-*element events; React 19 is under evaluation. Iframe pages can use React as usual.
defineExtension
An optional helper that makes the type of data and of the return value match the target.
import { defineExtension } from '@pancake-pos/ui-extensions';
export default defineExtension('order.before_submit', async (data) => {
// data: DetailsData<'order'>; must return BeforeSubmitResult
if (data.draft.total_price > 20_000_000 && !data.draft.bill_phone_number) {
return { ok: false, message: 'Large orders need a phone number.' };
}
return { ok: true };
});defineExtension returns the function you pass in, so it is equivalent to export default async function (data).
The resource comes from the target name, so the same helper types data.draft as a PurchaseDraft in purchase.before_submit or data.records as WarehouseTransferRef[] in warehouse_transfer.index.selection-action.
Exported types: Resource, DetailsData, IndexData, SelectionData, AfterSaveInput, BeforeSubmitResult, HeadlessActionResult, PageData, the Ref/Draft pair of each resource (OrderRef, OrderDraft, ProductRef, ProductDraft, PurchaseRef, PurchaseDraft, ExportRef, ExportDraft, WarehouseTransferRef, WarehouseTransferDraft), PosApiError, PosRuntimeError.
Iframe pages
import { connect } from '@pancake-pos/ui-extensions/page';
const pos = await connect({ timeout: 10_000 });connect()waits for POS to send the context and token; aftertimeout(10 seconds by default) it throws, usually because the page was opened outside POS.- The page runs only inside a POS iframe. Opening a sandbox URL in its own tab or window, including a popup the page opens itself, is refused (
403). A popup sign-in flow must return to a page of yours outside the sandbox and report back to the app through your server. - The page's
poshas the same API as in the worker, pluspos.page.*. See Runtime API.
CLI (pos-app)
| Command | Description |
|---|---|
pos-app login --token <token> [--shop <shop_id>] |
Stores the CLI session on this machine. Copy the full command, token included, from the Connect CLI button on the custom apps management page (requires the Settings permission); the token only works for that shop and the CLI's own commands (upload, versions, errors, sample data; it cannot activate apps or sign in to POS), lasts 30 days and stops working when you sign out of POS or when it is revoked from the Connect CLI button |
pos-app logout |
Revokes the CLI token, then removes the session from this machine |
pos-app dev [--port 5174] [--page-url <url>] |
Watch and build, serve at http://localhost:<port> for POS development mode; POS reloads after every build. --page-url: a self-hosted page running on your machine (for example http://localhost:3000) |
pos-app check |
Validates the manifest and code (see below) without uploading |
pos-app build |
Builds into dist/ |
pos-app deploy [--shop <shop_id>] [--message "..."] |
Runs check and build, uploads a new (inactive) version |
pos-app versions [--shop <shop_id>] |
Lists uploaded versions and the active one |
pos-app mcp [--shop <shop_id>] |
Starts the MCP server for AI tools, see MCP |
Every command accepts --json for machine-readable output (CI, scripts, the MCP server).
The pos-app command is available once @pancake-pos/app-cli is installed, so npx pos-app … works inside the project folder. Outside a project, call it by package name: npx @pancake-pos/app-cli <command>. The pos-app package on npm is not published by Pancake.
Activation and rollback happen on the custom apps page in POS, so an admin always approves permissions before an app runs.
pos-app check
Static checks that run automatically before deploy. Especially useful for AI-generated code:
- Manifest: valid against the schema, reported with line numbers; targets exist in
api_version;module/entryfiles exist;conditionskeys are valid for the resource. - Code: runs the code rules with a built-in configuration; nothing to configure in the project.
- Sizes of modules and bundles are within limits.
error findings block deploy; warning findings are only reported. Each finding has a file, line, rule id and a suggested fix (--json for CI and MCP). pos-app check ignores eslint-disable comments, so disabling a rule in code does not bypass this check.
Code rules
The rules ship in @pancake-pos/eslint-plugin. Element rules are generated from the component catalog of the exact api_version, so they always match POS.
| Group | Rules | Level |
|---|---|---|
| Worker environment | No window, document.querySelector, localStorage, alert…; no react, react-dom, CSS files or UI libraries imported in extensions/ |
error |
pos-* elements |
Elements, properties and events exist in the catalog (with a suggestion for typos); valid enum values; valid children and slots; text only inside elements that accept text | error |
| Data and security | pos.api.* methods are listed in access.methods; literal fetch URLs and pos-image sources belong to network.allowed_origins (images may also use public/); no hardcoded shop id; no strings that look like API keys, tokens or secrets |
error |
| Behavior | Modules export default a function; rendered lists have a key; input value is not set after an await; before_submit does not chain several API calls |
warning |
| Iframe pages | No window.top or window.parent; no routes starting with /_pos/ |
error |
Static checks cannot catch dynamic cases such as element names built from variables. In development mode POS also logs warnings to the console at run time.
Editor feedback
Projects created with create-pos-app include an eslint.config.js:
import posApp from '@pancake-pos/eslint-plugin';
// recommended reads pos-app.json for api_version, access and allowed_origins
export default [...posApp.configs.recommended];Install the ESLint extension in your editor (VS Code, Cursor…) to see problems as you type. npx eslint . gives the same results as the code part of pos-app check.
Build output
dist/
├─ pos-app.json # manifest with module paths pointing to built files
├─ ext/
│ ├─ order-block.js # one ES module per module, Preact included
│ └─ push-erp.js
├─ public/ # images for pos-image, copied as is
└─ page/ # iframe page built by Vite
├─ index.html
└─ assets/...- Extension modules are built with esbuild, targeting ES2020, without code splitting and without imports from CDNs.
- Iframe pages are built with Vite using the configuration that ships with the template.
MCP
pos-app mcp starts an MCP server (stdio) so AI tools can look up the docs, see the shop's real metadata, check the app, try it, read errors, deploy and report bugs to Pancake without leaving the chat.
npx @pancake-pos/app-cli mcp --shop 123 # run in the app's project folder, or add --cwd <folder>How to add it to Claude Code, Cursor, VS Code, Codex and Claude Desktop, the tool list and sample prompts: Build with AI › MCP.
