Status: PHASES 0 AND 1 COMPLETE, AND THE FIRST CLIENT-TIER TENANT IS LIVE, verified 2026-09-13.
Its identity model was corrected to match the GP and dental apps on 2026-09-15: test-client-pharmacy
now authenticates through its OWN Keycloak realm instead of the shared sector realm, so the apex and
client tenants can no longer reach each other's identities — see
client-realm for the evidence and the runbook for
the next client.
Phase 0: repository, sector and first tenant, Keycloak realm, app-engine stack, HTTPS. Phase 1:
pharmacy-realm identity and role gating, premises profile, GPhC/ODS capture with a verification record,
and the Responsible Pharmacist Log — built, exercised live and verified against the code, the live
site, Keycloak and the client database, with all follow-up items closed. See
phase-1-onboarding-rp-log.
OpenCode session 17 deployed and verified 2026-09-15 (ce072f0, 291 tests green independently):
migration pharmacy.0014 applied on the apex and on the client tenant, the client-org provisioning
command is live, and org_id is now consumed on the pharmacy OIDC path. The staff user-management
view was authorised the same day — see
staff-user-management.
The test client on a client-owned instance now exists — test-client-pharmacy, with its own
database, identities, host and TLS, and the Phase 1 behaviour exercised on it end to end. See
test-client for what exists, the isolation
evidence, the runbook for the next client and the gotchas.
Its data now sits on the client's own box (2026-09-13, later session): the client's database was
moved off the platform's shared client-tier Postgres instance onto the client-owned box that already
serves the dental client, reached down its own dedicated tunnel, with a ClientVPS inventory row
recorded. The client host, the app deployment and the Keycloak wiring were unchanged, and the dental
client's tunnel and database were not touched. See
client-vps for the runbook, the evidence and the
finding that matters most — a retarget needs an app restart before it takes effect.
The v1 BUILD IS COMPLETE — every build item across Phases 0–6 is delivered, deployed and verified (origin/dev =
e6a5fbb, 276 tests green). Phase 6 is 7 of its 8 acceptance criteria proven; the single gap is criterion 6,
the bookkeeper handover pack — work item A8, which is Hermes's — see
phase-6-accounting. Phases 0–6 are the whole v1 build
(the module map's next row, "Later", is DSPT-gated).
THE SATELLITE NOW DRIVES THE PORTAL — STAGES 3–4 AND THE INBOUND HALF (I1–I3) ARE BUILT, DEPLOYED AND VERIFIED
LIVE (2026-10-01). The order page's signal is contract 3 and the satellite stages the exported order file into
the portal's own upload control (FILE_DROP) or fills its quantity grid (GRID), stopping before any submission;
the price page renders an inbound signal whose download control the satellite activates, catching exactly one
new file and handing it to our own upload box. Verified by me against the live rig, through a new committed
verifier (veripath-ops c2238be) — the record is on the handback note in
agents/handoff/from-opencode. What remains on this thread: stage 5/6 and
I4/I5 — the probe-run record, and the first real wholesaler (which needs a real account and Matthew's say-so) —
plus the testing he will do over time. Before it, the satellite view was built, deployed and verified live
(2026-09-29) —
The Satellite View — Setup and Controls:
the in-app surface where a Business Manager sets the order satellite up without a terminal — the per-supplier
readiness block, the on/off switch (which turns every order page's signal to waiting with its own sixth sentence), the
browser's debugging port, a download generated from the repository's own satellite/, the instruction sheet, the
app-side export record, and a self-check panel that says "cannot tell" rather than inventing a status.
feature/pharmacy-satellite-view @ d11cbd0 (origin/dev; migration pharmacy_stock/0011), accepted after
96 live checks plus 9 host-side run by me (§9 of the spec), with the full suite green at 592 tests.
What is left on this thread is not code: Matthew's Windows pass (which decides the packaging — the download
ships provisional until then); the test rig — built, deployed, verified live and HOSTED (2026-09-30): four synthetic wholesaler portals, one per demo supplier, live over TLS at portal1–portal4.veripath.co.uk, with the build and verification record as The Synthetic Wholesaler Portals — the Test Rig (§12) — it is the asset stages 3–4 and the inbound route's I2–I3 are built and verified against, and those stages are the next build; its practical page — the addresses, what each portal does and how to re-check it — is The Synthetic Wholesaler Portals — the Test Environment; and his review of the
draft inbound scoping note (Supplier Price Downloads). W9 (the platform
administrator's tenant-by-tenant copy) stays deferred. Behind it, both built, deployed and verified live by me:
supplier portals stage 1 (the portal record and the signal) and stage 2 (the satellite skeleton — the signal
contract is version 2, carrying the order's last export time). The briefs and notes are in
agents/handoff/from-hermes.
BUILT, DEPLOYED AND VERIFIED LIVE, AND ACCEPTED (2026-10-01): the supplier register and its daily check.
The Supplier Register — MHRA WDA Licence Data
specs the MHRA wholesale dealer's authorisation register as a platform-level registry — 1,757 records and
2,902 licensed sites in the 2026-10-01 snapshot — with one supplier record per licensed site and the
trading name as the label. OpenCode imported it (release wda-2026-09-30-f33878ba, reading back 1,757 /
1,754 licence numbers / 2,902 sites) and, at Matthew's direct request, added the holder-then-site lookup to
the supplier screen with the chosen site's licence number, trading address and postcode stored on Supplier. Both
halves were verified live by me on 2026-10-01 and accepted, and the daily check is built, deployed and live
at 04:15 UTC — one GET, a hash compare, and an import only when the file has changed; my own run returned
already_held with one release before and after and no mail sent, and the register's row is the first feed in
the store to report last checked / next due rather than "release overdue". The licensing question is decided and
closed on his ruling — publish for the purpose the register serves, labelled with its source. Two deviations of
the check from its own spec are recorded on the Daily Check
and are Matthew's to settle (the failure alert against §5's "no notifications"; the relaxed file-shape gate).
On the horizon — his steer, 2026-10-01: a central place that checks every feed has happened when required.
"Given we now have other data feeds — NHSBSA, TRUD, CPE concessions — we will need a central place that checks these
feeds have happened as and when required." He will open it as its own dedicated backend topic when the pharmacy app
is close to completion. The home already exists — reference_data.tasks.check_reference_health and the
/infrastructure/reference-data/ health view — and the register's last-checked / next-due row is the pattern to
generalise across the sources; the failure alerting question above parks under that topic. Recorded on
Data Refresh Schedule §9, which also carries the store's
five source rows today (the NHSBSA notices feed and the register are missing from its §1 table).
Before it, the last verified build was PHASE 8 — the client comparison — BUILT, DEPLOYED AND VERIFIED LIVE (2026-09-27)
(Matthew approved it that day and delegated two decisions to Hermes, both recorded with reasons on the spec):
The Client Comparison — Phase 8. It puts
the derived reimbursement beside the contractor's own imported purchase price on the screens that already show a price
(the low-stock report, the expiry report, the order build — all read price_view), advisory only and never blocking an
order. Its prerequisite was the bridge from a StockItem to a dm+d pack, measured before it was specified: the
store holds no PIP attribute at all, so PIP matches a price to an item and never reaches a pack; a GTIN reaches
3,779 of the 4,439 packs carrying a figure (85.1%), a name+pack match covers some of the rest, and everything
else queues with a manual link. Live now: feature/phase8-client-comparison @ 49bd358 (migration
pharmacy_stock/0008) — 7 of the 13 catalogue rows bridge by GTIN, the other six queue with their reason, all seven
figures recompute equal to the stored rows, the resolver re-run is a no-op, the store is untouched, and the client
surfaces pass 64/64 over HTTPS. Spec §11 carries the evidence; the closure record is in
agents/handoff/from-hermes.
Still the live work alongside it: the WALKTHROUGHS, and A8. Matthew directed on 2026-09-15 that the live walkthroughs begin, from
the beginning. Agree the scope of each with him first (the standing working method is discuss before build), then
walk one at a time and stop for review. A8, the bookkeeper handover pack, is Hermes's and is the last Phase 6
acceptance criterion. Do not start anything else on the module map's "Later" row (CDs register,
DCB0129 expansion, click & collect, live NHS Spine) — every one is DSPT-gated. Procurement is the one
exception, opened deliberately by Matthew on 2026-09-28 — see supplier portals, above. The demo-readiness
gap is CLOSED (2026-09-16). The client tenant now carries all four roles — tc.rp.one, tc.rp.two,
tc.test.user (RP), tc.bm.one (Business Manager), tc.ca.one (Counter Assistant) and tc.tech.one
(Dispensing Technician, added 2026-09-16 through the app's own staff screen) — so group G, group D as
the Counter Assistant and the Technician's own view are all testable on the test model, and no guide is
blocked on a missing login. The apex tenant is not the test model and is to be removed — it does not
operate as SIAAS, having no client instance behind it (decision 2026-09-16:
apex-tenant); do not seed it further or build against it.
Identity position per realm: client-realm, and the
logins to use are on the Coverage Plan. The itemised history below is kept as the audit trail:
OPEN DESIGN QUESTION — several staff working at the same counter (raised by Matthew 2026-09-16, parked: "for
now we'll keep everything as it is"). A pharmacy has more than one person working at once: the Counter
Assistant serving customers while the Responsible Pharmacist works the prescriptions. What the app already
does, checked in the code 2026-09-16 — every act carries its own operator, so concurrent staff are
attributed individually: Basket.operator + operator_name, Tender.operator + operator_name,
Refund.actor + actor_name, TillSession.opened_by / closed_by, the dispensing acts' actor plus a name
snapshot, and PharmacyAuditEvent.actor on every audit row. The one-on-duty rule is per premises and
deliberately single: only one Responsible Pharmacist is on duty at a time. What is NOT settled: whether a
counter workstation is a shared terminal (several staff fast-switching on one device) or one device per
person; whether a till session is per operator or per counter; and whether the guides should teach
holding more than one identity at once at all. Note the distinction that prompted this: one browser holds
one identity (a Keycloak session fact — verified 2026-09-16), which is a testing constraint, not a
production one, because in production each member of staff uses their own device. No code, config or host
was changed; do not build against this until it is settled.
origin/dev = 2816aed); the9d74e53 (W4 state machine, W5 screens, W6 role gating, W7 audit, W8 seed,scripts/run_pharmacy_tests.sh gives 90 tests, OKDISPENSED is never re-opened, both actors are recorded (D6), every queryset is scoped to the0002 is applied nowhere (apex sector_pharmacy and the0001 only, read directly), and no dispensing table exists in thepharmacy_app until it restarts (restart after seeding, or reload on filetransaction.atomic() calls missing using=, raw exception textrelease talking to EPS before the local guard, the audit model having nodelete() barrier, and the mock store file belonging in the data-store inventory.0002), and themigrate_client --slug test-client-pharmacy --app-label pharmacy step for the client box.ClientVPSpharmacy_counter, C1–C13) is built, deployed and verified by me against the artifactsorigin/dev = eb1ce70, 183 tests green): the sale posts one submitted Sales Invoice whose VAT splititem_wise_tax_detail (standard 2.50/0.50, the zero-rated NHS charge 9.90/0.00), acounter:1 with no patient identity, Counter stock 60 → 59 and back to 60migrate_client's sector-appCompleted 2026-09-13 (later session) — the client's data moved to its own box. The pharmacy test
client's database now lives on the client-owned box (the one already serving the dental client), in its
own database with its own login role, reached down a dedicated tunnel of its own so the dental
tunnel never restarts. The tenant row was retargeted through the app's own seed, the pharmacy schema
applied and the synthetic data re-seeded, the RP cycle re-exercised on the live host through the
pharmacy realm, and the isolation re-checked in all three directions. Full record, runbook (including
the restart step that is easy to miss) and findings:
client-vps.
Completed 2026-09-13 (was the "next session" goal): create a test client for the pharmacy app on a
client-owned instance. What it turned out to involve: a client-sector tenant with its own database and
credentials on the client-tier Postgres instance; per-client alias routing in the sector router (a real
code change — pharmacy models previously always routed to the apex database); a migrate_client command
for the client schema; the ClientVPS.sector pharmacy choice given the migration it had been missing;
explicit Keycloak redirect URIs for the new host; two synthetic pharmacists in the pharmacy realm; a
vhost and certificate; and an isolation check on the live system.
Data refresh — where the cadence now lives (2026-09-26). The sequence each data source needs — dm+d's weekly TRUD
pull, the monthly Drug Tariff documents, CPE's ad-hoc monthly concessions and the client's own price-file uploads — is
written up as the Data Refresh Schedule, with the
human actions (D12, D10) and the open decisions (D11) called out. It is the operational companion to
Central Reference Data, which holds the plan, the
licences and the evidence. Phase 4 of that plan — the Drug Tariff ingestion — is APPROVED and specified
(D11 answered 2026-09-26: fetch-and-parse, four parameters, unattended publish behind a gate):
The Drug Tariff — Phase 4 Ingestion. When it lands, the
tariff source stops reading "Not automated yet" and the month's parameters appear in the release detail; CPE
concessions is the last part after it.
Next build (Matthew's decision, 2026-10-15): the Client Maintenance Surface.
The pharmacy app has no maintenance surface at all, so a client pharmacy cannot yet be shown the patching and
maintenance carried out on their server. It is a port, not a project: the model half is already in the database
this app uses, and the users half already exists (PHARMACY_BUSINESS_MANAGER + org_id is the client's own admin
role, and /pharmacy/staff/ already serves it). Briefed to OpenCode as note (23) on
from-hermes.
research_1 andresearch_2 — the original architectureveripath-pharmacy (mirror:tenancy.Sector / tenancy.Tenant models. Do notPharmacySectorConfig — Tenant already carries subdomainconfig JSON field and MinIO host/port. A pharmacySector row (slug='pharmacy') and Tenant rows, nothing new centrally.Checked against /root/work/gp_booking_app and /root/work/dental_booking_app. The dental app is a
fork of the GP codebase (same module tree, same settings module name dsp_clinic, same router,
plus its own dental/ app and DENTAL_APPS = {'dental'}) — so the pharmacy app is the same move a
third time: a fifth sector, not a new architecture.
Confirmed to exist and reusable
| Capability | Where |
|---|---|
| Sector-based DB router | tenancy/routers.py::SectorDatabaseRouter (partitioned GLOBAL_APPS / GP_APPS / DENTAL_APPS), wired at dsp_clinic/settings.py |
| Tenant registry | tenancy/models.py — Sector, Tenant (encrypted DB credentials, host/port, config JSON, MinIO) |
| NHS EPS (FHIR) plumbing + mock mode | clinical_data/integrations/eps_fhir.py, eps_client.py, mock_eps_fhir.py; USE_MOCK_EPS (default True) |
| Digital signing | clinical_data/integrations/aes_client.py — PAdES-B-LTA signing via the AES portal, plus a pharmacy lookup call |
| Keycloak realm provisioning | partner/management/commands/create_partner_realm.py |
| Patient notifications | services/nhs_notify.py, notifications/models.py::ReminderLog |
| ERPNext integration | integrations/erpnext_client.py (frappe REST: /api/resource/..., /api/method/...), dental/management/commands/provision_erpnext_config.py |
| Per-tenant payment config | dental/models.py::DentalPaymentConfig, appointments/models_payment.py::PaymentConfig |
| Patient PWA | separate repo /root/work/patient-portal-pwa |
Corrected — the research is wrong or off-pattern here
tabBatch / tabBin / tabStock Entry rows with raw SQL.ERPNextClient, which alreadyPharmacySectorConfig central model is redundant with tenancy.Tenant (above).pharmacy_governance app for pharmacy settings is likely redundant withTenant.config and the existing per-tenant config pattern.post_save side effects on HTTP requests.pharmacy exists with all four rolesRESPONSIBLE_PHARMACIST, DISPENSING_TECHNICIAN, COUNTER_ASSISTANT, BUSINESS_MANAGER) created/pharmacy/ at the view layer by role.users app ispharmacy-app-guides (the per-workflow analogue of dental-app-guides) is still to be minted, once2026-09-26 — the project directory was reorganised, and two obsolete pages were archived. The top level now
carries the live set only — this index, overview,
module-map,
central-reference-data,
supply-orders-part3-pricing and
regulatory-content-audit — with everything else moved
into three sub-directories so the record stays reachable without crowding the day-to-day pages:
build-records/ (the six delivered phase specifications and the build specs for supply-orders-spec,
staff-user-management, the four pricing stages and the two data-feed views), operations/ (the client-tier and
tenancy record — client-realm, client-vps, test-client, apex-tenant) and research/ (research_1,
research_2, nhsbds_data_api, expired_stock, supply_orders). Archived: the stage 2 consultation question set
(self-declared temporary — its answers are folded into the stage 2 spec, which is live) and supply_orders_part2 (an
external plan to build the same feature inside ERPNext with a custom Frappe DocType — not the route taken, and
contrary to the rule that Frappe tables are never written directly). Every inbound link was repointed, including in the
handoff archives and the skills mirror, and a full-wiki scan afterwards found no link left at a moved page's old path.
No page content was rewritten beyond the link paths. Full record: the housekeeping log, Round 11.
2026-09-14 — Phase 2 authorised and specified; Phase 1 re-verified from the live system. Matthew
authorised Phase 2 (dispensing core) and this session wrote the specification —
phase-2-dispensing — including the
pre-work gates, eleven work items, six decisions held for him, and a phase-acceptance list. The
session also re-confirmed Phase 1's behaviour against the live app rather than the record: both
pharmacy hosts render the four Phase 1 surfaces (dashboard, RP log, premises, verification) for a
pharmacy-realm identity with all four returning 200, and all four non-pharmacy roles are refused
with 403 at the view layer. Two findings were folded back into the plan and the module map: the
inherited EPS integration is prescriber-side only (no claim/dispense/endorse operation exists,
so Phase 2 must add a pharmacy-side surface rather than wire up an existing one), and the AES
client signs documents, not state transitions, which turned the Phase 2 row's "AES-signed
transitions" into a decision (D1) rather than an assumption. Corrected a stale module-map line that
still claimed the RP sign-on/off cycle had never been exercised live.
2026-09-13 (later) — The client's data moved onto the client-owned box. On the decisions taken
earlier the same day, the client's database left the platform's shared client-tier Postgres instance
for the box already serving the dental client: a dedicated role and database in the box's cluster, its
own reverse-tunnel unit (the dental tunnel has not restarted since 2026-09-11), a gateway unit on
the primary and a separate forward unit on the app engine, the tenant row retargeted through
seed_pharmacy_sector, the pharmacy schema applied with --app-label pharmacy and the synthetic data
re-seeded. Verified live: real OIDC login through the pharmacy realm, the full RP cycle (including a
refused concurrent sign-on), 2 RP log rows and 4 audit events in the box's database, and the apex
database, the dental database and the shared instance unchanged. A ClientVPS row was recorded and the
shared box's RAM drift corrected. Findings: the per-client DB alias is built once per process, so the
retarget only took effect after the app restarted — before that, writes silently continued to the old
database; the box's loopback pg_hba trust now covers two clients' data. Ops veripath-ops e442488
(two tunnel units, four scripts, bring-up wiring). Record:
client-vps.
2026-09-13 — First client-tier tenant stood up: test-client-pharmacy. A client-sector tenant
with its own database, credentials, host and TLS, and the Phase 1 behaviour exercised on it end to end
(real login through the pharmacy realm, RP sign-on, concurrent sign-on refused, history, sign-off),
with the client database and the apex database checked separately to prove the boundary. Three code
changes shipped on dev (20de069, 1caa3a9, c987d5f): client-tier alias routing in the sector
router, a migrate_client command, the missing ClientVPS.sector migration, the CSRF wildcard and
seed options for a second tenant — plus a defect fixed on the way (RP sign-on wrapped its write in a
transaction on the apex database regardless of tenant). Ops (e6bf083): the client vhost and two
idempotent provisioning scripts. Full record, runbook and gotchas:
test-client.
2026-09-12 — All outstanding small items closed. OpenCode rotated the demo credential out of
the wiki (now host-local, verified by logging in with the new value), removed the duplicated security
headers from the pharmacy and dental vhosts without losing HSTS (it had to be set in the application,
because Django was not emitting it — a good catch), added premises/verification to the navigation,
documented the test runner (Hermes then ran it: 20 tests passing), and corrected the record on demo
group membership. Hermes verified each of these against the live system and corrected its own earlier
finding about Keycloak groups.
2026-09-12 — Phase 1 exercised live; documentation started. Hermes drove the RP sign-on/off
cycle against the live site (real login through the pharmacy realm) and confirmed it in the client
database, closing the last Phase 1 verification gap; also proved the one-active-RP constraint
actually rejects a concurrent second entry. Documentation began the same day with the pharmacy guides
section, one page at a time. Matthew accepted the central-auth-users deviation and confirmed the two
Phase 1 decisions as recommended (signing deferred; GPhC verification captured with recorded
evidence).
2026-09-12 — Phase 1 built and verified. OpenCode built Phase 1 (identity/role wiring,
premises profile, GPhC/ODS capture with a verification record, the Responsible Pharmacist Log with a
database-enforced one-active-RP rule, a client-tier audit trail and a synthetic seed). Hermes
verified it against the branch, the live site (the login redirect now targets the pharmacy realm),
Keycloak and the client database, and recorded the four places the spec met reality — including the
central-auth-users deviation. Also resolved the ops working-copy drift (ops repo clean at c113447).
2026-09-12 — Phase 1 authorised. Matthew authorised Phase 1 the same day; the spec was written
and published at
phase-1-onboarding-rp-log and
briefed to OpenCode via agents/handoff/from-hermes. Two scope decisions were deliberately left held
for Matthew rather than assumed: cryptographic signing of RP log entries, and the source of GPhC/ODS
verification.
2026-09-12 — Phase 0 closed out. Matthew created the pharmacy.veripath.co.uk subdomain;
Hermes verified DNS, pre-flighted the ACME path, issued the Let's Encrypt certificate, swapped the
HTTP-only bootstrap vhost for the full HTTPS vhost and verified the result (HTTPS serving, HTTP 301
to HTTPS, security headers present, renewal path intact and the renewal timer enabled). Phase 0 is
complete; Phase 1 awaits authorisation.
2026-09-12 — Phase 0 built and verified. OpenCode forked the GP app's main into a new
private pharmacy repo (dental app and GP-only modules stripped), added the pharmacy app skeleton
and the router's data-driven SECTOR_APP_PARTITIONS registry, created the pharmacy sector and
first tenant, provisioned the Keycloak realm, clients and the four role groups, and stood up the
app-engine stack (port 8020) plus the ops wiring. Hermes verified all of it against the repository,
Keycloak, the running stack and the ops config, and folded the four build-time findings back into
the module map. Outstanding: TLS, blocked on DNS. Phase 1 not yet authorised.
2026-09-12 — planning session with Matthew: scope, module map, role map, repository strategy
and the central data model agreed; research claims verified against both application codebases
and the corrections above recorded. Module map published at
module-map. Matthew confirmed the data
boundary the same day (central tier holds client-organisation data only — see the module map),
and Phase 0 was briefed to OpenCode for its next session.
2026-09-12 — the author's draft page agents/hermes/skills/pharmacies was re-drafted into the
canonical skill mirror agents/hermes/skills/veripath-pharmacy (rendered from the installed skill
file at ~/.hermes/skills/veripath/veripath-pharmacy/SKILL.md, so the copy cannot drift); the
original is archived at
archive/agents/hermes/skills/pharmacies.