pharmacopeia

Roadmap

v0.2.0-scale

The changelog and the backlog, in one place. Everything below is rendered from a single typed file, so adding a new entry is one append, not a JSX edit.

Last shipped · Jump to changelog

89Shippedv0
1In progressActive now
0NextCommitted
10LaterPlanned

In progress

1

What we're actively building right now.

Data

  • EMA (European Medicines Agency) jurisdiction added alongside US-FDA

    DataStarted

    Multi-jurisdiction is additive: same drug, additional jurisdiction-tagged records. EMA is the obvious first non-US source. Groundwork shipped: /v1/drugs?jurisdiction= filters by regulatory agency on both backends (validated against the jurisdiction enum), so EMA records land without an API change. The EMA ingest source itself is still queued.

    • jurisdiction

Shipped

89

The changelog — what's done, with dates.

Platform

  • Read path scales to the 5,000+ drug dataset

    Platform✓ Shipped

    Browse pages, pickers, and the sitemap stopped assuming the dataset fits in one response: filtering and pagination moved server-side (?q= on every list endpoint, URL-driven browse pages), pickers query the API with debounced lookups instead of preloading hundreds of records, and the sitemap walks the full dataset in pages. One canonical search-haystack module keeps static and Postgres filtering identical.

    • scale
    • pagination
  • Derived views precomputed at seed time

    Platform✓ Shipped

    Brands, the ATC tree, and the mechanism graph are pure functions over the whole dataset — at 5,000+ drugs, building them per process means loading every payload on cold start. Seeding now materialises them into a derived_views table and the Prisma repository serves the precomputed row, falling back to building from the snapshot only when a row is missing. Same pure builders either way, so the two paths can't disagree.

    • scale
    • postgres
  • Per-key rate limiting + quotas

    Platform✓ Shipped

    Every key-gated endpoint now enforces two layers per key: a requests-per-minute window (in-memory fixed window, best-effort per instance) and a precise daily quota counted in Postgres on the key row, rolling over at UTC midnight. Limits are per-key columns (npm run keys:create -- --rate-limit N --daily-quota N), reported via X-RateLimit-* and X-Quota-* headers on every response, and exceeded requests get a 429 with Retry-After and error code rate_limited. Env-var keys keep the minute window (PHARMACOPEIA_RATE_LIMIT_PER_MINUTE) but carry no quota — zero-db deployments have nowhere durable to count.

    • billing
    • api-keys
  • Stage 1: Supabase Postgres backend with Prisma, switching getRepository() based on env

    Platformstage-1✓ Shipped

    Real database behind the same repository interface. Document-style tables keep the Zod schemas as the single source of truth (each row is a validated jsonb payload plus extracted filter/search columns), PrismaRepository serves every endpoint when DATABASE_URL is set, and the static seed stays as the zero-config local fallback. /api/v1/health reports which backend answered.

    • prisma
    • supabase
    • postgres
  • Next.js 16 App Router + TypeScript + Tailwind v4 + shadcn/ui scaffold

    Platformv0✓ Shipped

    The base every other line of code stands on. App Router gives us per-route server components, Tailwind v4 collapses the design tokens into CSS, and shadcn keeps the component surface small and ownable.

  • Repository pattern with seed-backed implementation

    Platformv0✓ Shipped

    Routes depend on getRepository(), never on seed files directly. Swapping seed for Supabase later is a single-file change instead of a sweep across every handler.

