From a plate read to the next molecule — and every number traceable.
A discovery workbench in the browser, on synthetic assay data: 384-well plates with their QC, dose–response curves fitted by its own 4PL engine and approved with an electronic signature, a SAR grid across targets, a multi-parameter ranking, and two analogues compared in a real crystal structure by a renderer written for this app.
Demo — not a validated GxP system — synthetic data only.

A plate was read. Something has to be made next.
Between the raw signal coming off a reader and a decision to make ten more compounds sit a dozen judgements. Each one has to be defensible months later, to someone who was not there.
A number you can follow back
Every potency on screen came from a fit, of a normalization, of wells that a reader may have masked. Each of those steps has to still be visible — with its version, its reason and its author — when someone asks in a year.
A decision, not a dashboard
Potency alone never decides anything. Selectivity, physicochemistry and predicted ADMET have to be weighed together, with the thresholds printed beside the values they judge rather than hidden in a score.
A reason the structure can show
“The pyridyl is a log unit better” is a fact. Why it is better is a question about a binding site, and answering it means looking at the molecule in one.
What the workbench holds to.
A few rules every screen keeps, because a number whose origin cannot be shown is worse than no number at all.
Raw data cannot be changed
Wells, masks, signatures and audit rows refuse UPDATE and DELETE in PostgreSQL itself. Not a rule the API remembers — a trigger that holds whatever code reaches the table, and that you can test from psql.
Every derived value names its version
Masking a well does not edit a normalization, it computes a new one. The earlier version stays readable, the QC is re-evaluated against the new one, and both steps land in the trail.
An approval locks the record
A signature binds name, meaning, UTC time and a SHA-256 of the exact record, and the database then freezes it. A correction is a new version with its own reason and its own signature.
A demo, and it says so
Not a validated system, no compliance claimed, synthetic data only — a banner on every screen, “(pred.)” on every model output, “modeled” on every pose, and zero external calls at run time.
Built and running.
Stills of the live app on its synthetic campaign: the registry, a plate under review, a curve being signed, the SAR grid, the ranking, and two analogues in the pocket.






