21-Apr-2026 — Loading member data
Two ways to populate a tenant: upload a real Insight Hub ExportAll.xlsx, or generate synthetic rows from a configurable template. Either replaces the tenant's current dataset — existing members not present in the new batch are soft-removed (their removed flag is set so consumers polling with ?since= can see the removal).
Members tab layout
Open a tenant and stay on the Members tab. The top panel shows the first 20 members currently in the dataset. Below it are two sub-tabs:
- Upload Insight Hub xlsx — push a real
ExportAll.xlsxin - Insight Hub export — pull the current dataset out as a 36-column xlsx
Synthetic generation lives on its own Generate tab, covered further down.

The Members tab shows the first 20 members. Below it, two sub-tabs — Upload Insight Hub xlsx (push data in) and Insight Hub export (pull data out).
Upload Insight Hub xlsx
Get an ExportAll.xlsx from Insight Hub's bulk download. The mock expects the standard 36-column schema documented in the #209 field table.
- Click Choose File, pick your
.xlsx - Hit Upload
While the file streams, a busy indicator runs and every action button is disabled. When the upload finishes, the page refreshes the Members preview with the first 20 rows of the newly loaded dataset and the tenant header updates to show the new last-import time and row count. There is no JSON summary surfaced in the UI; if you want the parse-and-ingest detail you can hit POST /admin/api/tenants/:code/upload directly with curl and inspect the response.
The detail response (curl path) returns four fields worth knowing about:
| Field | Meaning |
|---|---|
unmatchedHeaders |
Columns in your file that didn't match any of the 36 expected names. Typos and extras land here. Rename and re-upload if anything important is listed. |
missingHeaders |
Expected columns absent from your file. Not fatal — they come through as undefined on member records. |
warnings |
Rows skipped because they had no Mem No. or no Last Name. |
softRemoved |
Members that existed before the upload but are absent from this one — flagged with removed: true, not hard-deleted, so incremental-sync consumers can see the removal. |
Header text is matched case-insensitively with whitespace collapsed; the spelling has to be right. The parser reads the first worksheet of the workbook (the writer side, used by export and download, names the worksheet Full List to match what consumers like ngx-ramblers' member-bulk-load.ts look up by name).
Insight Hub export — download the current dataset
The second sub-tab on the Members tab. Click Download Insight Hub xlsx and the mock streams the tenant's current dataset back as InsightHub-<CODE>-<n>.xlsx, in exactly the 36-column shape Insight Hub itself produces.
Useful for:
- Round-tripping data through a third-party tool that reads
ExportAll.xlsx - Handing a tenant's snapshot to a consumer who hasn't wired up the public API yet
- Comparing two operators' datasets side by side without API access
Note: granular consent flags (groupMarketingConsent, areaMarketingConsent, otherMarketingConsent) and role assignments are stored on the member record but are not part of the 36-column Insight Hub schema, so they don't appear in the downloaded xlsx. They are preserved on the database side and continue to be served by the public API.
Generate synthetic data
If you don't have a real spreadsheet — or you want a controlled, reproducible scenario — open the Generate tab. The form has six sections.

The full Generate form — size, geography, email pattern, consent distribution (independent or joint), role proportions, and the two submit modes (import directly into the database, or download as Insight Hub xlsx).
Size and reproducibility
- Row count — 1 to 10,000. Start with a few hundred for quick iteration; 10,000 is enough to stress-test paging.
- Seed (optional) — same seed plus same options gives the same output every time. Leave blank for fresh randomness on each run.
Geography
Pool of towns and postcode prefixes that addresses are drawn from.
| Region | Towns | Postcode areas |
|---|---|---|
| Mixed UK (default) | every region's pool combined | every region's pool combined |
| Kent | Canterbury, Faversham, Maidstone, Tunbridge Wells, … | CT, ME, TN, BR, DA |
| Staffordshire | Stoke-on-Trent, Newcastle-under-Lyme, Stafford, Lichfield, … | ST, DE, WS |
| Newcastle upon Tyne and the North East | Newcastle upon Tyne, Gateshead, Sunderland, Durham, … | NE, SR, DH |
| Hampshire | Winchester, Southampton, Portsmouth, Basingstoke, … | SO, PO, RG, GU, SP |
Picking a single region is useful when you want a tenant whose addresses look believable for a specific group's catchment.
Email pattern
Four presets, plus custom. Available placeholders: {firstname}, {surname}, {nn} (a small integer disambiguator), {membershipNumber}, {groupCode}, {domain}, {base}.
| Preset | Template | Use when |
|---|---|---|
| Distinct | {firstname}.{surname}{nn}@{domain} |
You want addresses that look like real members. The domain needs a catch-all to actually receive mail. |
| Plus-addressing | {base}+m{membershipNumber}@{domain} |
You want every test member's mail to land in one shared inbox (e.g. test+m3300001@…). |
| Minimal | member{membershipNumber}@{domain} |
You want fully synthetic, obviously-fake addresses (no real names involved). |
| Custom | your own template | You need a different shape — e.g. {firstname}-{surname}@{domain}. |
The default domain is ngx-ramblers.org.uk. It's chosen because it's an unreachable address from real Brevo lists — which means accidentally pushing synthetic data into a real outbound campaign won't deliver to anyone.
Consent distribution
Two modes for the six consent flags:
Independent — each flag is rolled separately against its own probability. Two tables of sliders, grouped by where each flag rides:
- xlsx + API — the standard Insight Hub flags:
emailMarketingConsent,postDirectMarketing,telephoneDirectMarketing. These round-trip through both the public API and the Insight Hub xlsx download. - API only — granular flags:
groupMarketingConsent,areaMarketingConsent,otherMarketingConsent. Stored on the member record and returned by the public API; not in the 36-column Insight Hub schema, so absent from the downloaded xlsx.

Switching to Joint mode reveals a probability table over flag combinations. Add rows, set weights, and the running sum tells you when you've reached 100.
Joint — a probability table over flag combinations, weights summing to 100. Useful for correlated scenarios that independent rolls can't express ("give me a population where members who consent to group email always also consent to area email"). Add rows with + Add row; the running sum shows live so you can see when you've reached 100.
Role proportions (API only)
Three flags set on each member's primary groupMemberships[].roles: walkLeader, emailSender, viewMembershipData. Defaults are 8% / 5% / 3%. Like the granular consent flags, roles ride only through the public API — they are not part of the Insight Hub schema, so they don't appear in the downloaded xlsx.
Two ways to take the data away
The form has two submit buttons:
- Generate & import — generate the rows and save them into this server's database. Replaces the tenant's current dataset; the public API will now return them. Granular consent and roles survive into the DB because there's no xlsx round-trip on this path.
- Generate & download xlsx — generate the rows and stream them as a 36-column Insight Hub xlsx (
ExportAll-<tenant>-<count>.xlsx) without touching the database. Useful when a downstream consumer readsExportAll.xlsxdirectly and you just want a synthetic file to feed them.
Surnames are double-barrelled on collision
A subtlety worth knowing: synthetic data is generated against a fixed pool of first names and surnames. If your row count is high enough that the same (firstName, lastName) pair would otherwise repeat, the generator double-barrels the surname (Smith → Smith-Adams) until uniqueness is restored. This matters because consumers like ngx-ramblers enforce a unique (lastName, firstName, nameAlias) index on import; without the double-barrelling, large synthetic batches would be rejected mid-load.