Data

  • Scale the catalog to the full FDA-labeled set (2,577 drugs)

    Data✓ Shipped

    Pushed the dataset past the long tail of common medications: from a hand-curated 310 to 2,577 FDA-labeled mono-substance drugs in Postgres. A programmatic candidate universe built from RxNorm's full ingredient space (npm run ingest:universe, ~14k candidates tiered curated → prescribable → extended), a concurrent checkpoint-resumable ingest (npm run ingest:scale) sharing the exact record builder with the curated seed, an openFDA-label publish gate that holds unlabeled candidates in review.ndjson (~10.7k) instead of the public dataset, and NDJSON artifacts that db:seed loads straight into Supabase while the bundle keeps the small curated fallback. 2,577 is effectively the full set of FDA-labeled single-substance products; pushing toward 5,000+ from here means a deliberate scope expansion into combination products and review-queue promotion, tracked separately.

    • rxnorm
    • openfda
    • ingest
    • supabase
  • Per-drug enrichments scaled to the full catalog

    Data✓ Shipped

    The six per-drug enrichment layers stopped trailing the curated 310 and now cover the whole 2,577-drug scale dataset. A shared module (scripts/ingest/enrich-shared.ts) sources the universe from the scale NDJSON, writes each layer as its own NDJSON artifact that db:seed loads (no TS-seed bloat), and checkpoints the long per-drug API walks so a sleeping laptop never costs more than the in-flight drug; PHARM_DATASET=static still forces the curated path. Coverage in Postgres: 2D structures 304 → 1,773, FAERS adverse-event snapshots 308 → 2,220, PubMed literature 275 → 1,522, FDA shortages 260 → 606 entries, CPIC pharmacogenomics 123 → 249, ClinicalTrials.gov crosswalks scaling from 310 to the full set.

    • scale
    • pubchem
    • openfda
    • pubmed
    • cpic
    • clinicaltrials
  • Orange Book crosswalk (therapeutic equivalence, generics)

    Data✓ Shipped

    FDA's Orange Book is free, public-domain data. A conservative curated crosswalk (lib/ingest/orange-book.ts, same fill-only pattern as the ICD-10 and DEA crosswalks) attaches a therapeutic-equivalence summary to the drug record — TE code, whether a Reference Listed Drug exists, and whether an AB-rated generic is marketed — surfaced as an `orangeBook` field and a header badge. Applied identically at ingest, db:seed, and the static fallback so both backends agree. Per-product patent and exclusivity expiry dates move on FDA-update timescales and are deliberately left to a future authoritative ingest rather than hand-curated. Reference regulatory facts only — never substitution guidance.

    • orange-book
    • fda
    • generics
  • Conditions as a first-class entity (indication → drugs reverse index)

    Datav0✓ Shipped

    Every drug already carries indications and an ICD-10 crosswalk, but there was no way to land on a condition and see every drug indicated for it. Shipped a /conditions browse + /conditions/{slug} detail and GET /api/v1/conditions + /api/v1/condition/{slug}, built by transposing data already in the dataset — the same derived-view move that turned FAERS rows into the /reactions directory. Large SEO surface, zero new data dependency, reference-only framing (indications are labeled uses, never a treatment recommendation).

    • conditions
    • icd10
    • reverse-index
    • seo
  • DEA controlled-substance schedule badge

    Datav0✓ Shipped

    DEA schedules I–V are public structured facts. Shipped a curated name → schedule crosswalk (same conservative, fill-only pattern as the ICD-10 crosswalk) surfacing a schedule on the drug record and an amber badge near the drug header, like the existing FDA-shortage badge. Reference fact only — never prescribing or diversion-control guidance.

    • dea
    • crosswalk
    • controlled-substances
  • Ingest pipeline runs on schedule (Vercel cron) and writes deltas to Supabase

    Datastage-1✓ Shipped

    Daily Vercel cron hits /api/cron/refresh-shortages (CRON_SECRET-gated) to re-pull openFDA drug shortages straight into Postgres — the fastest-moving dataset is now hands-off. The crosswalk logic is shared with the seed pipeline via lib/ingest/shortages.ts, and a content hash over the rebuilt dataset skips the table rewrite when upstream hasn't changed. Heavier surfaces (labels, FAERS) join the schedule as they get the same delta treatment.

    • cron
    • openfda
  • ICD-10 + SNOMED CT (where licensable) crosswalks on indications

    Data✓ Shipped

    ICD-10-CM codes (public domain) now ride on indications via a curated keyword crosswalk in lib/ingest/icd10.ts — conservative by design: precision over recall, fill-only, never overwrites richer codes from a later extraction stage. Applied identically at ingest, at db:seed, and in the static fallback. SNOMED CT stays pending licensing review (IHTSDO member-country scope).

    • icd10
    • crosswalk
  • Rotating FAERS refresh on the cron schedule

    Data✓ Shipped

    openFDA caps requests, so 5,000 drugs can't refresh in one run. The cron route (/api/cron/refresh-adverse-events, CRON_SECRET-gated) walks the next window of drugs per invocation in slug order — a cursor persisted in cron_state that wraps at the end — so the whole dataset cycles continuously without any single run being heavy. Delta-aware: an aggregate whose provenance.sourceHash is unchanged is skipped without a write, and a transient openFDA miss never deletes an existing row. Same shared-module rule as shortages: per-record logic lives in lib/ingest/, used by both the script and the route.

    • cron
    • openfda
    • faers
  • Drug → trial crosswalk via ClinicalTrials.gov API

    Data✓ Shipped

    Every drug now carries its registered studies from the ClinicalTrials.gov v2 API: the most recently updated registrations naming the drug as an intervention (query.intr, so an abstract mention is not enough) plus the registry's total match count. New /api/v1/drug/{slug}/trials endpoint, a Clinical trials section on each drug page, and a get_drug_trials MCP tool — all carrying the framing inline: registration is NOT evidence of efficacy or safety. Ingest (npm run ingest:trials) is idempotent, checkpoint-resumable, and scaled from the curated 310 to the full catalog.

    • clinicaltrials
  • Pharmacogenomic markers and CYP450 interactions

    Data✓ Shipped

    CPIC-curated drug–gene pairs joined onto the dataset by RxCUI from the public CPIC API (the licensing-clean source in this space): per pair the gene symbol, CPIC level (A–D), ClinPGx/PharmGKB clinical annotation level (1A–4), the FDA-label PGx testing annotation where one exists, and a link to the published guideline. New /api/v1/drug/{slug}/pharmacogenomics endpoint, a Pharmacogenomics section on each drug page (249 drugs across the full catalog have pairs), and a get_drug_pharmacogenomics MCP tool. Framed as evidence metadata throughout — never testing or dosing guidance.

    • pgx
    • cpic
  • Zod schemas as single source of truth for every entity

    Datav0✓ Shipped

    Runtime validation and TypeScript types both derive from one schema per entity, so a malformed seed record or response fails fast at the boundary instead of leaking into the UI.

    • zod
  • Per-record provenance (sourceUrl, sourceHash, extractedAt, extractor, confidence) on every entity

    Datav0✓ Shipped

    Every record carries an audit trail so users can see where a fact came from, when it was extracted, and how confident the pipeline was. This is the unlock for selective refresh later.

  • Real-data ingest pipeline: RxNav (NIH) + openFDA, 310 drugs / 730 classes / 310 ingredients

    Datav0✓ Shipped

    The seed dataset is real public-source data joined from RxNav and openFDA. Idempotent re-runs keyed on (rxcui, hash) mean upstream refresh is one command. 100% openFDA label coverage; 100% mechanism (label + class-derived); 99% indications; 96% ATC; 93% brands; 93% contraindications.

    • rxnav
    • openfda
    • ingest
  • Depth pass: FDA label sections, approval history, NDC/UNII, derived MOA targets

    Datav0✓ Shipped

    Beyond the headline narrative, each record now carries verbatim openFDA label sections — boxed warning, dosage & administration, warnings & precautions, adverse reactions, use in specific populations, and overdosage — plus product NDCs and the UNII identifier from the same label. Approval history (application number, type, original approval date, sponsor) comes from the openFDA drugsfda endpoint. Mechanism targets are derived from the drug's MOA RxClass memberships, and drugs with no labeled mechanism narrative get a classification-style mechanism summary so the section is rarely empty. Coverage across the 310: dosage 99%, adverse reactions 93%, approval history 99%, derived targets 84%, boxed warnings 38%. All additive, all free-source, no schema break.

    • openfda
    • drugsfda
    • ingest
    • provenance
  • Structural analogs via Tanimoto similarity over 2D fingerprints

    Datav0✓ Shipped

    Every drug page surfaces its closest structural analogs, precomputed offline with OpenChemLib's 512-bit substructure index and the Tanimoto coefficient over the PubChem SMILES we already ingested. Large peptides/biologics are excluded (the fingerprint saturates and would link any two peptides), and the threshold is tuned for precision so same-class families — ACE inhibitors, benzodiazepines, beta blockers, PPIs — cluster cleanly. Exposed at /api/v1/drug/{slug}/similar. Structural proximity only, never therapeutic equivalence.

    • pubchem
    • openchemlib
    • chemistry
  • Molecular structure 2D diagrams from PubChem SMILES via OpenChemLib (1,773 drugs)

    Datav0✓ Shipped

    Drug pages render the actual 2D structure from a PubChem-sourced SMILES, pre-generated via OpenChemLib and post-processed to follow `currentColor` so bonds stay legible in both palettes. Scaled from the curated 310 to the full catalog (1,773 single-component structures rendered); biologics, salts, and multi-component mixtures are skipped where no single-component SMILES is meaningful.

    • pubchem
    • openchemlib
  • PMID literature crosswalks via PubMed

    Datav0✓ Shipped

    Per-drug PubMed reference lists pinned to MeSH major topic for precision. New /api/v1/drug/{slug}/literature endpoint and a Literature section on each drug page. Ingest pipeline (`npm run ingest:literature`) hits the NCBI E-utilities (esearch + esummary), respects the 3 req/s no-key throttle, and uses NCBI_API_KEY for the 10 req/s tier. Records carry title, journal, year, first three authors, optional DOI, and a pre-built pubmed.ncbi.nlm.nih.gov URL.

    • pubmed
    • ncbi-eutils
  • Aggregate adverse-event counts from openFDA FAERS

    Datav0✓ Shipped

    Per-drug top-N reactions from openFDA `/drug/event` aggregated by `patient.reaction.reactionmeddrapt.exact`. New /api/v1/drug/{slug}/adverse-events endpoint and a FAERS section on each drug page, both shipped with an inline `disclaimer` field and a heavy framing block on the UI: these are voluntarily-submitted REPORTS, not incidence rates, not signals, not causality. Counts reflect reporting volume only. Each row now also surfaces the share (count ÷ total matched reports) so the reader sees both raw volume and relative weight in one glance. Ingest pipeline (`npm run ingest:adverse-events`) refreshes per drug; throttled to keep the openFDA endpoint happy.

    • openfda
    • faers
  • MeSH definitions and PubMed literature per reaction

    Datav0✓ Shipped

    Every reaction page now ships a `meta` block sourced from NLM MeSH and PubMed — never authored in-house. The block carries the matching MeSH descriptor id (e.g. D003967), the librarian-written scope note as a quoted definition, the tree position with linkable parent descriptors, and a small set of recent PubMed papers indexed under the descriptor as a MeSH major topic. Surfaced as a top-of-page Definition block, a tree-position aside, and a Literature section; null for MedDRA administrative terms ("Drug Ineffective", "Off Label Use") that have no MeSH counterpart. A new `npm run ingest:reaction-meta` script drives a throttled E-utilities crawl with batched parent UID resolution. The schema-level guardrail stays the same: pharmacopeia quotes NLM content verbatim, never paraphrases, and the Literature section is explicit that the citations are about the reaction term itself — not about any specific drug.

    • mesh
    • pubmed
    • faers
    • definitions
  • FDA drug-shortage status crosswalk

    Datav0✓ Shipped

    openFDA `/drug/drugshortages` joined onto each drug record by generic name. New endpoints (/api/v1/drug/{slug}/shortages and /api/v1/shortages), an amber `FDA shortage` badge near the drug page header when at least one entry is active, and a dedicated Shortages section listing every reported presentation with status, sponsor, reason, and FDA-updated date. Independent refresh cadence (`npm run ingest:shortages`) since the upstream list moves on business-day timescales. Reference statistics only.

    • openfda
    • shortages

