Demo — not a validated GxP system — synthetic data only

Assay & SAR workbench · Portfolio project

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.

Affinis's structural context screen: two analogues modeled in the EGFR T790M binding pocket of PDB 3IKA, drawn by the app's own WebGPU renderer, with the hinge hydrogen bonds measured at 2.8 and 3.1 Å, the gatekeeper and P-loop residues named, and the matched pair's Δ pIC50 of −1.42
01 · The need

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.

01

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.

02

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.

03

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.

02 · The rules

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.

01

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.

02

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.

03

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.

04

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.

03 · The workbench

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.

The compound registry: rows of 2D structures drawn by the app's own SVG renderer, each with its compound ID, series, R groups, formula, molecular weight, cLogP, TPSA and measured potency, above a substructure search box
The registry, with substructure search
A 384-well plate as a viridis heatmap with its QC panel: Z prime 0.86 against an accept threshold of 0.50, the signal window and control CVs, a masked well struck through, and the well inspector naming the raw value as original and read-only
A plate, its QC, and a masked well kept visible
A dose–response curve: the 4PL fit with its 95 % confidence band, replicate points differing by shape, dashed asymptotes, the IC50 drop line, and the fit table giving each parameter with its confidence interval
A 4PL fit, every parameter with its CI
The electronic signature dialog: the record and its version restated, a meaning to choose between Approved and Reviewed, the password re-entered, and the note that the result will be locked by the database on approval
Approve and sign — the record then locks
The SAR grid: one row per compound with its 2D depiction, R-group columns, potency cells shaded on a cobalt scale, fold-selectivity bars and predicted ADMET columns marked (pred.)
The SAR grid, potency and selectivity
Two analogues modeled in the EGFR T790M pocket of PDB 3IKA, drawn by the app's own WebGPU renderer: the hinge hydrogen bonds measured at 2.8 and 3.1 Å, the gatekeeper, P-loop and covalent-handle residues named, and an interactions table lining both poses up on the same protein atoms
Two analogues in the pocket, contacts measured
04 · Choices

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.

  1. 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”.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
05 · How it's built

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.

01

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.

02

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.

03

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.

04

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.

06 · Status

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.