Weighed, chosen, and why.
Each key decision, with the alternatives, the choice and its reason. Every one is written down as a decision record in the repository — including the two where the brief turned out to be wrong.
- 01
Fitting the curves
- Weighed
- A curve-fitting library · SciPy behind a service · an in-house fit
- Chosen
- An in-house 4PL: Levenberg–Marquardt with the analytic Jacobian, bounded and fixed parameters, standard errors from the covariance matrix and Student-t 95 % confidence intervals.
- Because
- A potency is the number the whole screen rests on, so the way it is computed should be readable, not imported. It is checked against SciPy's `curve_fit` on committed reference values — parameters, standard errors and intervals — so “our own” does not mean “unverified”.
- 02
Keeping raw data raw
- Weighed
- Checks in the API · an audit log beside the data · constraints in the database
- Chosen
- PostgreSQL triggers refuse UPDATE and DELETE on wells, masks, signatures and audit rows; an approved fit is frozen; the audit trail is hash-chained in the database.
- Because
- A rule enforced by the application is a rule that holds until something else writes to the table. This one you can try to break from psql and watch it refuse.
- 03
Chemistry at run time
- Weighed
- RDKit in a Python sidecar · RDKit-JS in the browser · writing it
- Chosen
- RDKit offline at build time for coordinates and properties; a TypeScript SMILES parser, canonicaliser, Morgan fingerprint and VF2 substructure matcher at run time, checked against RDKit as an oracle.
- Because
- A 12 MB WebAssembly build to canonicalise a string is a dependency with a weight problem, and the matching rules are worth understanding. The oracle caught a real one: RDKit's SMILES query matching is asymmetric — an aromatic query atom demands an aromatic partner, an aliphatic one takes either.
- 04
Drawing the molecules in 3D
- Weighed
- 3Dmol.js · Mol* · reusing Halcyon · an in-house WebGPU renderer
- Chosen
- An in-house WebGPU renderer: screen-space impostors with the sphere and cylinder intersection solved in the fragment shader, two draw calls for the whole pocket, and a Canvas 2D painter's fallback the toolbar names when it is the one running.
- Because
- The brief said to reuse Halcyon, my own 3D engine. Reading it showed that premise was wrong: Halcyon is a from-scratch Canvas 2D software rasterizer whose molecule code caps at 8,192 triangles, and a kinase pocket is far past that. What crossed over is its vocabulary — the atom and bond model, the Cordero radii, its V2000 reader — not its renderer.
- 05
Where the poses come from
- Weighed
- A docking program · published complexes · constrained embedding on a real template
- Chosen
- One CC0 crystal structure downloaded once, its pocket extracted offline, and one conformer per made compound embedded on the scaffold it shares with the crystal ligand, minimized with that core pinned and filtered for clashes.
- Because
- The protein and the frame are then real and the substituents are an estimate, which is exactly what the screen says on every view. 372 of 405 compounds cleared the clash filter; the other 33 say they have no pose rather than showing a wrong one.
- 06
Signing without a login
- Weighed
- Real accounts and passwords · a mocked login · choosing who is acting
- Chosen
- No login at all: a menu picks who is acting, that name goes on every write, and the signature dialog asks for a password it says is simulated.
- Because
- The demo is about the Part 11 mechanic — meaning, time, record hash, the lock afterwards — not about authentication. Building a login that is not real would have added the one part of the story nobody should believe.
From the generated plate to the signed result.
A generator that stands in for the instrument, an API that stands in for the registry and the ADMET service, and a database that refuses to let the raw data change.
Generator
A seeded, byte-for-byte reproducible campaign: 4,160 compounds enumerated with RDKit, 143 plates, 34,656 raw wells and 1,650 curves, with a hidden Free–Wilson potency model the tests check the fit against.
API
NestJS 12 on PostgreSQL with plain SQL and `pg`: the registry and its duplicate detection, normalizations, QC, fits and their versions, signatures, the decision log, and the hash chain — all append-only where it matters.
Front
Next 16 and React 19 with the React Compiler, Tailwind 4 on design tokens: plate heatmaps, curves and 2D depictions drawn in SVG, the pocket on the GPU, and a `default-src 'self'` CSP that would refuse an outside call.
Server
PM2 and nginx on my own server, PostgreSQL on localhost, a nightly reset to the committed seed; deploys gated by a full verify suite, registered in Zeus, watched by Iknos.
What is built, and what is next.
Every screen in the specification is live, built ticket by ticket, each decision written down as it was taken.
Built · the data
- Plates and QC384-well heatmaps in raw, % inhibition and layout, Z′, signal window, S/B and control CVs, the edge effect quantified, masking with a reason and a new normalization version.
- Curves and signaturesThe 4PL fit with confidence bands and flags, refits and corrections as new versions, approve-and-sign that locks the record in the database.
- Audit and regulatoryA hash-chained trail recomputed on demand, and a page on what a validated system would actually have to satisfy.
Built · the chemistry
- Registry and substructure searchIts own SMILES parser, canonicaliser and 2D depictions; registration with exact-duplicate and close-analogue detection; a subset-safe screen fingerprint and a VF2 matcher.
- SAR and rankingA grid across three assays with selectivity bars and predicted ADMET, and a multi-parameter score out of eight whose every desirability function is drawn on the page.
- Structures and the design systemTwo analogues in a real crystal pocket on the app's own WebGPU renderer, and a design-system page whose specimens are the live components and whose contrast figures are computed from the tokens.
Next
- Cartoon ribbonsSecondary-structure assignment and a tube mesher, so the pocket can be shown in its domain.
- A real ADMET adapterThe predictions are a mock behind the interface a real model service would implement; swapping it is one class.
- Matched molecular pairs across the setThe structural screen finds a pair when you give it two compounds; finding every pair in the series is the next step.
Mask a well,
and watch the record keep up.
Mask an outlier with a reason, watch a new normalization version appear and the QC re-evaluate, then approve the curve and try to change it. Then compare two analogues in the pocket and read the hydrogen bonds off the structure.