API

  • Typo-tolerant search (trigram fallback)

    API✓ Shipped

    When exact substring search comes up empty, a shared trigram-similarity scorer (lib/data/fuzzy-search.ts) re-ranks candidate names so 'metfornin' still finds metformin. The Jaccard-over-3-grams scorer mirrors Postgres pg_trgm but runs in JS on both backends, fed the same name-only candidates — so /search returns identical fuzzy results whether the data lives in the bundle or Postgres. Pinned by a unit suite and a both-backends contract test.

    • search
  • Dataset time-travel (?asOf= snapshots + record history endpoint)

    API✓ Shipped

    Leans directly on the provenance investment instead of adding a version store. GET /api/v1/drug/{slug}/history returns the current snapshot's provenance plus the drug's change-event timeline (drawn from the same feed as /changelog, filtered to the entity), and ?asOf= trims that timeline to a past instant. The drug endpoint also accepts ?asOf= to gate by extraction time — a record extracted after the instant 404s, so a consumer can pin the dataset to a date. Field-level payload time-travel stays future work behind a version store; this surface reports existence and change timing from data already on hand.

    • provenance
    • history
    • snapshots
  • Bulk dataset export (downloadable NDJSON dumps + /data page)

    APIv0✓ Shipped

    The most "PokeAPI for X" gap: no way to grab the whole corpus in one shot. Shipped streaming NDJSON exports of drugs, classes, and ingredients at stable URLs, plus a /data page documenting them — reusing the same repository every endpoint reads from, so a dump can never disagree with the live API. Takes scraping pressure off the per-record endpoints and makes the project genuinely forkable.

    • export
    • ndjson
    • bulk
  • LLM citation/grounding API tier (paid)

    API✓ Shipped

    First key-gated tier: POST /v1/grounded returns the same passage retrieval as /v1/semantic-search, repackaged for LLM consumers — every passage carries a citation id and a full-coverage character-span → citation mapping, and every citation carries the record's provenance (source URL, content hash, extraction timestamp, confidence) plus a permalink. Keys are minted by npm run keys:create (sha256-at-rest, shown once) or supplied via PHARMACOPEIA_API_KEYS for zero-db deployments; db-backed keys track lifetime request counts surfaced in the response usage block.

    • llm
    • billing
    • api-keys
  • Webhooks on entity changes

    API✓ Shipped

    Outbound webhooks (drug.created / drug.updated / drug.deleted / dataset.refreshed) fire when the dataset load detects record changes by provenance hash, so downstream caches can invalidate instead of polling /changelog. Consumers register HTTPS endpoints via the key-gated /v1/webhooks API; deliveries are HMAC-SHA256 signed (Stripe-style t=<ts>,v1=<hex> over <ts>.<body>), logged per attempt, and an endpoint auto-disables after 25 consecutive failures.

    • webhooks
    • hmac
  • v1 API routes for stats, drugs, classes, ingredients, search

    APIv0✓ Shipped

    Public JSON surface under /api/v1 covers reading drugs, classes, ingredients, pairwise interaction checks, and full-text search — the contract the rest of the project is being built against.

  • Typed response envelope schemas for every endpoint

    APIv0✓ Shipped

    Each endpoint's payload shape now lives in lib/schemas/responses.ts as Zod, and handlers are annotated with `satisfies` against it. The response contract is as type-safe as the entities, and it's the single definition both the API and the SDK generator read from.

    • zod
    • contract
  • OpenAPI 3.1 document generated from the schema registry

    APIv0✓ Shipped

    Every v1 endpoint is described in a generated sdk/openapi.json, emitted from the same Zod response schemas the handlers validate against. It's the source the SDK clients are built from and the seed for an interactive API reference.

    • openapi
    • codegen
  • ETag + conditional GET (304 Not Modified) on read routes

    API✓ Shipped

    Every GET response is hashed into a strong ETag and short-circuits to 304 Not Modified when the client sends a matching If-None-Match. Layered with the existing Cache-Control policy so CDNs and clients revalidate cheaply.

    • caching
    • easy-win
  • Health/version endpoint (GET /api/v1/health)

    API✓ Shipped

    A tiny liveness + dataset-version envelope so monitors and consumers can confirm the API is up and which snapshot they're hitting, without parsing a real payload. Schema lives next to the others and ships in the generated SDKs.

    • easy-win
  • MCP server (pharmacopeia-mcp) for Claude / Cursor / Codex

    APIv0✓ Shipped

    A standalone npm package (pharmacopeia-mcp) that wraps the generated TypeScript client as a Model Context Protocol server. Hosts spawn it over stdio with `npx -y pharmacopeia-mcp`; every tool is a thin proxy over the live API contract (search, get_drug, get_drugs_batch, check_interactions, structure_search, get_drug_shortages, get_drug_adverse_events, get_drug_literature, etc.), so the tool surface can't drift from the API. Released in lockstep with the TS / Python SDKs via the same GitHub Actions workflow.

    • mcp
    • claude
    • cursor
    • agents
  • Batch lookup endpoint (POST /api/v1/drugs/batch)

    APIv0✓ Shipped

    Resolve up to 100 drug slugs in one round trip. Body is { slugs: string[] }; the response carries the full Drug records that resolved and a separate `missing` array for the slugs that did not, so callers never have to diff request and response themselves. Duplicates collapse server-side. Picked up by the OpenAPI doc and both SDK clients automatically through codegen.

    • easy-win
  • Field selection (sparse fieldsets via ?fields=)

    APIv0✓ Shipped

    GET /api/v1/drug/{slug}?fields=mechanism,interactions,labelSections drops the un-requested sections from the response — identity fields (slug, name, classes, identifiers, provenance) always come back. Heavy verbatim FDA label sections, the full indications list, and approval history are the main payload-savers. Additive and backwards-compatible: omit the param and the response is the full record exactly as before. Documented on the SDK manifest so both TS and Python clients accept the parameter.

    • easy-win
  • GraphQL surface alongside REST

    APIv0✓ Shipped

    A thin GraphQL layer (powered by graphql-yoga) over the same repository the REST API uses. One round-trip can fetch a drug, its mechanism, its interactions, and its structural analogs — exactly the fields the caller selects. GraphiQL IDE at /api/graphql.

    • graphql
    • graphql-yoga
  • Reactions directory (MedDRA Preferred Terms → drugs)

    APIv0✓ Shipped

    A first-class /reactions browse + /reactions/{slug} detail surface (and matching /api/v1/reactions and /api/v1/reaction/{slug} endpoints) that transposes the FAERS top-N reactions across every drug in the dataset. Each reaction page ranks the drugs reporting it by share of their matched reports, ships a 'related reactions' panel ranked by Jaccard similarity over drug-id sets (purely derived graph density — no paid-licence MedDRA SOC mapping required), and 301-redirects American-English alias slugs (Diarrhea → Diarrhoea, Anemia → Anaemia) to their canonical counterparts. Drug-page FAERS rows now link out to the reaction pages, closing the drug ↔ reaction ↔ drug loop. Deliberately framed as a reverse index of reporting volume, NOT a symptom checker and NOT diagnostic guidance — every page and the schema-level `disclaimer` field carry that framing forward to SDK and MCP consumers.

    • faers
    • meddra
    • seo
    • graph
  • /api/v1/health reports repository backend + deployment commit

    APIv0✓ Shipped

    The liveness envelope now includes which repository implementation is serving (`static` for the seed fallback, `supabase` for the real backend) plus the short git SHA from VERCEL_GIT_COMMIT_SHA and the serving region from VERCEL_REGION when available. Monitors can finally distinguish 'up on the seed' from 'up on the real backend', and clients can correlate responses with a specific deployment.

    • easy-win
    • ops

