Point of Sale
The POS plugin adds terminals, shifts, transactions, and multi-method payment splitting to any Porulle instance. It does not require a storefront — it’s designed for brick-and-mortar retail, pop-up shops, and hybrid online+in-store operations.
For a complete step-by-step walkthrough, see the Tea Shop POS tutorial.
Install
Section titled “Install”bun add @porulle/plugin-posRegister
Section titled “Register”import { posPlugin } from "@porulle/plugin-pos";
export default defineConfig({ plugins: [posPlugin()],});Update drizzle.config.ts:
schema: [ "./node_modules/@porulle/core/src/kernel/database/schema.ts", "./node_modules/@porulle/core/src/auth/auth-schema.ts", "./node_modules/@porulle/plugin-pos/src/schema.ts",],Push the new tables and restart:
bunx drizzle-kit push --config drizzle.config.tsbun run src/server.tsThis adds six tables: pos_terminals, pos_shifts, pos_transactions, pos_payments, pos_cash_events, pos_return_items.
Core objects
Section titled “Core objects”Terminal — represents a physical register or device. Created once and reused across shifts.
Shift — a cashier session tied to one terminal. Tracks opening float, cash events, sales totals, and cash variance. Must be opened before transactions can be created.
Transaction — a sale linked to a shift. Accumulates payments until completed or voided. Voided transactions do not count toward shift totals.
Payment — one payment method entry on a transaction. Multiple payments on a single transaction = split payment.
Quick reference
Section titled “Quick reference”| Operation | Endpoint |
|-----------|----------|
| Register terminal | POST /api/pos/terminals |
| Open shift | POST /api/pos/shifts/open |
| Create transaction | POST /api/pos/transactions |
| Add payment | POST /api/pos/transactions/:id/payments |
| Complete transaction | POST /api/pos/transactions/:id/complete |
| Void transaction | POST /api/pos/transactions/:id/void |
| Close shift | POST /api/pos/shifts/:id/close |
| Z-report | GET /api/pos/shifts/:id/report |
All monetary values are integers in the smallest currency unit (cents for USD).
Split payment
Section titled “Split payment”To split a transaction across multiple payment methods, POST multiple payments before completing:
curl -X POST http://localhost:4000/api/pos/transactions/$TXN_ID/payments \ -H "x-api-key: dev-staff-key" \ -d '{"method": "cash", "amount": 3500}'
curl -X POST http://localhost:4000/api/pos/transactions/$TXN_ID/payments \ -H "x-api-key: dev-staff-key" \ -d '{"method": "card", "amount": 1500, "reference": "****4321"}'
curl -X POST http://localhost:4000/api/pos/transactions/$TXN_ID/complete \ -H "x-api-key: dev-staff-key"PIN authentication
Section titled “PIN authentication”Cashiers share a terminal but authenticate individually with a numeric PIN. A successful PIN login mints a short-lived, per-shift API key scoped to pos:operate that the terminal then sends as x-api-key for that operator’s session. PINs live in the pos_operator_pins table, hashed with PBKDF2-SHA256 via Web Crypto (Workers-safe) — never stored in plaintext.
Set or rotate an operator’s PIN with PUT /api/pos/auth/pin (permission pos:admin). pin is 4–8 digits (/^\d{4,8}$/); set canOverride to allow this operator to approve manager overrides:
curl -X PUT http://localhost:4000/api/pos/auth/pin \ -H "x-api-key: dev-staff-key" \ -d '{"operatorId": "'$OPERATOR_ID'", "pin": "4821", "canOverride": true}'Returns { data: { operatorId, canOverride } }.
The operator logs in with POST /api/pos/auth/pin-login (permission pos:operate — the terminal’s device credential authorizes the login). The operator must have an open shift:
curl -X POST http://localhost:4000/api/pos/auth/pin-login \ -H "x-api-key: dev-staff-key" \ -d '{"operatorId": "'$OPERATOR_ID'", "pin": "4821"}'Returns { data: { operatorId, shiftId, terminalId, apiKey, expiresAt } }. apiKey is the raw minted key — scoped to pos:operate, prefixed pos_shift_, with a short TTL (default 12h, minimum 15 min). The terminal sends it as x-api-key for the rest of the session:
curl -X POST http://localhost:4000/api/pos/transactions \ -H "x-api-key: pos_shift_..."Approve a restricted action with POST /api/pos/auth/override (permission pos:operate). action is a short string describing what’s being approved (max 200). Only operators whose PIN was set with canOverride: true may approve:
curl -X POST http://localhost:4000/api/pos/auth/override \ -H "x-api-key: dev-staff-key" \ -d '{"operatorId": "'$MANAGER_ID'", "pin": "9930", "action": "void completed transaction"}'Returns { data: { approved: true, operatorId, action, approvedAt } }.
Service failures here — a bad PIN or no open shift — surface as 500 with the message, not 401/422; handle them on the 500 path.
Cash variance
Section titled “Cash variance”When closing a shift, pass the cashier’s physical count as closingCount. The engine calculates:
expected cash = opening float + all cash payments across completed transactionscash variance = closing count - expected cashA negative variance means cash is short. A positive variance means there’s more cash than expected.
Order notes & timeline
Section titled “Order notes & timeline”Every core order carries free-text notes and a merged activity timeline — useful at the counter for flagging a held order or reviewing what happened to a return. These are core endpoints (permission orders:read to read, orders:update to write), not POS-specific.
Add a note (optionally pinned to the top) and list them (pinned-first, then newest-first):
curl -X POST http://localhost:4000/api/orders/$ORDER_ID/notes \ -H "x-api-key: dev-staff-key" \ -d '{ "body": "Customer to collect Saturday", "pinned": true }'
curl http://localhost:4000/api/orders/$ORDER_ID/notes \ -H "x-api-key: dev-staff-key"The timeline merges status changes, notes, and refund events (including undos) newest-first:
curl http://localhost:4000/api/orders/$ORDER_ID/timeline \ -H "x-api-key: dev-staff-key"{ "data": [{ "type": "refund", "at": "2026-07-02T10:15:00Z", "actor": "user_9", "summary": "Refund of 1800", "data": {} }] }Delete a note with DELETE /api/orders/{id}/notes/{noteId}.
Related
Section titled “Related”- Tea Shop POS tutorial — full 10-step walkthrough
- Plugin API Reference — all POS endpoints and data types
- Refunds & Exchanges — line-level refunds, policy caps, and POS exchanges