Architecture & the hook seam
Business view
The seeder is one self-contained folder that builds a clinic in a fixed sequence of steps, from the company outward to its patients and their financial history. Its cleverest idea is a plug point — a "hook seam" — that lets the exact same files behave two ways:
- On its own (this branch): it builds a complete clinic on the base product
schema. No accounting, no payroll ledger — just the operational data the app reads.
- Alongside the accounting module (a separate, in-flight branch): the same
files gain a neighbor, accountingHooks.js, that switches on real general-ledger postings, HR/payroll seeding, and a reconciliation check.
The point of this design is merge safety. The shared files are copied byte-for-byte between the two branches. All the accounting-specific behavior lives in files that only exist on the accounting branch. So when accounting merges into this line later, nothing collides — the accounting behavior simply "turns on."
Technical view
One folder, one entry point
Everything lives under packages/server/src/generateServerData/dynamicSeed/. The entry point is index.js, run via yarn seed-dynamic. It orchestrates the pipeline and owns the top-level concerns: config parsing, the safety guard, idempotency, the step sequence, counter persistence, and the final summary/reconcile.
index.js orchestrator (main) index.js:51
config.js params + DB target + safety config.js:57, 105
countries.js SA / AE / EG presets countries.js:7
rng.js seeded Faker toolkit rng.js:15
catalog.js inventory / assets / policy tiers catalog.js
provisionBase.js S-1 company + groups + catalogs provisionBase.js:45
setupBranches.js S-2 branches, treasuries, prices setupBranches.js:70
setupPeople.js S-3 owner, doctors, staff setupPeople.js:37
setupAssets.js S-4 suppliers, stock, insurance setupAssets.js:14
generate.js S-5 patients, visits, invoices generate.js:245
money.js money primitives (shared) money.js
report.js summary + credential block + cleanup report.js
hooks.js the extension seam hooks.js
The step-by-step behavior is documented in The seed pipeline.
The hook seam
hooks.js defines a defaults object whose methods reproduce the base (pre-accounting) behavior: legacy JSON enum-slot catalogs on the company, plain treasury writes, and no GL postings. At the bottom it tries to load an optional sibling:
// hooks.js:156-165
let impl = null
try {
impl = require('./accountingHooks')
} catch (e) {
const optionalMiss = e.code === 'MODULE_NOT_FOUND' &&
String(e.message).includes('accountingHooks')
if (!optionalMiss) throw e // a broken accountingHooks must fail loudly
}
export const hooks = { ...defaults, ...(impl ? impl.accountingHooks : {}) }
Two important properties:
- Graceful absence, loud failure. A missing
accountingHooks.jsis the
normal generic case. But any other load error — a syntax error, or one of accountingHooks's own imports missing — re-throws, so a half-present accounting module can't silently degrade into wrong data.
- Merge over replace. The accounting implementation overrides only the
methods it cares about; everything else stays the shared default.
The generators never branch on "is accounting present?" — they just call hooks.postInvoiceCreated(...), hooks.injectCapital(...), etc. On this branch those are no-ops or plain operational writes; on the accounting branch they add GL legs. This is why the same generate.js produces a self-consistent clinic in both worlds.
The merge invariant
Both index.js:18-21 and hooks.js:12-13 carry the rule in a header comment: the shared files must stay byte-identical across branches, and accounting behavior may enter only through accountingHooks.js (plus its private helpers ledger.js, setupHr.js, accountingCoverage.js, reconcile.js, which don't exist here). Editing a shared file here without mirroring it there would break the no-conflict merge.
What the hooks cover
The default (base) hook surface, all in hooks.js:36-153:
| Hook | Base behavior | Accounting behavior |
|---|---|---|
companyCreateExtras | legacy JSON enum-slot catalogs | Category taxonomy columns |
provisionCompanyModules | no-op | Category tree + Chart of Accounts |
enableFeatureFlags | no-op | turn on FEATURE_ACCOUNTING_MODULE |
loadCategoryContext | null category ids | real Category rows |
createSalaryProfile | no-op | Salary-Hub profile per user |
injectCapital | incoming treasury txn | Dr Cash / Cr Capital |
acquireAsset | paid equipment expense | FixedAsset + GL |
post* (invoice/payment/expense/inventory/commission/wallet) | no-ops | GL legs |
runHr / runCoverage | return null (skipped) | HR + payroll/claims/depreciation |
reconcile | { ok: true }, "skipped" | Δ0 GL reconciliation gate |
The operational bookkeeping (subledger rows + balances the UI reads) always happens in money.js; only the GL leg is hook-gated. That separation is the whole reason both branches stay consistent — detailed in The money model.