UI

  • Patient-facing summary mode (plain-language toggle)

    UI✓ Shipped

    Toggle on every drug page that swaps clinical prose for a plain-language view at roughly an 8th-grade Flesch-Kincaid reading level. The simplification is a deterministic glossary transform over the same provenanced label text — never a paraphrase — with every swapped phrase highlighted and the original clinical term on hover. Provenance and disclaimer stay.

    • readability
    • plain-language
  • Apothecary palette: warm parchment light + warm ink dark

    UIv0✓ Shipped

    Custom palette tuned for long-form reference reading instead of generic shadcn neutral. The dark mode leans warm so chemical structures and code samples stay legible.

  • Custom theme switch with View Transitions API clip-path reveal

    UIv0✓ Shipped

    Theme toggle uses the View Transitions API to wipe the new palette from the cursor outward — a small touch that turns a routine control into a memorable one.

  • Outfit + Inter + Geist Mono typography stack

    UIv0✓ Shipped

    Outfit for display, Inter for body, Geist Mono for code and identifiers. Three families is the most we can spend before page weight gets noticeable.

  • Landing page with hero, live stats counters, code samples

    UIv0✓ Shipped

    First-impression page sells the API in three glances: what it is, how big the dataset is right now, and what a request looks like.

  • Browse pages for drugs and classes

    UIv0✓ Shipped

    Flat index pages that let humans discover what the API already exposes, instead of forcing them to read /api/v1/drugs JSON.

  • Detail pages: drug, drug class, with full content sections

    UIv0✓ Shipped

    Per-drug pages render mechanism, indications, dosing, pharmacokinetics, identifiers, and interactions as readable sections — the same shape that ships through the API.

  • ATC classification explorer

    UIv0✓ Shipped

    A /atc page that organises the WHO Anatomical Therapeutic Chemical hierarchy by anatomical main group, with each subgroup linking to its class record and member drugs. The 14 level-1 groups are anchored from the canonical WHO set since RxClass only returns the deeper subgroups.

    • atc
    • rxclass
  • Provenance badge UI with AI-extracted / auto-sourced / curated classification

    UIv0✓ Shipped

    Each fact is tagged with how it got here. A reader can tell at a glance whether a sentence came from an LLM extraction, a deterministic API join, or a human review.

  • Animated aurora hero background

    UIv0✓ Shipped

    Subtle motion behind the landing hero. Disabled under prefers-reduced-motion so it never becomes a vestibular trigger.

  • Stat-card count-up animations on scroll

    UIv0✓ Shipped

    Cards tween from zero to their real value when they enter the viewport, gated by IntersectionObserver and prefers-reduced-motion. The numbers feel earned.

  • Floating sticky ToC on drug detail and docs pages

    UIv0✓ Shipped

    Right-rail table of contents tracks the active section as you scroll. Long reference pages stop feeling like an infinite scroll.

  • /ingredients browse + detail pages

    UIv0✓ Shipped

    Ingredient records were already in the API; this exposes them with a browse index, a per-ingredient detail page (identifiers, structure where shared with the drug, and every drug that contains it), and ingredient-side filtering on /api/v1/drugs.

  • /interactions interactive check UI

    UIv0✓ Shipped

    Interactive multi-select that posts to /api/v1/interactions/check, renders severity-graded pairs and a per-severity summary, and links to the per-drug openFDA narrative for the (still empty) pair-graph gap.

  • Pagination + client filter on drugs/classes/ingredients lists

    UIv0✓ Shipped

    Every browse page now paginates (24 per page, windowed control with first/last + neighbors) and the client filter scopes to the full dataset. Ready for 5,000-drug scale without changing the UI when server-side pagination lands.

  • Sync viewport.themeColor (dark) and manifest.theme_color to the warm-ink palette

    UIv0✓ Shipped

    Mobile browser chrome and installed-PWA chrome now match the apothecary warm-ink. The seam between OS chrome and the page is gone in dark mode.

  • Real apple-touch-icon + maskable PWA icon + Safari mask-icon

    UIv0✓ Shipped

    Generated 180×180 apple-touch and 32×32 favicon via Next file-based ImageResponse, plus static SVG + maskable SVG + monochrome Safari pinned-tab — replacing the create-next-app placeholder set.

  • Interactive MoA graph + fully expandable ATC tree (levels 1–5)

    UIv0✓ Shipped

    The /atc page is now a fully expandable WHO ATC tree from anatomical main group down to substance, with the intermediate level-2/3 WHO group names filled in. A new /moa page renders a D3 force-directed mechanism-of-action network — drugs, MoA classes, and molecular targets as nodes — backed by GET /api/v1/atc and GET /api/v1/mechanisms/graph. Educational structural views, never clinical.

    • d3
    • atc
    • visualization
  • Side-by-side drug comparison view at /compare

    UIv0✓ Shipped

    A /compare surface that puts two or three drugs next to each other across the canonical reference axes — identity, classes, mechanism, indications, identifiers, 2D structure, and pairwise interactions between the selected set. The selection lives in ?drugs=a,b,c so a comparison is a shareable URL. Reference contrast only — no "better than" / "preferred over" language.

  • Brand → generic crosswalk index page

    UIv0✓ Shipped

    A dedicated /brands index that lets users land on a brand name (Glucophage) and pivot straight to the generic (metformin) with the full record. Built from RxNorm brand concepts already in the dataset.

