craftivus-manager's built React bundle is physically copied into craftivus_core's own public/ and www/ folders and served by that Frappe site — it is not a separately hosted service. See Why the manager app is served same-origin.
POS Profile per till, not per outlet. This is deliberate: ERPNext's own shift/session concepts key off POS Profile, and Craftivus needed each physical till to be independently identifiable without inheriting ERPNext's assumptions about what a "profile" represents.Outlet doctype. ERPNext's POS Profile already carries company, warehouse, price list, and payment methods — everything an outlet needs — so Craftivus doesn't duplicate it.Every important record has exactly one authoritative owner. Craftivus never becomes a second, competing source of truth for something ERPNext already owns.
| Owned by ERPNext (authoritative) | Owned by Craftivus (authoritative) |
|---|---|
| Company, Item, Item Group, Item Variant, Barcode, UOM | POS Device (identity, activation, revocation) |
| Item Price, Price List, Warehouse | POS Cashier (roster, PIN auth is device-local) |
| Customer, Supplier | POS Shift (cash reconciliation) |
| Purchase Receipt, Purchase Invoice, Sales Invoice, Stock Ledger, GL | POS Correction (void/payment-fix audit trail) |
| Mode of Payment, POS Profile | Craftivus Subscription State (local cache of billing status) |
| Subscription, Subscription Plan (billing engine) | Sync/offline metadata, local-only receipt snapshots |
If you find yourself about to add a new craftivus_core doctype, check this table first — and check whether ERPNext already has a doctype that does what you need. It usually does.
No customer-side user (owner, manager, or cashier) ever sees a Frappe-rendered page — not Desk (/app), not Frappe's built-in portal. Every screen is a Craftivus-built React application talking only to craftivus_core's versioned API.
Every cashier and manager is still a real Frappe User — never a shared service account — which preserves proper permission checks and owner/audit attribution on every document. But every customer-side user has User Type = Website User, which structurally blocks Desk login regardless of Role configuration. Only Craftivus staff (via craftivus_subscription) and, opt-in, a customer's own accountant, ever get System User + real Desk access.
Why this matters for you as a developer: adding a new customer-facing capability always means adding a craftivus_core API endpoint plus a UI in craftivus-pos or craftivus-manager. It never means "just expose a Frappe report" or "just give them a portal page." If you're tempted to do the latter, you're about to violate this ADR.
craftivus-manager is a browser-based web app. A browser has no way to bypass CORS the way craftivus-pos can (see below) — so hosting it as an independent service would require the customer's ERPNext site to allow cross-origin requests from it, which is both a security and an operational headache multiplied across every customer.
Instead, craftivus-manager's build output is copied directly into the customer's own craftivus_core installation and served as static files by that same Frappe site. This buys three things at once: no CORS configuration needed, ordinary session cookies work exactly as they would for any same-origin web app, and the URL itself is the tenant boundary — shop1.craftivus.com/manager and shop2.craftivus.com/manager are simply different sites, with zero shared infrastructure to misconfigure.
tauri-plugin-http, not the browser's fetchThe Tauri webview's fetch() is still a browser fetch, subject to the same CORS restrictions as any other browser context. craftivus-pos uses tauri-plugin-http instead — a Rust-side HTTP client — so the Rust process, not the browser, makes the request. Rust has no CORS concept at all, so this sidesteps the problem entirely, and it's why the till doesn't need the same-origin trick the manager app needs: it isn't a browser making the call.
Sales Invoice, not POS InvoiceERPNext ships a POS Invoice doctype specifically for point-of-sale, but it defers stock and GL impact to a later shift-close consolidation step. Craftivus tills are offline-first and independent of each other — there's no natural moment to run that consolidation — so a POS Invoice-based sale would leave stock counts wrong for an indeterminate time. Every Craftivus sale is a plain Sales Invoice with is_pos=1, update_stock=1: stock and GL are hit immediately, atomically, the moment the sale is submitted.
POS Shift), not ERPNext's POS Opening/Closing EntryERPNext does have a native shift-opening/closing mechanism, but it's wired to is_created_using_pos — a flag only ERPNext's own native POS screen ever sets — and it hard-enforces exactly one open POS Opening Entry per POS Profile at a time. Since Craftivus never uses ERPNext's native POS screen, that mechanism simply never engages for a Craftivus sale (verified directly against ERPNext's own sales_invoice.py). Craftivus instead has its own lightweight POS Shift doctype: same one-open-shift-per-till discipline, but owned entirely by Craftivus, with its own cash-reconciliation logic, and no dependency on ERPNext internals we don't otherwise use.
make_return_doc(), and corrections on a Journal EntryReturns reuse ERPNext's own erpnext.controllers.sales_and_purchase_return.make_return_doc() — the same function that powers ERPNext's own "Make Return" button — trimmed down to whichever line(s)/quantity a partial return actually needs. Over-returning is rejected by ERPNext's own built-in validation (validate_returned_items), which fires automatically on any is_return=1 document; Craftivus does not reimplement that check.
A payment-method correction (the sale was rung up as Cash, the customer actually paid by Card) never touches the original, already-submitted invoice — Frappe won't allow it anyway, since Sales Invoice Payment has no fields marked allow_on_submit. Instead it books a real Journal Entry reallocating the amount between the two payment methods' own GL accounts, exactly as an accountant would do it by hand. The original document is never deleted or rewritten — the platform-wide rule for every correction.
Several ERPNext-internal functions (get_mapped_doc, Document.cancel(), Document.submit()) check the calling session's doctype permissions before proceeding — and a Craftivus device's session intentionally has none (every write happens through craftivus_core's own explicit, purpose-built API with ignore_permissions=True, never generic CRUD, per ADR 0001). Where this collides with an ERPNext-internal call that doesn't expose its own ignore_permissions option, the established fix is to swap frappe.session.user directly to "Administrator" for the duration of that one call, and restore it in a finally block.
Never call frappe.set_user() for this. It was verified live, and cost real debugging time, that frappe.set_user() silently destroys the calling session for the rest of the request — logging the real caller out. Always assign frappe.session.user directly instead.
Craftivus sells two different things, managed from one system (craftivus_subscription):
craftivus-pos / craftivus-manager / craftivus_core stack on the customer's own site, capped by device/outlet counts and gated feature flags.Both are Subscription records against ERPNext's own native billing engine (Subscription + Subscription Plan), distinguished by one field, service_type, on Subscription Plan. See Developer: Subscription App for the full integration contract between craftivus_subscription and each customer's craftivus_core.
Entitlements reach a customer's till through a push, not a pull: craftivus_subscription pushes status/plan changes to a local cache doctype (Craftivus Subscription State) on the customer's own site whenever something changes; bootstrap() reads that local cache. A till's ability to operate never depends on craftivus_subscription's uptime.