RetailOSRetailOS · User Manual

RetailOS — Detailed User Manual (Forms, Fields & Connections)

Companion to the Usage Guide. The usage guide explains what the system does and how the flows work; this manual is the field-level reference: for every form it lists each input — name, type, whether it's required, its default, its validation — and, crucially, how that form connects to the others (what it looks up, what it feeds, and what happens on submit). This manual is served at /manual. Base URL: https://goretailos.com. Admin paths need a signed-in staff session; storefront paths carry a shop slug (/store/<shopSlug>/…, demo trendies-store).

How to read this manual

Each form is documented as:

ColumnMeaning
UI labelWhat the user sees on screen.
Field keyThe underlying form/database field name.
Typetext, number, money, integer, select, multi-select, toggle, date, file, textarea, color, computed.
ReqRequired (shown as ● or "Yes") · optional (○ or "No") · auto = set by the system, not typed by the user.
DefaultPre-filled value, if any.
Validation / optionsConstraints, or the choice list for selects.
NotesAnything else worth knowing.
Convention used everywhere: a field ending in _id is a pointer to another form's record. The manual names the human form (e.g. "Category") and the pointer (category_id) together.

The tenant model (read this first)

Everything hangs off one root record: the shop. Every table in the system carries a shop_id, and the server always filters by the shop you have active in the header switcher. A single login can be a member of several shops (see Team & Roles) and switch between them; data never crosses between shops. Branch-bound staff are further limited to one branch within a shop.

Because of this, shop_id (and often branch_id) is an implicit, auto field on every form below — set from your session/active shop, never typed — so the per-form tables don't repeat it unless it behaves unusually.

Data model at a glance

The forms wire together along these spines (arrow = "points at / looks up"):

Catalog spine

Selling spine

Inventory spine

Purchasing spine

Accounting spine

Storefront + AI

A full table-by-table foreign-key index is in Appendix B.


Contents

  1. Accounts, Team & Shop Settings
  2. Catalog
  3. Inventory
  4. Purchasing & Accounting
  5. Sales — POS, Registers, Orders & Sync
  6. Storefront Editor & AI (Admin)
  7. Storefront — Customer Forms

Accounts, Team & Shop Settings

Sign in / Sign up — /auth (?mode=signup)

Fields

UI labelField keyTypeReqDefaultValidation / optionsNotes
Full namefullNametext● (signup only)""Passed as options.data.full_name → user metadata → profiles.full_name. Hidden in sign-in mode.
Emailemailemail""trimmedUsed for both signup and signin
Passwordpasswordpassword""Supabase policy (min 6)
modesearch param(signin)"signup" toggles viewAlso toggled by "Create a new account" link

Connections


Forgot password — /auth (inline action)

Fields

UI labelField keyTypeReqDefaultValidation / optionsNotes
Emailemailemail""non-empty (reuses the sign-in Email input)Always shows generic "if that email has an account…" message

Connections


Reset password — /reset-password (?next=<relative path>)

Fields

UI labelField keyTypeReqDefaultValidation / optionsNotes
New passwordpasswordpassword""min 6 chars
Confirm new passwordconfirmpassword""must equal password
nextsearch param/adminmust start with / and not // (same-origin only)Redirect target after success

Connections


Onboarding — create shop — /onboarding (auth-gated)

Fields (authoritative = createShop zod schema)

UI labelField keyTypeReqDefaultValidation / optionsNotes
Shop namenametext""2–80 chars (button disabled < 2)
Store address (URL slug)slugtextauto from name2–60 chars; server re-slugifies ([^a-z0-9-]-) & checks uniquenessClient defaults to name lowercased-hyphenated
Currencycurrency_codeselect"USD"2–5 chars; USD/PKR/QAR/AED/SAR/INR/EUR/GBP
currency_symboltext● (auto)"$"1–5 charsauto — derived client-side from selected currency
Timezonetimezoneselect"UTC"UTC/Asia/Karachi/Riyadh/Qatar/Dubai/Kolkata/Europe/London/America/New_York
Default languagedefault_localeselect"en"enum en/ar/ur/hiFrom SUPPORTED_LOCALES
Storefront languagessupported_localescheckbox[]["en"]array of en/ar/ur/hi, min 1Falls back to ["en"] if none checked
Phonephonetext""≤ 40 chars
Emailemailtext""≤ 120 chars (not validated as email)
Addressaddresstext""≤ 240 chars
created_byuuidautocontext.userIdServer-set
settingsjsonbautoDEFAULT_SHOP_SETTINGSLarge default object (tax/receipt/inventory/pricing/etc.)

Connections


Accept invitation — /invite/$token

Fields

UI labelField keyTypeReqDefaultValidation / optionsNotes
tokenURL param≥ 10 charsRoute param $token
(Accept invitation button)actionrequires signed-in email == invite email (case-insensitive)Disabled/blocked if revoked, accepted, expired, or email mismatch

Preview returns (read-only, auto): status, expired, role, email, shopName.

Connections


Invite a staff member — /admin/team

Fields

UI labelField keyTypeReqDefaultValidation / optionsNotes
Emailemailemail""z.string().email(); lowercased on insert
Roleroleselect"staff"enum owner/admin/manager/cashier/accountant/inventory/staff
BranchbranchIdselect"" → nulluuid nullable; "Any branch" = nullOptions from Branches (shop-scoped)
shopIduuidautoactive shopFrom useAdminShop
originurlautowindow.location.originUsed to build invite link
tokentextautogen_random_bytes(24) hexDB-generated
statustextauto"pending"pending/accepted/revoked/expired
expires_attimestamptzautonow + 14 days
invited_byuuidautocontext.userId

Connections


Revoke invitation — /admin/team (Invitations list)

Fields

UI labelField keyTypeReqDefaultValidation / optionsNotes
(Revoke button)invitationIduuiduuidOnly shown when status='pending'
shopIduuidautoactive shop

Edit / deactivate member — /admin/team (Members list)

Fields

UI labelField keyTypeReqDefaultValidation / optionsNotes
Role (inline select)roleselectmember's currentenum owner/admin/manager/cashier/accountant/inventory/staffSaves on change
Branch (inline select)branchIdselectmember's currentuuid nullable; "Any branch"=nullOptions from Branches
Deactivate / ReactivateisActivebooleanmember's currenttoggles is_activeButton label flips on state
memberIduuidthe shop_users.id
shopIduuidautoactive shop

Connections


Shop Settings — /admin/settings

Business profile — settings section business

Writes shops columns + settings.business.tax_id.

UI labelField keyTypeReqDefaultValidation / optionsNotes
Shop namenametext (shop col)shops.nametrimmed, non-empty
Store address (slug)slugtext (shop col)shops.slugread-only"Contact support to change"
Legal / registered namelegal_nametext (shop col)"" → null
Tax ID / registration numbertax_idtext (settings.business)""Only settings-jsonb field here
Emailemailemail (shop col)"" → null
Phonephonetext (shop col)"" → null
Business logologo_urlimage upload (shop col)"" → nullvia AdminImageUpload (folder branding)Used on receipts/invoices/storefront
Addressaddresstextarea (shop col)"" → null
Timezonetimezonetext (shop col)shops.timezonefree-text

Localization — settings section localization (writes shops columns)

UI labelField keyTypeReqDefaultValidation / optionsNotes
Default languagedefault_localeselectshops.default_localeen/ar/ur/hiDrives RTL
Currency codecurrency_codetextshops.currency_codeuppercased
Currency symbolcurrency_symboltextshops.currency_symbol
Symbol positioncurrency_positionselectbeforebefore / after
Decimal placesdecimal_placesnumber20–4
Decimal separatordecimal_separatortext"."
Thousand separatorthousand_separatortext","
Date formatdate_formattext"dd/MM/yyyy"
Time formattime_formatselect"HH:mm"24h / 12h