SEO

  • SEO pass: full metadata, canonical URLs, robots, manifest, sitemap

    SEOv0✓ Shipped

    Every page exports a real Metadata object and canonical URL. Sitemap is generated from the repository so new entities surface to crawlers automatically.

  • JSON-LD structured data: WebSite + Organization + Drug + TechArticle + BreadcrumbList

    SEOv0✓ Shipped

    Structured data so Google can render rich results for drugs and so the breadcrumb trail in SERPs matches the on-page trail.

  • Dynamic OG images via next/og ImageResponse

    SEOv0✓ Shipped

    Every page gets a generated 1200×630 social card with the right title and subtitle — no manual PNG juggling.

  • X-Robots-Tag noindex on API responses

    SEOv0✓ Shipped

    JSON endpoints carry X-Robots-Tag: noindex so search engines don't index raw API responses and dilute the canonical page URLs.

  • llms.txt for AI-agent discoverability

    SEO✓ Shipped

    Publishes /llms.txt and a longer /llms-full.txt following the llmstxt.org convention. Both are generated live from the API manifest and the repository, so they always reflect the current endpoint surface and dataset counts.

    • llms
    • easy-win

Accessibility

  • Accessibility + Web Interface Guidelines audit of the public site

    Accessibility✓ Shipped

    Audited the layout, header, navigation, theme toggle, browse shell, search, code blocks, and the interactive MoA graph against the Web Interface Guidelines. The surface already holds up: a skip link and `main` landmark, labelled controls, `:focus-visible` rings everywhere, decorative icons `aria-hidden`, accessible names on every icon-only button, `prefers-reduced-motion` honoured, explicit (never `transition: all`) transitions, and a `role="img"` accessible name plus per-node labels on the SVG graph. No defects found; the audit stands as the baseline to hold the line against.

  • WIG (Web Interface Guidelines) audit pass

    Accessibilityv0✓ Shipped

    Skip-link, focus-visible rings, motion-reduce on every transition, color-scheme meta, tabular-nums on numeric columns, and translate="no" on drug identifiers so Google Translate doesn't mangle them.

