POS Apps let a shop build its own apps (hand-written or vibe-coded with AI tools), upload them to Pancake POS and use them right inside the POS interface. An app only runs in the shop that installed it; it is never published publicly.
An app can:
- Add actions to existing action menus, for example "Push to ERP" in the order action menu.
- Attach to orders, products, purchases, exports and warehouse transfers with the same extension points and the same code shape.
- Insert blocks into existing screens, for example a "Customer debt" box in the order form.
- Add its own pages to the POS menu, for example a "Debt reconciliation" screen.
- Run hooks before or after an operation, for example blocking an order save when the customer exceeds a credit limit.
- Read and write shop data through the Open API, with exactly the permissions of the current user.
An app cannot modify, hide or replace existing POS UI or actions.
Terminology
| Term | Meaning |
|---|---|
| App | A shop's code bundle: a manifest plus built files. Every upload creates a version. |
| Extension | A part of an app attached to one place in POS. An app has many extensions. |
| Extension point / target | A place POS exposes to apps, for example order.details.action or purchase.index.action. See Extension points. |
| Action extension | Adds an item to an existing action menu. |
| Block extension | Inserts a box into an existing screen. |
| Page extension | The app's own page, opened from the POS menu. |
| Hook | Logic that runs before/after an existing operation, with no UI. |
| should_render | A condition that decides whether an action or block is shown. |
| Worker UI | How blocks and modals render: app code runs isolated in a Web Worker and only describes the UI with pos-* elements; POS draws it with real components. |
| App token | A short-lived token POS issues so the app can call the Open API on behalf of the current user. |
Architecture
POS page (pos.pages.fm) App sandbox (separate domain)
┌──────────────────────────────────┐ ┌──────────────────────────────────┐
│ Extension points │ postMessage│ Hidden iframe │
│ ├─ Block / modal (Worker UI) │◄──────────►│ └─ Web Worker │
│ │ drawn with POS components │ │ ├─ App code + Preact │
│ ├─ App page (iframe) │ │ └─ POS runtime (pos, DOM) │
│ └─ Hooks, display conditions │ │ Page iframe: the app's SPA │
│ Bridge: context data, │ │ └─ SDK client (connect) │
│ token, toasts, navigation │ │ fetch ──► Open API (app token) │
└──────────────────────────────────┘ └──────────────────────────────────┘Principles:
- App code never runs on the POS domain. Apps run in a web worker or an iframe on a sandbox domain dedicated to each app, so they cannot read the POS session, cookies or UI.
- POS draws the UI; the app only describes it. For blocks and modals, the app builds a tree of
pos-*elements. POS receives that description and renders it with its own components, so the UI matches POS exactly and the app never touches the page. - All data goes through the Open API, using an app token that carries the current user's permissions.
- Targets, components and context data are a versioned contract (
api_versionin the manifest). POS keeps backward compatibility for the support period of each version.
SDK
Apps do not implement the communication with POS themselves. It is split into two parts:
- POS runtime: POS loads it into the worker before the app's code. It provides the global
posobject, the minimal DOM for Worker UI, thepos-*elements, the postMessage channel, app tokens and the Open API client. The runtime is not part of the app bundle, so POS can update or patch it without apps redeploying. - SDK packages installed in the app project:
| Package | Role |
|---|---|
@pancake-pos/ui-extensions |
Types for pos and pos-* elements, the defineExtension helper; the connect() client for iframe pages |
@pancake-pos/ui |
Theme and POS-styled components for iframe pages |
@pancake-pos/app-cli |
The pos-app command: sign in, try in POS, check code, build, upload |
@pancake-pos/eslint-plugin |
Code rules; editors show problems as you type, and pos-app check uses the same rules |
create-pos-app |
Creates a starter project with an AGENTS.md for AI tools |
Package versions follow api_version (package 2026.1.x for 2026-01). pos-app check validates the manifest and catches mistakes AI tools often make (using the real DOM, plain HTML tags, undeclared methods or domains). Details: SDK & CLI.
Choosing a presentation
| Need | Presentation | Why |
|---|---|---|
| A box or modal inside an existing screen | Worker UI | Looks like POS, lightweight, popovers/modals work normally |
| A one-step operation with no UI | Headless (worker only) | Shows a toast when done, opens nothing |
| Button visibility conditions, before/after-save hooks | Headless | No UI |
| A complex standalone screen: charts, maps, drag and drop, third-party UI libraries | Iframe | Free to use any web library |
Rule of thumb: inside a POS screen → Worker UI; a screen of its own → iframe; no UI → headless.
How Worker UI works
A web worker has no DOM. The POS runtime loads a minimal DOM into the worker, just enough for Preact or plain JS to build a tree of pos-* elements. Whenever the tree changes, the runtime records the changes (nodes added or removed, properties or text changed) and sends them to POS as one batch per render pass. POS validates each element and draws it with real components. When the user clicks or types, POS sends the event into the worker and the app's code receives the value in event.detail.
Worker (app code) POS
setState → Preact re-renders on the minimal DOM
→ runtime batches the changes ──────────► validate → draw with POS components
onChange(event.detail) ◄───────────────────────── user clicks, typesConsequences:
- State lives in the app's code (
useState, variables…). POS only keeps the rendered UI, never the app's state. - Each time an extension is shown it runs in its own worker. Closing the modal or leaving the screen stops the worker and its state is lost; persist anything long-lived through the Open API or the app's own server.
- Inputs stay responsive while the app is busy: POS shows what the user types immediately, then notifies the app.
- The runtime is built into the worker; apps install no extra library to talk to POS.
- Only elements from the component catalog can be used. Unknown elements are not rendered and the extension reports an error.
- There is no
window,document.querySelector,localStorage,canvasor DOM measurement. Chart and third-party UI libraries do not run in the worker; use POS-provided components or move that part to an iframe page.
Security model
| Risk | Mitigation |
|---|---|
| The app uses the POS login session | App code runs on a separate sandbox domain and cannot read POS cookies or tokens |
| The app reads or modifies the POS UI | The worker has no real DOM; POS only renders valid pos-* elements; iframes are cross-origin |
| Customer-entered data contains malicious code | Worker UI accepts no raw HTML; for iframes, damage is limited to the app token's permissions |
| Staff use an app to exceed their permissions | App tokens always carry the current user's permissions; only users with the configuration management permission can upload them |
| The app sends data elsewhere | The app can only call, and only show images from, domains declared in network.allowed_origins, which admins see when activating |
| The app slows down or freezes POS | Lazy loading, timeouts, killing stuck workers, size limits, an emergency kill switch |
| A token leaks | Tokens expire after 15 minutes, are bound to one shop, one user and one app, and are revoked as soon as the app is disabled |
Data access
Apps use the existing Open API: same endpoints, format and pagination. The difference is the credential:
- No static API key in code. POS issues an app token automatically while the app runs.
- App permissions = the current user's permissions ∩ the limits the admin sets for the app (warehouses, hidden fields, read-only…).
- Every write is audited with the real user and the app.
Details: Data access.
App lifecycle
- A shop member with the configuration management permission uploads a bundle with the CLI. Each upload creates a version.
- POS validates the manifest: valid targets, existing files, sizes within limits.
- The admin reviews the data access and external domains the app requests, confirms the shop is responsible for the app, then activates the version on the Custom apps page.
- By default every shop member sees the app; limiting it by department is not yet available on the management page.
- The admin can roll back to an earlier version or disable the app at any time. Disabling revokes all app tokens in use.
Limits
| Item | Limit |
|---|---|
| Bundle per version | ≤ 10 MB, at most 1,000 files |
| Extension module (Worker UI, headless) | ≤ 256 KB gzip per module |
| Extensions per app | 30 |
Domains in network.allowed_origins |
10 |
| Active apps per shop | 10 |
| Versions kept per app | The latest 20, plus the active one |
| Total app storage per shop | 500 MB |
| Worker UI elements per extension | 2,000 |
| Worker UI updates | 60 per second; an extension that exceeds this for 3 seconds in a row is stopped |
should_render timeout |
300 ms |
before_submit hook timeout |
2 seconds by default (timeout_ms), 5 seconds max |
after_save hook and headless action run time |
30 seconds |
| Open API request rate | Under 30 reads (GET) and under 100 writes per second, per app + user + shop + endpoint |
| App token lifetime | 15 minutes, never beyond the user's login session (renewed by the SDK) |
| Error log per app | The latest 500 errors, kept for 7 days |
Support scope
- POS web only. The mobile app does not run extensions yet.
- UI hooks only run when the operation happens on POS web, at the place where the hook is attached. Records created or changed through the Open API, the mobile app, bulk updates or marketplace sync do not go through hooks. Rules that must always hold belong on the server.
- Apps do not run in the background. Scheduled jobs and webhook processing need the shop's own backend (see Data access).
Roadmap
| Phase | Scope |
|---|---|
| PoC | Sandbox, app tokens, order detail block, order action menu item, iframe page, a sample app written by AI |
| Phase 1 | Upload, versions, rollback, visibility permissions, audit, rate limits, component catalog v1, SDK, CLI |
| Phase 2 | before_submit hook, Worker UI pages (render: "worker"), custom print templates, custom columns, customer blocks, per-app webhooks and service keys |
| Phase 3 | Consider the mobile app, install one app across shops of the same owner |