Tax — settings section tax (+ inline tax_rates editing)

UI labelField keyTypeReqDefaultValidation / optionsNotes
Tax registeredenabledtoggletrue
Prices include taxinclusivetogglefalse
Tax registration numberregistration_numbertext""
Rounding ruleroundingselect"nearest_0.05"none / nearest_0.05 / nearest_0.10 / nearest_1

Tax rates sub-table (writes tax_rates directly): rate (number, saved on blur), is_default (radio — one default).

Receipts — settings section receipt

UI labelField keyTypeReqDefaultValidation / optionsNotes
Header textheadertextarea""
Footer textfootertextarea""
Show tax breakdownshow_tax_numbertoggletrue
Show logo on receiptshow_logotoggletrue
Roll widthroll_widthselect"80mm"58mm / 80mm
Reprint policyreprint_policyselect"allowed"allowed / manager_approval / blocked

Printer (this device only, saved to localStorage — not DB): Printer type (browser / escpos-bridge), Paper width, Bridge URL, Printer name, Auto-cut, Kick cash drawer.

Point of sale — settings section pos

UI labelField keyTypeReqDefaultValidation / optionsNotes
Default branchdefault_branch_idselect""from BranchesClears register on change
Default registerdefault_register_idselect""from Registers filtered by branch
Allow line-item discountsallow_line_discountstoggletrue
Maximum discount %max_discount_percentnumber200–100Cashier ceiling
Require customer on salerequire_customertogglefalse
Cash roundingcash_roundingselect"none"none / nearest_0.05 / nearest_0.10
Offline modeoffline_mode_enabledtoggletrue

Inventory — settings section inventory

UI labelField keyTypeReqDefaultValidation / optionsNotes
Allow negative stockallow_negative_stocktogglefalse
Default locationdefault_location_idselectdefault locationfrom Inventory locations
Low-stock threshold (default)low_stock_thresholdnumber5≥ 0
Reorder pointreorder_pointnumber10≥ 0
Reorder quantityreorder_quantitynumber20≥ 0

Pricing & discounts — settings section pricing

UI labelField keyTypeReqDefaultValidation / optionsNotes
Default markup %default_markup_percentnumber30
Price roundingprice_roundingselect"none"none / nearest_0_05 / nearest_0_50 / nearest_1 / charm_99
Max line discount % (cashier)max_line_discount_percentnumber10
Max order discount % (cashier)max_order_discount_percentnumber15
Require manager approval above limitsrequire_manager_overridetoggletrue
Allow selling below costallow_below_costtogglefalse

Purchasing — settings section purchasing

UI labelField keyTypeReqDefaultValidation / optionsNotes
Default supplier payment terms (days)default_payment_terms_daysnumber30
Require approval before sending a POrequire_po_approvaltoggletrue
Approval required above amountpo_approval_thresholdnumber00 = every PO needs approval
Allow receiving more than orderedallow_over_receipttogglefalse
Update item cost on goods receiptauto_update_cost_on_receipttoggletrue
Costing methodcosting_methodselect"weighted_average"weighted_average / last_cost

Document numbering — settings section documents

UI labelField keyTypeReqDefaultValidation / optionsNotes
Sales order prefixorder_prefixtext"SO-"
Receipt prefixreceipt_prefixtext"RCP-"
Purchase order prefixpurchase_order_prefixtext"PO-"
Stock transfer prefixtransfer_prefixtext"TR-"
Number paddingnumber_paddingnumber55 → SO-00001
Reset sequences each yearreset_yearlytogglefalse
This section writes only to shops.settings.documents. The live document_sequences rows (their own prefix/padding/next_number) are created at onboarding and are not edited here.

Business hours — settings section hours

Weekday/weekend open & close times (text, e.g. "09:00"), closed days (comma-separated), holiday notice (textarea).

Notifications & alerts — settings section notifications

Low stock alerts, Daily briefing, End-of-day summary — all toggles, default on.

Branches & registers — read-only summary

Read-only overview of branches and registers; "Manage registers →" links to /admin/registers.

Connections (Shop Settings overall)

Tables owned: shops (PK id; created_by→auth user; unique slug), profiles (PK id→auth.users), shop_users (shop_id→shops, user_id→auth user, branch_id→branches), staff_invitations (shop_id→shops, branch_id→branches, invited_by/accepted_by→auth users, unique token), branches (shop_id→shops), document_sequences (shop_id→shops, unique (shop_id,doc_type)), storefront_settings (shop_id→shops, unique). Provisioned/referenced: registers, inventory_locations, tax_rates, payment_methods, accounts, ai_permissions, storefront_navigation, storefront_policies, expense_categories, audit_logs (all shop_id→shops).


Catalog

New / Edit Product — /admin/products

Fields (product level)

UI labelField keyTypeReqDefaultValidation / optionsNotes
shopIduuidYesactive shopauto
productIduuidNonullauto; edit only
Product namenametextYes1–200 charsdrives slug + SKU seed
Categorycategory_iduuid (select)NonullEntitySelect categoriesFK → categories
Brandbrand_iduuid (select)NonullEntitySelect brandsFK → brands
Selling unitunittextNopcsmax 24server falls back to pcs
StatusstatusenumYesactive (UI)active / draftDB column default draft
Short descriptionshort_descriptiontextNonullmax 500
Full descriptiondescriptiontextareaNonullmax 20000
Tagstagsstring[]No[]≤30 tags, each 1–40comma-separated input
Tax ratetax_rate_iduuid (select)NonullEntitySelect tax_ratesresolves variant→product→category→shop default
Featuredis_featuredbooleanNofalsemerchandising flag
New arrivalis_new_arrivalbooleanNofalse
Best selleris_best_sellerbooleanNofalse
slugtextslugify(name)unique per shopauto on create only
has_variantsbooleanvariants.length>1auto computed
Media(images)sub-formNo≤30 imagessee Product Images
Variantsvariantssub-formYesmin 1see Variant Builder

Variant Builder sub-form (src/components/admin/VariantBuilder.tsx)

UI labelField keyTypeReqDefaultValidation / optionsNotes / target
iduuidNonullauto; edit only
VariantnametextNonullmax 120auto = attribute combo label ("Black / M")
SKUskutextYes<BASE>-<suffix>1–80; unique per shop; no in-product dupesauto-seeded → product_variants.sku
BarcodebarcodetextNonullmax 64; unique per shopbarcodes (is_primary); "Gen" button generates
CostcostnumberNo00–10,000,000variant_costs.cost_price & avg_cost (never public)
PricepricenumberNo00–10,000,000product_variants.price
Sale pricesale_pricenumberNonull0–10,000,000>0 → prices (type='sale'); ≤0/empty deactivates
WeightweightnumberNonull0–1,000,000product_variants.weight
Taxtax_rate_iduuid (select)Nonulltax rate listcollected + bulk-appliable but not persisted per-variant
Opening stockopening_stocknumberNo00–10,000,000create-only → inventory_movements (opening) + inventory_balances
Activeis_activebooleanYestrueproduct_variants.is_active
attributesrecord<string,string>{}from attribute matrixproduct_variants.attributes (jsonb)
is_defaultbooleanindex===0auto; first variant is default

Attribute matrix: pick an existing attribute (EntitySelect attributes) or create inline (ensureAttributeFnproduct_attributes + product_attribute_values); tick values → Generate variants builds the cartesian product. A bulk-apply bar sets price/cost/sale_price/weight/opening_stock/tax_rate_id/is_active across all or selected rows.

Product Images sub-form (saveProductImagesFn)