Docs

  • Docs page with endpoint reference and quickstart

    Docsv0✓ Shipped

    Self-contained reference: quickstart code, conventions, every endpoint with a one-line description, and the trust indicator legend.

  • FAQ and glossary pages with FAQPage + DefinedTermSet structured data

    Docsv0✓ Shipped

    A /faq page answers the recurring "is this clinical / where's the data from / can I use it" questions, and a /glossary defines the domain terms (ATC, MoA, Tanimoto, provenance, RxCUI). Both emit schema.org JSON-LD (FAQPage and DefinedTermSet) so the answers and definitions are eligible for rich results.

    • seo
    • content
  • Interactive API reference rendered from the OpenAPI document

    Docs✓ Shipped

    Live OpenAPI 3.1 document served at /api/v1/openapi.json and rendered as a browsable, try-it reference (Scalar) at /reference. Built from the same Zod schemas the handlers validate against, so the spec, the SDK clients, and the live API can't drift.

    • openapi
    • easy-win
  • "What's new" changelog page + RSS 2.0 + JSON Feed

    Docsv0✓ Shipped

    A public /changelog page plus /feed.xml (RSS 2.0) and /feed.json (JSON Feed 1.1), all reading from the same listChangelog repository method so the three surfaces can't drift. Entries are typed (drug / class / ingredient / interaction / structure / dataset / endpoint × added / updated / removed / released) and the HTML page advertises both feeds via <link rel="alternate"> so subscribers don't have to guess the URL.

    • rss
    • json-feed

