Skip to main content

Customers module

The Customers tab — the first module shipped on the clean-room chassis. Self-contained: every line of code that belongs to Customers lives under src/modules/customers/; nothing leaks into the Kernel or other modules.

Tab labelCustomers
Tab accent#f43f5e (rose)
Display order10 (first business module)
Foldersrc/modules/customers/
Vertical canonSlice 2 — Customers

What it owns

src/modules/customers/
index.ts — registers the module with the Kernel
view.ts — markup + styles for the Customers screen
client.ts — browser JS (popups, ledger, filters)
api.ts — server-side routes (mutations, history)
search-domain.ts — registers Customers as a search domain with core/search

Plus its own migrations: 002_customers.sql, 003_customer_full.sql, 004_transactions.sql, 005_loyalty.sql, 006_txn_indexes.sql, 010_search_customers_fts.sql, 011_customer_family.sql, 013_customer_discounts.sql, 015_linked_accounts_and_family_members.sql.

What it does

The screen is a 3-pane work area: search input on the left, a swappable ledger in the middle (Recent visits when empty / the selected customer's interaction history when one's loaded), a read-only Till on the right.

Real dataset. v1.0.0 imported the production customer dataset at launch: 3,099 customers · 2,541 bikes · 20,360 transactions. Not a stub; not a fixture; the operator has been moving real money on this build since day one.

Search — FTS5 with BM25 ranking + synonyms. Free-text across name, phone, email, with per-token prefix matching (typing je matches jeremy) and a synonym layer (mikemichael, bobrobert, etc.) registered via search-domain.ts against core/search. The legacy LIKE search was replaced in v1.0.0 to scale past the dataset growth that broke the prior implementation.

The Till — read-only by design. The Till shows the customer's whole picture at a glance: contact, linked accounts, family members, marketing preferences, notes, equipment (bikes on record), loyalty, identity, account balance. No inline editing anywhere on the Till — every editable section has its own Edit pill in its header; click the pill, get a focused popup, save once.

This is the canonical no-inline-editing section-pill pattern — the Customers module is the first worked example; every future module inherits it.

Sections in display order (v1.1.0):

SectionEdit pill opens
ContactName, phone, email, address
LinkedAccount-to-account relationships (personal ↔ business; partners with separate accounts)
FamilyIn-card family members (spouse / children) — name, relationship, phone, minor flag
MarketingSMS / email opt-ins and topic preferences
NotesFree-form operator notes; popup-note banner surfaces on every load
EquipmentBikes on record (make / model / year / serial / colour / size)
LoyaltyLoyalty programme enrol; points balance
IdentityDOB, ID type, ID number (gated by stronger permissions)

The Ledger — keyset-paginated. Every interaction the shop has ever had with this customer: sales, service tickets, deposits, refunds, loyalty redemptions. Filterable by transaction type. Paginated by keyset (via kbPager) — no silent 500-row caps; a customer with 4,000 interactions can be fully scrolled.

Account credit + debit. Adjust the customer's account balance with audit-chain entries on every transition.

Merge. Combine two customer records — typical when an operator catches a duplicate after the fact. All sub-records (bikes, transactions, notes, family members, linked accounts) reattach.

Print — Print the customer's profile via kbPrint, the shared Core control extracted from this module in v1.0.0.

Lifecycle — archive / reactivate (soft-delete); the audit chain captures every transition.

Loyalty enrol — opt-in to the shop's loyalty programme; permission-gated (customers.loyalty).

PIPA export + erasure — BC's Personal Information Protection Act gives the customer the right to a full export of every record about them, and the right to ask for it to be erased. Both are first-class flows. See Handle customer erasure request and Handle data export request.

Discounts (read). Each customer can have an assigned discount that applies to Sales and/or Service. Discounts are authored in Settings → Discount builder and assigned per-customer here on the card. The Sales / Service tills (when built) read the customer's assigned discount at checkout.

Permissions

Every CTA + sensitive display in the module is gated, both client-side (to hide / disable affordances) and server-side (to refuse the mutation). The keyed permissions:

  • customers.read — see the tab + ledger
  • customers.edit — open any per-section popup; the Edit pills don't render without it
  • customers.delete — archive / soft-delete
  • customers.loyalty — the Enrol button hides without this permission
  • customers.export — trigger a PIPA export
  • customers.erase — trigger a PIPA erasure

Permission keys + role grants are seeded in migrations/007_permissions.sql; the resolver lives in core/perms.ts; the client cache is core/perms-client.ts. See the Permissions module of Settings for how an admin actually manages role grants.

How it composes with the Kernel

The contract is the Module interface:

// src/modules/customers/index.ts
import { register } from "../../core/registry.js";
import { customersScreen, customersStyles } from "./view.js";
import { customersClient } from "./client.js";
import { customersApi } from "./api.js";

register({
tab: { name: "customers", label: "Customers", accent: "#f43f5e", order: 10 },
screen: () => customersStyles + customersScreen + `<script>${customersClient}</script>`,
api: customersApi,
});

The Kernel doesn't know what a customer is. It calls screen() when the operator clicks the tab; it routes API requests at /api/customers/* to customersApi. That's the entire interface.

See also