UI labelField keyTypeReqDefaultValidation / optionsNotes
(upload)urltextYes1–1000 charspublic URL (bucket product-images)
storage_pathtextNonullmax 400storage key
AltalttextNonullmax 200
iduuidNonullauto on existing rows
positionintarray indexauto; index 0 = cover

Connections


Barcode generate / attach — /admin/products (variant panel + builder "Gen")

UI labelField keyTypeReqDefaultValidation / optionsNotes
shopIduuidYesactive shopauto
variantIduuidYestarget variant
(Generate EAN-13 / CODE128)typeenumYesean13ean13 / code128
Attach manual barcodecodetextYes (attach)1–64; EAN-13 needs valid GS1 check digit; CODE128 ≤48duplicate active code rejected
(check digit)autoGS1 mod-10 (ean13CheckDigit)auto
sourcetextautogenerated / manualauto per path
is_primarybooleanautotrue if first activedeactivate promotes next barcode
is_activebooleanautotruesoft-delete keeps history

Generation uniqueness: barcode.server uses a shop-scoped sequence (ean13FromSequence, prefix 20); catalog.server uses random makeEan13 (prefix 200). "Bulk-generate missing" issues EAN-13 for every variant lacking one. Scan to find (Products page): lookupBarcodeFn resolves an active code to variant+product for search/POS.


Category create / edit — /admin/categories

UI labelField keyTypeReqDefaultValidation / optionsNotes
NamenametextYesrequired
Parent categoryparent_iduuid (entity)NonullEntitySelect categories; not selfFK → categories
SlugslugtextNoslugify(name)unique per shopauto when blank; dedup-suffixed
Display orderpositionnumberNo0intsort order
Default tax ratetax_rate_iduuid (entity)NonullEntitySelect tax_ratescategory-level tax fallback
Category imageimage_urlimageNonullupload folder categories
DescriptiondescriptiontextareaNonull
Visible on storefrontis_activebooleanNotruearchive toggle

Connections — self parent_id; tax_rate_id→tax_rates; used by products (category_id), storefront menu/category pages, filters. Delete blocked while referenced by products or child categories (archive instead).


Brand create / edit — /admin/brands

UI labelField keyTypeReqDefaultValidation / optionsNotes
NamenametextYesrequired
SlugslugtextNoslugify(name)unique per shopauto when blank
Brand logologo_urlimageNonullfolder brands
DescriptiondescriptiontextareaNonull
Visible on storefrontis_activebooleanNotruearchive toggle

Connections — used by products (brand_id), storefront brand filters/pages. Delete blocked while products reference it.


Attribute + values — /admin/attributes

Attribute

UI labelField keyTypeReqDefaultValidation / optionsNotes
Attribute namenametextYesrequired
CodecodetextNoslugify(name)unique per shopauto when blank

Attribute value sub-form

UI labelField keyTypeReqDefaultValidation / optionsNotes
ValuevaluetextYesrequired
attribute_iduuidYesselected attributeauto from panel scope

Connections — value attribute_id→product_attributes; feeds VariantBuilder → product_variants.attributes. Attribute delete blocked while it has values.


Tax rate create / edit — /admin/tax

UI labelField keyTypeReqDefaultValidation / optionsNotes
NamenametextYesrequiredplaceholder "VAT 15%"
Rate (%)ratenumberYes0numeric(6,3); step 0.001
Prices include this taxis_inclusivebooleanNofalse
Use as shop defaultis_defaultbooleanNofalseexclusiveauto unsets other defaults
Activeis_activebooleanNotruearchive toggle

Connections — used by products, categories, and variant/line tax resolution across POS & online; fires tax_version trigger. Delete blocked while referenced by products, categories or sold order lines; default must be reassigned before delete.


Barcode labels print — /admin/labels

UI labelField keyTypeReqDefaultValidation / optionsNotes
(variant select)selectedcheckbox setnoneper variantdrives label queue
Copiescopies[id]numberNo1min 1per selected variant
Label sizesizeIdenumYes40x3040x30, 50x25, 58roll, a4-3x8, a4-4x10mm dims from LABEL_SIZES
Show → shop name/logofields.shopbooleanNotrue
Show → product namefields.productNamebooleanNotrue
Show → variantfields.variantNamebooleanNotrue
Show → SKUfields.skubooleanNotrue
Show → pricefields.pricebooleanNotrue
Show → barcodefields.barcodebooleanNotrueformat auto-resolved (EAN13/CODE128)

Connections — reads primary barcodes.code, variant price/sku, product name, shop name/logo; output goes to window.print only.


Bulk import — /admin/import

Wizard controls

UI labelField keyTypeReqDefaultValidation / optionsNotes
TypekindenumYesproductsproducts / suppliers / inventorystep 1
File(upload)fileYes.csv, .xlsx, .xls; ≤5000 rowsparsed client-side
Column mappingmappingrecordauto-guessedfile header → target fieldper-field select

Products import target fields

UI labelField keyReqValidation / optionsNotes / target
Product nameproduct_nameYesnon-emptygroups rows into one product; slug-matched for upsert
SKUskuYesnon-empty; dedup in file; existing → updateproduct_variants.sku
Variant labelvariantNoproduct_variants.name
AttributesattributesNoColor:Red;Size:Mparsed → jsonb attributes
CategorycategoryNomatched by name (warns if not found)category_id
BrandbrandNomatched by name (warns if not found)brand_id
CostcostNonumeric, default 0variant_costs
PricepriceYesnumericproduct_variants.price
Sale pricesale_priceNonumericprices (sale)
Opening stockopening_stockNonumeric, default 0new variants only → movements+balances
BarcodebarcodeNodedup in file; cross-product clash = errorbarcodes

(Suppliers import → suppliers; Inventory import → inventory_*.) Validate cross-checks existing categories, brands, variant SKUs and barcodes, returning per-row ok/warning/error counts. Commit skips error rows, imports ok+warning, audit-logged; products grouped by name → one product with N variants.

Tables owned: products (category_id→categories, brand_id→brands, tax_rate_id→tax_rates; unique shop_id+slug), product_variants (product_id→products; unique shop_id+sku; extra DB cols not in UI: compare_at_price, track_inventory, low_stock_threshold, reorder_point, reorder_quantity), product_attributes (unique shop_id+code), product_attribute_values (attribute_id→product_attributes), product_images (product_id→products, variant_id→product_variants), prices (variant_id→product_variants; price_type='sale'), variant_costs (variant_id→product_variants; private cost), barcodes (variant_id→product_variants; unique shop_id+code, partial-unique on active; type/source/is_primary/is_active), categories (self-FK parent_id, tax_rate_id; unique shop_id+slug), brands (unique shop_id+slug), tax_rates (is_default exclusive). Import also touches inventory_movements/inventory_balances and suppliers.


Inventory

Manual stock adjustment — /admin/inventory

Fields

UI labelField keyTypeReqDefaultValidation / optionsNotes
Productvariant_idselect (uuid)Yes""variants that already have a balance row→ movement + balance variant_id
Quantity (+/-)quantitytext→numberYes""Number(quantity) must be non-zero; signedpositive adds, negative removes
ReasonnotetextNo""inventory_movements.note
Locationlocation_idautoshop's is_default locationlimit 1"No inventory location configured" if none
Movement typemovement_typeauto'adjustment'enumfixed
On-hand afterbalance quantityautoexisting + deltarunning balanceupserts inventory_balances

Connections


Reorder / stock defaults — /admin/settings (Inventory section)

UI labelField keyTypeReqDefaultValidation / optionsNotes
Allow negative stockallow_negative_stocktoggleNofalselets sales pass below zero
Default locationdefault_location_idselectNodefault locationactive inventory_locations"No default" allowed
Low-stock threshold (default)low_stock_thresholdnumberNo5≥ 0fallback when variant has none
Reorder pointreorder_pointnumberNo10≥ 0used by AI reorder insights
Reorder quantityreorder_quantitynumberNo20≥ 0suggested order size