DevX

  • Every public endpoint covered by a route-handler test

    DevX✓ Shipped

    Extends the initial six-endpoint suite to the whole v1 surface — catalog, derived views (ATC tree, MoA graph, brands), structure and semantic search, per-drug subresources, reactions, and the changelog — each asserted for status, cache headers, and a schema-valid body. GraphQL gets execution + REST-parity tests so the two read surfaces can't diverge. The suite now runs 170 tests across 16 files.

    • vitest
    • coverage
  • Automated guards against SDK/OpenAPI/route drift

    DevX✓ Shipped

    Tests that fail the build when the SDK manifest, the generated OpenAPI 3.1 document, and the actual route files disagree: every manifest operation resolves to a real handler that exports its method, every request/response schema exists in the registry, every $ref resolves, and the committed openapi.json is regenerable byte-for-byte. Drift between the spec, the SDKs, and the live API is now a red CI check instead of a silent lie.

    • openapi
    • sdk
    • codegen
  • Route tests across the whole public surface

    DevX✓ Shipped

    Extend the route suite beyond the core read path: semantic-search and grounded (auth + envelope), webhooks (key gating), shortages, adverse events, reactions, brands, ATC, mechanism graph, changelog, stats, and the OpenAPI document. Every contract the SDKs generate against gets a test that fails when a handler drifts. Landed as the 170-test, 16-file suite (v1-catalog, v1-routes, v1-tools, webhooks, structure-search, reactions-index, graphql, api-surface, response, and the pure-module tests) wired into CI.

    • vitest
  • Go SDK + self-host Docker image

    DevX✓ Shipped

    The schema-driven codegen pipeline now emits a third client: typed Go structs + a client (sdk/go) generated from the same Zod registry as the TS and Python SDKs, so it can't drift either. And a multi-stage Dockerfile on Next.js standalone output makes the "forkable open reference" story concrete — docker build, docker run, and the app serves the static seed with no env vars (or a real Postgres backend when DATABASE_URL is set). Self-host instructions live in the README.

    • sdk
    • go
    • docker
    • self-host
  • Postman / Insomnia collection generated from the API manifest

    DevXv0✓ Shipped

    A Postman v2.1 collection emitted from the same OPERATIONS manifest the SDKs and OpenAPI document are built from, so import-and-try stays in lockstep with the live surface and can never drift. One more codegen target off the artifact already maintained — no hand-curated request lists.

    • postman
    • codegen
    • easy-win
  • Vitest test suite + GitHub Actions CI

    DevX✓ Shipped

    Unit tests for the pure modules (passages, dataset views, sparse fieldsets, rate limits, webhook signing), a repository contract suite that pins StaticRepository and PrismaRepository to identical behaviour, and route-handler tests for envelopes, ETags, and error shapes. CI runs typecheck + tests on every push and PR so regressions surface before deploy, not after.

    • vitest
    • ci
  • .env example with NEXT_PUBLIC_SITE_URL

    DevXv0✓ Shipped

    Documented environment surface so a fresh clone knows exactly which variables matter for absolute URLs and OG image generation.

  • Idempotent ingest scripts (npm run ingest, npm run ingest:structures)

    DevXv0✓ Shipped

    Re-running ingest is safe: upserts keyed on (sourceId, sourceHash) mean we never duplicate records and selective refresh is one flag away.

  • Generated TypeScript + Python SDK clients from Zod schemas

    DevXv0✓ Shipped

    A schema-driven codegen pipeline (npm run codegen) turns the route manifest and Zod response registry into typed TypeScript and Python clients plus an OpenAPI document — no hand-rolled fetch wrappers, and the clients describe responses with the exact Zod definitions the API serves. Regenerating is one command, so the SDKs can never drift from the contract.

    • sdk
    • codegen
    • openapi
  • Publish SDKs to npm and PyPI on release (GitHub Actions)

    DevXv0✓ Shipped

    A .github/workflows/release-sdks.yml workflow regenerates both clients from the live Zod schemas, stamps the SDK manifests with the release version, builds, and publishes — @pharmacopeia/client to npm with provenance, pharmacopeia to PyPI via OIDC trusted publishing. Triggered by an sdk-v<semver> GitHub Release or a manual workflow_dispatch (with a dry-run flag) so a single tag pushes the same version to both registries.

    • sdk
    • ci
    • npm
    • pypi

