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 label | Customers |
| Tab accent | #f43f5e (rose) |
| Display order | 10 (first business module) |
| Folder | src/modules/customers/ |
| Vertical canon | Slice 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 (mike → michael, bob → robert, 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):
| Section | Edit pill opens |
|---|---|
| Contact | Name, phone, email, address |
| Linked | Account-to-account relationships (personal ↔ business; partners with separate accounts) |
| Family | In-card family members (spouse / children) — name, relationship, phone, minor flag |
| Marketing | SMS / email opt-ins and topic preferences |
| Notes | Free-form operator notes; popup-note banner surfaces on every load |
| Equipment | Bikes on record (make / model / year / serial / colour / size) |
| Loyalty | Loyalty programme enrol; points balance |
| Identity | DOB, 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 + ledgercustomers.edit— open any per-section popup; the Edit pills don't render without itcustomers.delete— archive / soft-deletecustomers.loyalty— the Enrol button hides without this permissioncustomers.export— trigger a PIPA exportcustomers.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
- Three strata — Kernel · Meta · Modules — the architectural shape
- Slice 2 — Customers — the vertical canon (what every bike-shop Customers tab does)
- Customer entity — the domain object
- no-inline-editing doctrine — the inline-edit chassis the Till uses
- Audit everything — every mutation lands in the chain