Roadmap
v0.2.0-scaleThe 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
What we're actively building right now.
Data
EMA (European Medicines Agency) jurisdiction added alongside US-FDA
DataStartedMulti-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
The changelog — what's done, with dates.
Platform
Read path scales to the 5,000+ drug dataset
Platform✓ ShippedBrowse 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✓ ShippedBrands, 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✓ ShippedEvery 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✓ ShippedReal 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✓ ShippedThe 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✓ ShippedRoutes 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✓ ShippedPushed 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✓ ShippedThe 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✓ ShippedFDA'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✓ ShippedEvery 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✓ ShippedDEA 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✓ ShippedDaily 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✓ ShippedICD-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✓ ShippedopenFDA 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✓ ShippedEvery 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✓ ShippedCPIC-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✓ ShippedRuntime 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✓ ShippedEvery 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.
Depth pass: FDA label sections, approval history, NDC/UNII, derived MOA targets
Datav0✓ ShippedBeyond 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✓ ShippedEvery 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✓ ShippedDrug 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✓ ShippedPer-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✓ ShippedPer-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✓ ShippedEvery 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✓ ShippedopenFDA `/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✓ ShippedWhen 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✓ ShippedLeans 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✓ ShippedThe 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
Embeddings + semantic search across drug content
API✓ ShippedA /v1/semantic-search endpoint over passage-level retrieval: every drug record is chunked into citable passages (lib/data/passages.ts), embedded offline into pgvector (npm run db:embed, text-embedding-3-small at 512 dims), and queried by cosine similarity with the query embedded at request time. Without an embeddings provider or database the same passages are scored by a lexical TF-IDF fallback — identical response shape, and the `method` field reports which path answered. Re-embedding is delta-based: re-seeding nulls the vector only for passages whose text hash changed.
- embeddings
- pgvector
LLM citation/grounding API tier (paid)
API✓ ShippedFirst 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✓ ShippedOutbound 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✓ ShippedPublic 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✓ ShippedEach 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✓ ShippedEvery 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✓ ShippedEvery 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✓ ShippedA 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✓ ShippedA 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✓ ShippedResolve 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✓ ShippedGET /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✓ ShippedA 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
Structure search: paste a SMILES, get the nearest drugs
APIv0✓ ShippedInteractive query over the same OpenChemLib 512-bit fingerprint index that powers per-drug analogs. Paste a SMILES and get the closest drugs in the dataset by 2D Tanimoto similarity. Available at /structure-search and POST /api/v1/structure-search. Structural proximity only, never therapeutic equivalence.
- chemistry
- openchemlib
Reactions directory (MedDRA Preferred Terms → drugs)
APIv0✓ ShippedA 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✓ ShippedThe 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✓ ShippedToggle 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✓ ShippedCustom 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✓ ShippedTheme 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✓ ShippedOutfit for display, Inter for body, Geist Mono for code and identifiers. Three families is the most we can spend before page weight gets noticeable.
Sticky header with logo, nav, theme toggle
UIv0✓ ShippedPersistent navigation so deep drug detail pages still feel anchored to the rest of the site.
Landing page with hero, live stats counters, code samples
UIv0✓ ShippedFirst-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✓ ShippedFlat 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✓ ShippedPer-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✓ ShippedA /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✓ ShippedEach 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✓ ShippedSubtle 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✓ ShippedCards 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✓ ShippedRight-rail table of contents tracks the active section as you scroll. Long reference pages stop feeling like an infinite scroll.
Global ⌘K command palette + inline list-page filter + /search page
UIv0✓ ShippedOne search surface across the site: a ⌘K palette anywhere, an inline filter on every index page, and a full /search results page for deep queries.
- cmdk
- search
/ingredients browse + detail pages
UIv0✓ ShippedIngredient 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✓ ShippedInteractive 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✓ ShippedEvery 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✓ ShippedMobile 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✓ ShippedGenerated 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✓ ShippedThe /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✓ ShippedA /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✓ ShippedA 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✓ ShippedEvery 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✓ ShippedStructured 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✓ ShippedEvery 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✓ ShippedJSON 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✓ ShippedPublishes /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✓ ShippedAudited 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✓ ShippedSkip-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✓ ShippedSelf-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✓ ShippedA /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✓ ShippedLive 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✓ ShippedA 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✓ ShippedExtends 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✓ ShippedTests 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✓ ShippedExtend 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✓ ShippedThe 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✓ ShippedA 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✓ ShippedUnit 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✓ ShippedDocumented 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✓ ShippedRe-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✓ ShippedA 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✓ ShippedA .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
Planned, but not scheduled.
Platform
Marketplace listings (RapidAPI, Vercel Marketplace)
PlatformDistribution 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-2Per-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-2Drop provenance granularity from per-record to per-section so refreshing one paragraph doesn't cascade across an entire drug.
Multi-language drug summaries
DataTranslated summaries for the most-read fields, with the same provenance and confidence model used for English content.
Interaction network graph across the dataset
DataBlockedFill 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)
DataDailyMed'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)
DataDailyMed 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)
DataBlockedNIH 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
DataChEMBL (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
APIOffset 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.