Later

10

Planned, but not scheduled.

Platform

  • Marketplace listings (RapidAPI, Vercel Marketplace)

    Platform

    Distribution channels for the paid tier — turnkey signup, billing, and quotas without rebuilding the platform pieces ourselves.

    • distribution

Data

  • Stage 2: LLM-driven section extraction per drug (Claude) with anti-hallucination review pass

    Datastage-2

    Per-section LLM extraction with a deterministic second-pass verifier. Below-threshold confidence lands in /review, never in the public API.

    • claude
    • llm
  • Section-level provenance (every mechanism/indication/contraindication paragraph carries its own provenance + confidence)

    Datastage-2

    Drop provenance granularity from per-record to per-section so refreshing one paragraph doesn't cascade across an entire drug.

  • Multi-language drug summaries

    Data

    Translated summaries for the most-read fields, with the same provenance and confidence model used for English content.

  • Interaction network graph across the dataset

    DataBlocked

    Fill the still-empty pair-graph gap: a drug↔drug interaction network with severity-weighted edges, exposed as data and as a D3 view that reuses the mechanism-graph rendering. Educational aggregate view, not a clinical checker. Gated on sourcing a real structured DDI dataset — RxNav `interaction` is retired, openFDA's `drug_interactions` field is one-sided narrative (already surfaced via `interactionsNarrative` on the drug record), and the licensed full feeds (DrugBank Plus, Lexicomp) sit outside the project's free-and-public rule. The schema, repository surface, and check endpoint are all ready; waiting on the data.

    Blocked: No free, structured drug–drug interaction dataset. RxNav's interaction API is retired, openFDA's field is one-sided narrative (already surfaced), and the licensed feeds (DrugBank Plus, Lexicomp) sit outside the free-and-public rule. Schema, repository surface, and check endpoint are ready — waiting on the data.

    • d3
    • interactions
    • blocked-on-data
  • DailyMed SPL ingest (richer, fresher labels)

    Data

    DailyMed's daily Structured Product Label XML is the planned-but-unstarted source named in the README. Richer and fresher than the openFDA label join — structured sections, inactive ingredients, and packaging — slotting into the existing shared ingest record builder rather than forking it.

    • dailymed
    • spl
    • ingest
  • Inactive-ingredient / excipient index (allergen lookup)

    Data

    DailyMed SPL exposes inactive ingredients per product (free, public). Surfaced as a browsable entity so a reader can answer "which formulations are lactose-free / dye-free / gluten-free" — a reference fact, never advice. Additive entity following the standard six-step recipe; depends on the DailyMed ingest landing first.

    • excipients
    • allergens
    • dailymed
  • Pregnancy & lactation reference data (NIH LactMed)

    DataBlocked

    NIH LactMed is free and public. Carefully framed reference summaries (never dosing guidance) carrying the same per-record provenance and confidence model as everything else — additive to the drug record, gated behind the same educational disclaimer.

    Blocked: Framing decision unresolved: pregnancy/lactation content is exactly where reference-style language is hardest to keep clear of advice. Needs a deliberate presentation + disclaimer design before any ingest, so it stays squarely educational.

    • lactmed
    • nih
    • blocked-on-framing
  • ChEMBL drug–target bioactivity to flesh out the MoA graph

    Data

    ChEMBL (named in the README) is the free source for cited drug–target relationships. Would fill the still-thin target nodes in the mechanism-of-action graph with real, provenance-tagged bioactivity edges rather than label-derived inference alone.

    • chembl
    • targets
    • moa

API

  • Cursor-based pagination on list endpoints

    API

    Offset pagination is fine at a few thousand records but drifts under concurrent writes. Stable cursors are the right primitive before heavy bulk consumers arrive — additive alongside the existing limit/offset so nothing breaks.

    • pagination
    • scale

Educational and informational use only. Dates on planned items are best-effort targets, not commitments.

Search pharmacopeia

Search drugs, classes, and ingredients