Per-variant overrides (low_stock_threshold, reorder_point, reorder_quantity) exist on product_variants but are not editable in the current UI. These feed AI reorder recommendations and Dashboard "Low stock priority" (variant value ?? default).


Create transfer (draft → send → receive) — /admin/transfers

Header fields

UI labelField keyTypeReqDefaultValidation / optionsNotes
From locationfrom_location_idselectYes""active locations; must differ from To
To locationto_location_idselectYes""active locations, excludes From
NotesnotestextNomax 500

Line items (transferItemSchema)

UI labelField keyTypeReqDefaultValidation / optionsNotes
Productvariant_idselectYes""active variants (limit 500)
QtyquantitynumberYes""0.001–999999; rows ≤0 dropped; ≥1 line required
Avail(computed)autobalance.quantity − reserved at Fromdisplay-only guide
Unit costunit_costnumberNo00–999999in schema/server; not in create UI

Auto/computed record fields: transfer_number (nextDocNumber(..., "transfer", "TR-"), unique per shop), status (default draft; enum draft/requested/approved/shipped/received/cancelled), created_by, per-transition _by/_at stamps, and per-line received_quantity (default 0, set at receipt, editable). The Receive step sends receivedQuantities keyed by transfer-item id; Ship validates source availability.

Connections


Stock count (start → count → review → post) — /admin/stock-counts

Create fields

UI labelField keyTypeReqDefaultValidation / optionsNotes
Locationlocation_idselectYes""active locationssets expected snapshot source
Scopemode (UI only)selectYes"all"all / categorymaps to fields below
— (all)all_productsbooleantrue when scope=allseeds every variant
Category checkboxescategory_idsuuid[]cond.[]shown when scope=categoryseeds variants of those categories
NotenotetextNomax 500
variant_idsuuid[]Noin schema, not in UIexplicit variant seed

Count-sheet line items (stock_count_items)

UI labelField keyTypeReqDefaultValidation / optionsNotes
Productvariant_idautoseeded at create
Expectedexpected_qtynumeric(14,3)autobalance at creationsnapshot, not editable
Countedcounted_qtynumberYes0≥ 0editable until completed
Difference(computed)autocounted − expectedhighlighted if ≠ 0

Save progress sets status='counting', counted_by. Approve/post posts a count-type movement for every non-zero diff (qty = counted − expected), sets status='completed', approved_by, approved_at, completed_at.

Connections


Location create / edit — /admin/locations

UI labelField keyTypeReqDefaultValidation / optionsNotes
Location namenametextYesplaceholder "Main store"
Branchbranch_identity selectNonullreferences branchesFK, nullable
Default location for salesis_defaultbooleanNofalse
Activeis_activebooleanNotruearchivable

Connections — references Branch via branch_id; owns the inventory_balances/inventory_movements rows; supplies location options to transfers, counts, adjustments, and the settings default location.


Branch create / edit — /admin/branches

UI labelField keyTypeReqDefaultValidation / optionsNotes
Branch namenametextYes
CodecodetextNonull
PhonephonetextNonull
AddressaddresstextareaNonull
Default branchis_defaultbooleanNofalse
Activeis_activebooleanNotrue
business_hoursjsonb[]not in this UI

Connections — referenced by inventory_locations.branch_id and registers.branch_id; drives branch scoping for locations, registers, staff and reporting.

Tables owned: inventory_balances (unique (location_id, variant_id); FKs location_id→inventory_locations, variant_id→product_variants), inventory_movements (FKs location_id, variant_id; enum movement_type; polymorphic reference_type/reference_id), inventory_locations (FKs shop_id, branch_id→branches), inventory_transfers (unique (shop_id, transfer_number); from_location_id/to_location_id→inventory_locations; enum transfer_status), inventory_transfer_items (transfer_id→inventory_transfers, variant_id→product_variants), stock_counts (FKs shop_id, location_id; counted_by/approved_by), stock_count_items (stock_count_id→stock_counts, variant_id→product_variants), branches (shop_id→shops), registers (branch_id→branches). Reorder defaults live in shops.settings.inventory, with nullable per-variant overrides on product_variants.


Purchasing & Accounting

Two admin pages hold most of these forms: Purchasing & suppliers (/admin/purchasing) and Accounting (/admin/accounting). The master-data pages (Suppliers, Chart of accounts, Expense categories, Payment methods) share the generic MasterDataPage component → saveMasterDataFn / setMasterDataActiveFn / deleteMasterDataFn (src/lib/masterdata.functions.ts). Financial writes (invoices, payments, expenses) run through server functions gated by FINANCE_ROLES (owner, admin, manager, accountant).

Supplier — quick add — /admin/purchasing

UI labelField keyTypeReqDefaultValidation / optionsNotes
NamenametextYes""non-empty
EmailemailtextNo""
PhonephonetextNo""
payment_termstextautonet_30set even though no input is shown here

Supplier — full record — /admin/suppliers

UI labelField keyTypeReqDefaultValidation / optionsNotes
Supplier namenametextYesrequired
CodecodetextNo
EmailemailtextNo
PhonephonetextNo
Tax numbertax_numbertextNo
Payment termspayment_termsselectNocod / net_7 / net_15 / net_30 / net_60
Credit limitcredit_limitnumberNostep 0.01
AddressaddresstextareaNo
Activeis_activebooleanNotruearchive toggle

Connections — referenced by purchase_orders.supplier_id, supplier_invoices.supplier_id; delete blocked while purchase orders reference it (masterdata.server guard).


Purchase order — /admin/purchasing (New purchase order)

UI labelField keyTypeReqDefaultValidation / optionsNotes
Suppliersupplier_iduuid (EntitySelect)No"" → nullfrom Suppliers
Productvariant_idselectYes""active variants (limit 500)one line per PO in this UI
QuantityquantitynumberYes1`Number()1`
Unit costunit_costnumberYes""`Number()0`
po_numbertextautoPO-#####document_sequences prefix+padding, increments next_numberauto
statusenumautosubmittedpo_statusauto
subtotal / totalnumberautoquantity × unit_costauto
— (line)totalnumberautoquantity × unit_costauto
The AI assistant can also create multi-line POs via create_purchase_order (ai-actions.server.ts) subject to the AI permission matrix.

Connections — references Supplier (supplier_id) and Variant (variant_id); consumed by Receive goods and Supplier invoice (prefill).


Receive goods — /admin/purchasing (per-PO "Receive goods" button)

FieldTypeReqDefaultNotes
purchase_order_iduuidautothe POfrom button
location_iduuidautoshop default location"No inventory location configured" if none
receipt_numbertextautoGRN-#####document_sequences
per line variant_id, quantity, unit_costautofrom PO itemsreceived at full ordered qty

Cross-form effects — each line adds stock (inventory_movements purchase + inventory_balances), updates cost (variant_costs.cost_price/avg_cost), stamps purchase_order_items.received_quantity, and flips the PO to received.


Supplier invoice — /admin/purchasing (New supplier invoice)

UI labelField keyTypeReqDefaultValidation / optionsNotes
Suppliersupplier_iduuid (EntitySelect)Yes""from Suppliers
Invoice numberinvoice_numbertextYes""1–80 chars
Invoice dateinvoice_datetext (date)Nooptional
Due datedue_datetext (date)Nonullabledrives AP ageing / due-soon
TotaltotalnumberYes""≥ 0
Prefill from purchase orderpurchase_order_idselectNoyour POscalls createInvoiceFromSourceFn to draft from a PO/receipt
goods_receipt_iduuidNonullschema onlyalternate source
notestextNonull≤2000schema only
statustextautodraftdraft → posted → partially_paid / paid / overdue / cancelledauto
paid_totalnumberauto0auto; updated by payments

Actions: Post (draft → posted, writes the AP journal entry), Cancel (only when nothing paid). Connections — Supplier (supplier_id), optional PO (purchase_order_id) / goods receipt (goods_receipt_id); feeds AP ageing and "Outstanding by supplier".


Supplier payment — /admin/purchasing (Record a payment)

UI labelField keyTypeReqDefaultValidation / optionsNotes
Invoicesupplier_invoice_idselectYes""invoices in posted / partially_paid / overdueshows outstanding per option
AmountamountnumberYes""> 0
Methodmethod_codeselectYescashcash / bank / card
ReferencereferencetextNo""≤200nullable
paid_attext (date)autonowoptionalauto

Cross-form effects — increments the invoice's paid_total, moves it to partially_paid / paid, and posts a balanced journal entry (Dr AP / Cr Cash or Bank).


Record expense — /admin/accounting (Overview tab)

UI labelField keyTypeReqDefaultValidation / optionsNotes
Categorycategory_idselectNo"" → nullfrom Expense categoriesmaps to a ledger account
AmountamountnumberYes""> 0
NotenotetextNo""≤500; ""→null
Payment methodpayment_method_codeselectYescashcash / bankdefault cash
branch_iduuidautotop-bar Branch filternullabletaken from the page's branch filter

Cross-form effects — writes an expenses row and a balanced journal entry (Dr the category's expense account / Cr Cash or Bank). Also reachable via a cash-drawer expense on /admin/registers (see Cash movement).


Chart of accounts — /admin/accounts

UI labelField keyTypeReqDefaultValidation / optionsNotes
Account codecodetextYese.g. 1000; locked on system accounts
Account namenametextYesrequired
TypetypeselectYesasset / liability / equity / income / expense; locked on system
Parent accountparent_iduuid (entity)NonullEntitySelect accountstree self-reference
Activeis_activebooleanNotruearchive toggle

Connectionsjournal_entry_lines.account_id, expense_categories.account_id, cash_accounts.account_id, self parent_id. System accounts (is_system) are protected from deletion; the default 14-line chart is provisioned at onboarding.


Expense category — /admin/expense-categories

UI labelField keyTypeReqDefaultValidation / optionsNotes
NamenametextYese.g. "Rent"
Ledger accountaccount_iduuid (entity)NonullEntitySelect accountsexpense account this category posts to
Activeis_activebooleanNotruearchive toggle

Connections — selected on Record expense (category_id); each category maps to an accounts row via account_id.


Payment method — /admin/payments

UI labelField keyTypeReqDefaultValidation / optionsNotes
Display namenametextYese.g. "Cash"
CodecodetextYesstable id stored on payments (cash, card…)
TypetypeselectYescash / card / bank / wallet / credit / othercash types post to the drawer + cash ledger
Display orderpositionnumberNo
Enabledis_enabledbooleanNotrue

Connections — codes are referenced by POS payments, supplier payments and expenses. (Note: storefront checkout uses a hardcoded cod/bank/card list, not this table.)


Financial reports (read-only) — /admin/accounting

Tabs computed from the double-entry ledger (journal_entries / journal_entry_lines), not from UI sales totals. Shared filters: From / To date range, Branch, and (Ledger tab) Account; a Print current report button.

TabServer fnFilters
OverviewP&L + Balance sheet + Trial balancerange, branch
P&LgetProfitAndLossrange, branch
Balance SheetgetBalanceSheetas-of (range end), branch
Cash FlowgetCashFlowrange, branch
General LedgergetGeneralLedgerrange, account, limit 500
Trial BalancegetTrialBalancerange, branch
AR AgeinggetArAgeingas-of
AP AgeinggetApAgeingas-of

A ledger-integrity banner (getBalanceValidation) warns if total debits ≠ total credits in the range.

Tables owned: suppliers (shop_id→shops), purchase_orders (shop_id→shops, supplier_id→suppliers; po_status; unique (shop_id, po_number)), purchase_order_items (purchase_order_id→purchase_orders, variant_id→product_variants), goods_receipts (purchase_order_id→purchase_orders, location_id→inventory_locations; unique (shop_id, receipt_number)), goods_receipt_items (goods_receipt_id→goods_receipts, variant_id→product_variants), supplier_invoices (supplier_id→suppliers, purchase_order_id→purchase_orders, goods_receipt_id→goods_receipts; payment_status), supplier_payments (supplier_invoice_id→supplier_invoices), accounts (self-FK parent_id; account_type; is_system), journal_entries (shop_id→shops, branch_id→branches), journal_entry_lines (journal_entry_id→journal_entries, account_id→accounts), expenses (category_id→expense_categories, branch_id→branches), expense_categories (account_id→accounts), payment_methods (shop_id→shops), bank_accounts (account_id→accounts).


Sales — POS, Registers, Orders & Sync

POS sale (cart) — /admin/pos

Order-level fields

UI labelField keyTypeReqDefaultValidation / optionsNotes
Register (header select)register_iduuidNofirst open/first registermust be a shop registernull allowed
(scan box)scan/SKUtextmatches barcode or SKUEnter adds item; not persisted
Search (F2)searchtextNoclient filter, ≤60 shownnot persisted
Category chipscategorytextNoallderived from catalogclient filter
Guest / Customer (F4)customer_iduuidNonull (Guest)select from customersattach/detach
Order discount (F6)cartDiscountnumberNo0capped at gross − lineDiscountsspread evenly across lines on submit
local_uuidtext(≤80)autocrypto.randomUUID()idempotency keydedup on (shop_id, local_uuid)
notestext(≤500)Nonullin schema; not sent by POS online path
branch_iduuidNonullnot in POS UI
SubtotalgrossnumberautoΣ price×qtyauto
Discountsline+order discnumberautoΣ line.discount + orderDiscountauto
Totalsubtotalnumberautogross − discountsauto; tax added authoritatively server-side

Cart lines (UI Line; server posLine)

UI labelField keyTypeReqDefaultValidation / optionsNotes
(product)variant_iduuidYesvalid variantone row per variant
Qty (−/input/+)quantitynumberYes10.001–9999; ≤0 removes linenumeric(14,3)
Disc.discountnumberNo0≥0, max 1,000,000submitted as line.discount + perLineOrderDiscount
line amountline totalnumberautoprice×qty − discountauto

Offline snapshot (sent only on offline queue / sync retry — client_snapshot): device_id (text ≤80, marks orders.is_offline), local_seq (int), offline_created_at (ISO), and immutable client_snapshot.{lines, subtotal, discount_total, tax_total, total} — the "as-charged" record the server reconciles against; variance → pos_sync_conflicts.

Keyboard shortcuts: F2 Search · F4 Customer · F6 Order discount · F8 Hold · F9 Pay · Esc Cancel/close (↑/↓/←/→ move the grid highlight; Enter in search adds the highlighted item). Hold/Resume held sales are client-only state (no backend).

Connections


Payment screen — /admin/pos (Pay modal, F9)

Split-payment rows (UI PaymentRow; server payments[])

UI labelField keyTypeReqDefaultValidation / optionsNotes
Method tiles / Pay N selectmethod_codetext(≤40)Yescashcash, card, bank, credit, othercash→acct 1000, else→bank 1100
Pay N amountamountnumberYessubtotal (first row)≥0; only rows >0 submittednumeric(14,2)
referencetext(≤120)Noin schema; not in POS UI
statustextautocapturedauto
AllocatedallocatednumberautoΣ amountsauto
Remainingremainingnumberautosubtotal − allocatedauto
Change duechangeDuenumberautomax(0, allocated − subtotal)auto; returned as change

≥1 payment required; "Split payment" adds a row prefilled with the remaining. Complete is enabled only when fully allocated (allocated ≥ subtotal, some amount >0, subtotal >0).


Open cash session — /admin/pos header & /admin/registers

UI labelField keyTypeReqDefaultValidation / optionsNotes
(register select)registerIduuidYesshop register
Opening floatopeningBalancenumberYes0≥0rejected if register already has an open session
statustextautoopenauto
opened_by / opened_atuuid / tsautocaller / nowauto

Cash movement (in/out/expense) — /admin/registers (session panel)

UI labelField keyTypeReqDefaultValidation / optionsNotes
(session context)sessionIduuidYesselected sessionmust be open
TypemovementTypeenumYescash_incash_in, cash_out, expense
AmountamountnumberYes>0 (min 0.01)numeric
Notenotetext(≤300)Nonull
categoryIduuidNonullexpense categoryonly for expense; not in registers UI
reference_type/reference_idtext/uuidautonullset to expense/expenseIdauto when expense

Close cash session — /admin/pos header & /admin/registers

UI labelField keyTypeReqDefaultValidation / optionsNotes
(session context)sessionIduuidYesselected/open sessionmust be open
Counted cashcountedBalancenumberYes0≥0numeric(14,2)
Expectedclosing_balancenumberautoopening + cash_sales − cash_refunds + cash_in − cash_out − expensesauto
Differencediscrepancynumberautocounted − expectedauto; alert if abs>0 (critical if >5% of expected)
closed_at / statusts / textautonow / closedauto

Read-only reconciliation stats: Opening, Cash sales, Refunds, Cash in, Cash out, Expenses, Expected, Counted, Difference.


Register create / edit / assign — /admin/registers

Create/edit

UI labelField keyTypeReqDefaultValidation / optionsNotes
NamenametextYes1–120 chars
CodecodetextNonull≤40 chars
branch_iduuidNonullshop branchnot in UI
Active/Inactive (toggle)is_activebooleanNotruethe only field edit UI toggles

Assign users — per-member checkbox writes register_users (registerId + userId, idempotent, unique (register_id,user_id)).

Connections (registers) — references Register (register_id), Branch (branch_id), staff from shop_users, session (cash_session_id). An open register session gates POS selling; movements + sessions feed reconciliation and journal entries; discrepancies raise ai_alerts. One open session per register; refunds land back in the originating open session's drawer.


Return / refund — /admin/orders (order detail)

Header fields

UI labelField keyTypeReqDefaultValidation / optionsNotes
(order)orderIduuidYesorder in shop
Reasonreasontext(≤300)NonullUI sends "Customer return"
(refund method)method_codetext(≤40)Nocashcash refunds re-enter open drawerUI sends cash
Refund totaltotalnumberautoΣ proportional line totals; must be >0auto
Return numberreturn_numbertextautoper-shop uniqueauto

Return lines (server items[])

UI labelField keyTypeReqDefaultValidation / optionsNotes
(line)order_item_iduuidYesbelongs to order
QtyquantitynumberYesfull line qty≥0.001; clamped to remaining (sold − already returned)UI does full-line only
RestockrestockbooleanNotruetoggles inventory returnUI forces true; restocked units reverse COGS

The current admin.orders.tsx UI only offers "Full return & refund" (all lines, full qty, restock=true, cash). Partial qty / restock toggle / method exist in the schema but aren't surfaced.


Offline sync — queue & conflict resolution — /admin/sync

Conflict resolution fields

UI labelField keyTypeReqDefaultValidation / optionsNotes
(row)conflictIduuidYesconflict in shop
Retry / Accept / RejectactionenumYesaccept \reject \retryaccept→resolved; reject→rejected; retry→re-post (rejected) or bump attempts
Notenotetext(≤500)Nonulldisabled unless status pending/retryingstored in resolution
Kindkindtextautoprice_variance / tax_variance / rejectedauto
Statusstatustextautopendingpending/retrying/resolved/rejectedauto
Offline vs Server totalsnapshot.total / server_state.totalnumberautodisplay onlyauto
Attemptsattemptsintauto1+1 per retryauto

Local device queue ("On this device" / "Rejected on this device", IndexedDB via src/lib/offline, not a DB table): Retry resends the PendingSale payload (incl. device_id, local_seq, offline_created_at, client_snapshot) via syncOfflineSales; Discard (manager-only) drops the local pending sale/conflict.

Connections

Tables owned: orders (FKs shop_id→shops, branch_id→branches, register_id→registers, cash_session_id→cash_sessions, customer_id→customers; unique (shop_id, order_number), partial-unique (shop_id, local_uuid)), order_items (order_id→orders, variant_id→product_variants), payments (order_id→orders), receipts (order_id→orders; unique (shop_id, receipt_number)), returns (order_id→orders; unique (shop_id, return_number)), return_items (return_id→returns, order_item_id→order_items, variant_id→product_variants), refunds (order_id→orders, return_id→returns), registers (branch_id→branches), register_users (register_id→registers; unique (register_id, user_id)), cash_sessions (register_id→registers), cash_movements (cash_session_id→cash_sessions, register_id→registers; type sale/refund/expense/cash_in/cash_out/opening/closing), cash_accounts (account_id→accounts), pos_sync_conflicts (branch_id→branches, register_id→registers; unique (shop_id, local_uuid)).


Storefront Editor & AI (Admin)

All Storefront Editor forms load via getAdminStorefrontFngetAdminStorefront (src/lib/storefront-admin.server.ts), returning { shop, settings, banners, sections, navigation, policies, pages, themes } for the active shop. All writes require owner / admin / manager (assertManager). The editor is a single route with tabs (theme, banners, sections, navigation, pages, policies, contact) plus an opt-in live-preview iframe of /store/<slug>?preview=1.

Draft vs Publish model:


Theme — /admin/storefront (tab: Theme)

Saved as one draft patch. Colour fields initialise from the theme's own palette when the shop hasn't genuinely customised them (customColor / THEME_DEFAULT_SETTINGS, src/lib/storefront-theme.ts).

UI labelField key (patch)TypeReqDefaultValidation / optionsNotes
Homepage varianthomepage_variantselect"index"from themes[0].variants[].code, else index/index-2…index-5
Primaryprimary_colorcolor/hex#0A69D8free hex + pickerlegacy default #ff6a00 treated as "not customised"
Secondarysecondary_colorcolor/hex#333333legacy default #111111
Accentaccent_colorcolor/hex#ffa500legacy default #f5a623
Texttext_colorcolor/hex#333333
Backgroundbackground_colorcolor/hex#ffffff
Font family (optional)font_familytext""empty → saved as null (keeps theme typography)
Hero titlehero.titletext""packed into hero JSON
Hero subtitlehero.subtitletext""
Hero imagehero.image_urlimage URL""AdminImageUpload folder cms
Hero CTA labelhero.cta_labeltext""
Show announcement barannouncement_bar.enabledcheckboxfalse
Announcement textannouncement_bar.texttext""
Facebook / Instagram / TikTok / WhatsAppsocial_links.*text""packed into social_links JSON
Meta titleseo.titletext""packed into seo JSONfeeds storefront <title>
Meta descriptionseo.descriptiontext""feeds meta description
Meta keywordsseo.keywordstext""

Reset to theme default patches the five colours + font_family to THEME_DEFAULT_SETTINGS, leaving hero/announcement/social/SEO untouched. Still requires Publish. Persisted draft keys (SETTINGS_DRAFT_KEYS): homepage_variant, primary/secondary/accent/text/background_color, font_family, announcement_bar, header, footer, hero, social_links, seo, theme_code (header/footer/theme_code are draftable but not in this UI).


Banners — /admin/storefront (tab: Banners)

UI labelField keyTypeReqDefaultValidation / optionsNotes
iduuidnull (new)present → update
PlacementplacementselectYes"hero"hero, promo
PositionpositionnumberYes(count+1)*10int ≥ 0↑/↓ buttons
Titletitletext""nullable"(untitled)" if blank
Subtitlesubtitletext""nullable
Banner imageimage_urlimage URL""folder cms wide
Link URLlink_urltext""nullable
Visibleis_visiblecheckboxYestrue
is_draftbooleanautotrue on savecleared on publish

Sections — /admin/storefront (tab: Sections)

UI labelField keyTypeReqDefaultValidation / optionsNotes
iduuidnull
Page slugpage_slugtextYes"home"which storefront page
Section typesection_typeselectYesfeatured_categoriesfeatured_categories, featured_products, flash_deals, product_section, promotional, footer
Titletitletext""nullable
PositionpositionnumberYes(count+1)*10int ≥ 0
Visibleis_visiblecheckboxYestrue
Section imageconfig.image_urlimage URLstored in config JSON
Config (JSON)configJSON textareaYes{}must parse ("Config must be valid JSON")free-form block config
is_draftbooleanautotrue on save

Navigation — /admin/storefront (tab: Navigation)

UI labelField keyTypeReqDefaultValidation / optionsNotes
iduuidnull
LocationlocationselectYes"header"header, footer
LabellabeltextYes""min 1
URL (relative)urltextYes""empty allowed; col NOT NULLrendered as /<url>
PositionpositionnumberYes(count+1)*10int ≥ 0
Visibleis_visiblecheckboxYestrue
parent_iduuidnullself-FK for nestingnot in UI
is_draftbooleanautotrue on save

Pages — /admin/storefront (tab: Pages)

UI labelField keyTypeReqDefaultValidation / optionsNotes
iduuidnull
SlugslugtextYes""min 1unique (shop_id, slug)
TitletitletextYes""min 1
Publishedis_publishedcheckboxYesfalsecol default true"· unpublished" in list
(content)contenttextareaYes*""empty allowedbody text

Policies — /admin/storefront (tab: Policies)

UI labelField keyTypeReqDefaultValidation / optionsNotes
Pagepolicy_typeselectYes"privacy"privacy, terms, return, shipping, faqselects which row is edited
titletextautofrom POLICY_TYPES mapmin 1derived, not a visible input
(content)contenttextareaYes*existing / ""
Publishedis_publishedcheckboxYestrue

POLICY_TYPES titles: Privacy Policy, Terms & Conditions, Return Policy, Shipping Policy, FAQ.


Branding & Contact — /admin/storefront (tab: Contact)

UI labelField keyTypeReqDefaultValidation / optionsNotes
Store namenametextshop.nameonly written if non-emptyupdates shops.name
Taglinetaglinetextshop.taglinenullable
Store logologo_urlimage URLshop.logo_urlfolder brandingheader + receipts
Faviconfavicon_urlimage URLshop.favicon_urlfolder brandingbrowser tab icon
Phonephonetextshop.phonenullable
Emailemailtextshop.emailnullable
WhatsAppwhatsapptextshop.whatsappnullable
Addressaddresstextshop.addressnullable

Storefront Connections


AI (Admin) — /admin/ai

Three tabs: Assistant (chat + right-rail panels: reports, pending actions, action history, permissions, alerts), Knowledge base, Business memory. Most AI writes require owner / admin / manager; chat itself only needs active shop membership.

Assistant chat — /admin/ai (tab: Assistant)

UI labelField keyTypeReqDefaultValidation / optionsNotes
message boxmessagetextYes""1–2000 charsEnter to send
shopIduuidautocurrent shop
conversationIduuidautonull first turn, then returned idthreads the conversation
localetextautoshop.default_locale≤5

Reports: Morning briefing (getBriefing) and End-of-day summary (getEndOfDay), input {shopId} only.


Permission matrix — /admin/ai ("AI permissions" panel)

UI labelField keyTypeReqDefaultValidation / optionsNotes
(agent label)agentenumautoseededai_agent = salesman \assistantdisplay only
(capability label)capabilitytextautoseededdisplay only
mode dropdownmodeselectYescol default disableddisabled \approval \automaticthe only editable field
id, shop_iduuidautounique (shop_id, agent, capability)

Seeded capabilities (one ai_permissions row per shop): salesman → view_products / view_inventory / view_prices / recommend_products / add_to_cart / track_order (automatic), create_order (approval). assistant → view_sales / view_inventory / view_purchases / view_expenses / view_accounting / view_customers / suggest_reorders / generate_reports (automatic), create_purchase_order / adjust_inventory / record_expense / issue_refund (approval).


Approval queue — /admin/ai (Pending AI actions / Action history)

UI labelField keyTypeReqDefaultValidation / optionsNotes
Approve / RejectdecisionbuttonYesapproved \rejected
approvalIduuidautorow id
shopIduuidautocurrent shop

Row display fields: action, payload (summarised: supplier, items, total, amount, quantity, variant, reason, note), status (approval_status: pending/approved/rejected/executed, plus app-written failed), created_at, executed_at, result, error, requested_by, decided_by, decided_at, conversation_id.


Alerts — /admin/ai ("Alerts" panel)


Knowledge base — /admin/ai (tab: Knowledge base)

UI labelField keyTypeReqDefaultValidation / optionsNotes
TitletitletextYes""1–200; file upload falls back to file.name
(content)contenttextareaYes""1–200000markdown/CSV/plain
(file picker)content (from file)file.txt, .md, .csv read as text
sourcetext"manual"≤100not in UI
docTypetextserver "text"col doc_type default policynot in UI
is_activebooleanautotrue

content is split by chunkText (~800 chars, 120 overlap) into ai_knowledge_chunks (each with a generated search tsvector). Returns {documentId, chunkCount}. List row shows title, chunk_count (auto), source, created_at.


Business memory — /admin/ai (tab: Business memory)

Add memory

UI labelField keyTypeReqDefaultValidation / optionsNotes
ScopescopetextYes""1–50; col default generale.g. policy, supplier, hours
(content)contenttextareaYes""1–4000; sanitised (cards/tokens/emails redacted)
sourceenumauto"user"user-created
statusenumauto"approved" (user) / pending (AI)
confidencenumericauto1 (AI proposals 0.6)
is_activebooleanautotrue

Edit id + scope?/content?. Toggle active id + isActive. Decide AI-proposed id + decision (approved\|rejected) — only affects source='ai' AND status='pending'; approve also sets is_active=true. The UI splits into AI-proposed (pending → Approve/Reject) and Approved (Edit / Activate-Deactivate). AI reads (activeBusinessMemory) only include is_active=true AND status='approved'.

AI Connections

Tables owned:


Storefront — Customer Forms

Auth pages (sign in / up / forgot) call Supabase Auth directly from the browser (supabase.auth.*), not a server fn. Cart, wishlist, and compare are client-only, persisted in localStorage (zenis-store:<shopSlug>) via useStorefrontCart (src/lib/cart-store.ts); the DB carts / cart_items / wishlists tables are not written by any storefront route. A customers row is created lazily on first authenticated account/checkout access (getOrCreateCustomer), or by placeOnlineOrder for guests.


Customer sign in — /store/$shopSlug/sign-in

UI labelField keyTypeReqDefaultValidation / optionsNotes
EmailemailtextYes""
PasswordpasswordpasswordYes""
(Continue with Google)buttonOAuth redirect to sign-in
(Resend confirmation email)buttonshown only when error is "not confirmed"auth.resend({type:"signup"})

Connections — on success navigates to account; session drives getCustomerAccount/checkout prefill; account is scoped per shop (metadata shop_slug).


Customer sign up — /store/$shopSlug/sign-up

UI labelField keyTypeReqDefaultValidation / optionsNotes
Full namefullNametextNo""user_metadata.full_name
EmailemailtextYes""non-empty (trimmed); Supabase email format
PasswordpasswordpasswordYes""min length 6 (client)
(shop scope)shop_slugautoautoshop.sluguser_metadata.shop_slug
(Continue with Google)buttonOAuth redirect to origin

Connections — confirmed session → account; later getOrCreateCustomer inserts customers (shop_id, user_id, email). Duplicate-email surfaced as "already exists".


Forgot password — /store/$shopSlug/forgot-password

UI labelField keyTypeReqDefaultValidation / optionsNotes
EmailemailtextYes""trimmedneutral "if that email exists" message

Product page — add to cart — /store/$shopSlug/product/$slug

UI labelField keyTypeReqDefaultValidation / optionsNotes
OptionsvariantIdselect (buttons)Yes if >1 variantdefault variant (is_default) else first activeactive variants onlyhidden when single variant
QuantityquantitynumberYes1min 1; coerced max(1, n)
Add to cartbuttondisabled if no variant or out of stockwrites CartLine to localStorage
Wishlist (heart)buttontoggles wishlist list
Comparebuttontoggles compare list

CartLine written per add: variant_id, quantity, name (product + variant), sku, price, image (first product image), product_slug.

Connections — reads Product / active Variant (price, compare_at_price, track_inventory, stock); cart/checkout/wishlist/compare read the same localStorage store; stock drives disabled state.


Cart — /store/$shopSlug/cart

UI labelField keyTypeReqDefaultValidation / optionsNotes
Itemname,sku,pricedisplayread-only
QuantityquantitynumberYescurrentmin 0; <=0 removes line
Totalautoprice × quantitycomputed
(trash)buttonremoves line

Totals: Subtotal (auto, Σ price×qty client-side); note "Taxes and totals calculated securely at checkout". "Checkout" → checkout, which re-prices server-side.


Checkout — /store/$shopSlug/checkout

Contact / Billing details

UI labelField keyTypeReqDefaultValidation / optionsNotes
Full namecustomer.full_nametextYesprefilled from session1–120 charsUI blocks empty name/email
Emailcustomer.emailemailYesprefilled from sessionvalid email
Phonecustomer.phonetextNo""max 40
customer.user_iduuidautosession user idoptionalattaches order to logged-in customer

Shipping address sub-form (shipping_address, all optional, stored as JSON on order): line1 (max 200), city (max 80), postal_code (max 30), country (max 80).

Payment / meta

UI labelField keyTypeReqDefaultValidation / optionsNotes
Payment (radio)payment_method_coderadioYescodcod \bank \cardcod = unpaid/AR (1150); bank/card = prepaid → paid, Bank (1100)
— (notes)notestextNomax 500not in UI (schema-only)
— (idempotency)client_uuiduuidautocrypto.randomUUID()max 80dedupes retries via orders.local_uuid
Itemsitems[]autoYesfrom cartvariant_id uuid, quantity 0.001–9999, min 1 lineserver re-prices

Order summary (auto, from quoteCart): subtotal, tax_total, total, discount_total, and generated order_number / receipt_number.

Connections


Track order — /store/$shopSlug/track-order

UI labelField keyTypeReqDefaultValidation / optionsNotes
Order numberorderNumbertextYes""min 1; trimmed
Email / phonecontacttextYes""min 3; must match contact_email or contact_phonebutton disabled until both filled

Returns status/payment/fulfillment/tracking + items + total (read-only).


Account — Profile — /store/$shopSlug/account (tab: Profile)

UI labelField keyTypeReqDefaultValidation / optionsNotes
Full namefull_nametextNocustomer valuemax 120; ""→null
EmailemailemailNocustomer valuemax 160; ""→null
PhonephonetextNocustomer valuemax 40; ""→null

Account — Addresses — /store/$shopSlug/account (tab: Addresses)

Address sub-form

UI labelField keyTypeReqDefaultValidation / optionsNotes
iduuidautopresent = update, absent = insert
LabellabeltextNo""max 60
Address line 1line1textNo""max 200
Address line 2line2textNo""max 200
CitycitytextNo""max 100
StatestatetextNo""max 100
Postal codepostal_codetextNo""max 30
CountrycountrytextNo""max 100
PhonephonetextNo""max 40
Set as defaultis_defaultcheckboxNofalsesetting true unsets other defaults first

Connections — saved addresses are the customer's own list (not auto-injected into the checkout form, which has its own inline fields).


Account — Order history / reorder — /store/$shopSlug/account (tab: Orders)

Read-only list: Order # / Date / Status / Payment / Total (from orders); View opens the detail modal; Reorder these items re-adds each order_item (with variant_id) to the cart.


Wishlist / Compare — /store/$shopSlug/wishlist · /compare


Contact — /store/$shopSlug/contact

Tables owned: customers (shop_id→shops, user_id→auth.users, unique (shop_id,user_id)), customer_addresses (customer_id→customers), orders (customer_id→customers, unique (shop_id,order_number), partial-unique (shop_id,local_uuid)), order_items (order_id→orders, variant_id→product_variants), payments (order_id→orders), receipts (order_id→orders, unique (shop_id,receipt_number)). Defined but unused by these storefront routes (client-side localStorage instead): carts, cart_items, wishlists. Checkout payment methods are hardcoded (cod/bank/card), not from payment_methods. No product-review table/form exists.


Appendix A — Enumerations

Many forms share these fixed choice lists (defined as Postgres enum types). Where a form has a "status" or "type" select, it almost always draws from one of these:

EnumUsed byValues
shop_roleTeam & Roles, invitationsowner · admin · manager · cashier · accountant · inventory · staff
product_statusProductdraft · active · archived
order_statusOrderspending · confirmed · processing · completed · cancelled · refunded
order_channelOrderspos · online · ai
payment_statusOrders, invoicesunpaid · partial · paid · refunded
fulfillment_statusOrdersunfulfilled · processing · shipped · delivered · picked_up · returned
movement_typeInventory movementsopening · purchase · sale · return · adjustment · transfer_in · transfer_out · count
transfer_statusTransfersdraft · requested · approved · shipped · received · cancelled
po_statusPurchase ordersdraft · submitted · approved · partial · received · cancelled
account_typeChart of accountsasset · liability · equity · income · expense
approval_statusAI action approvalspending · approved · rejected · executed
ai_agentAIsalesman (storefront) · assistant (admin)

Appendix B — Tables ↔ forms index

The database has ~75 tables, all scoped by shop_id. Key foreign keys (the wiring behind the "Connections" blocks):

Pointer fieldPoints atMeaning
shop_idshopstenant scope on every table
branch_idbranchesbranch binding
category_idcategories / expense_categoriesproduct category · expense category
parent_idcategories / accounts / storefront_navigationtree self-reference
brand_idbrandsproduct brand
tax_rate_idtax_ratesproduct/line tax
product_idproductsvariant/image parent
variant_idproduct_variantsthe sellable unit on order/PO/cart/count/transfer/return lines
customer_idcustomersorder/cart owner
order_id / order_item_idorders / order_itemssale + returned line
return_idreturnsrefund parent
register_idregistersPOS till
cash_session_idcash_sessionsopen till session
location_id / from_location_id / to_location_idinventory_locationsstock location(s)
transfer_idinventory_transferstransfer lines
stock_count_idstock_countscount lines
supplier_idsuppliersPO / invoice / payment
purchase_order_idpurchase_ordersreceipt/invoice source
goods_receipt_idgoods_receiptsreceipt lines
supplier_invoice_idsupplier_invoicessupplier payment target
account_idaccountsjournal line / expense account
journal_entry_idjournal_entriesledger lines
document_idai_knowledge_documentsknowledge chunks
conversation_idai_conversationsAI messages

Per-domain table ownership is listed at the end of each section above.