A rewrite of the DecodingUs platform (originally Play Framework / Scala 3) in Rust. It coexists with the Scala app during the transition and replaces it at a single cutover. The Rust app is the AppView: a curated Y/mtDNA phylogenetic catalog, a public read surface + JSON API, the curator tooling, and privacy-preserving federated reporting (it aggregates, it does not analyze).
Status: the spine is done — redesigned schema, data layer, public HTML/HTMX +
JSON API, auth + the full curator suite, tree versioning + merge + SNP-graft, the
multi-source tree build (ISOGG + decoding-us + FTDNA, Y + mt), the production ETL,
the YBrowse ingestion pipeline, the variant naming authority, and the federated
reporting mirror. Workspace builds clean; live-DB integration tests (gated on
DATABASE_URL) + unit tests pass. The data cutover is verified end-to-end on a
real prod dump (ETL --skip-tree + tree-init); what's left is executing it and the
live AT-Protocol OAuth handshake (verified to consent locally; confidential
round-trip is an Edge joint test), plus the re-scoped federation subsystems (IBD
coordination, social, sequencer-lab inference). See Roadmap. A living,
detailed status lives in STATUS.md; the feature-by-feature
comparison with the Scala app is in docs/scala-vs-rust-diff.md.
- Drop the JVM's memory/startup overhead for a single static binary.
- Replace a sprawling, accreted schema (~84 tables across 6 schemas + a second "metadata" DB) with a de-sprawled design that leans on Postgres JSONB for document-shaped data.
- Run fully Docker-less for local dev/test on Apple Silicon via Apple's
containerCLI, while remaining Docker-deployable for production.
| Concern | Choice |
|---|---|
| Web | Axum 0.7 (+ tower / tower-http / tower-cookies) |
| Templates | Askama (compile-time typed, Twirl analog) |
| Frontend | HTMX 2 + Bootstrap 5 (vendored), HATEOAS-first |
| Database | SQLx 0.8 (Postgres, runtime-checked queries) |
| Genomics | du-bio — coordinate math + text parsing (VCF / BED callable-loci / UCSC-chain liftover); the YBrowse GFF3 parser lives in du-jobs. Raw reads (BAM/CRAM) + calling are out of scope (done in Navigator) |
| Async | tokio |
| Auth | AT Protocol OAuth (PKCE/DPoP/private_key_jwt); legacy Argon2 sessions for dev/curator seeding |
| External | OpenAlex, ENA, NCBI/PubMed; AWS SES + Secrets Manager behind the aws feature; reCAPTCHA |
| i18n | embedded key=value catalogs (en/es/fr) |
| Local Postgres | Apple container running imresamu/postgis (arm64) |
Shared crates live in the sibling decodingus-shared repo (also consumed by
Navigator), pulled in as git deps pinned to a rev in Cargo.toml (so the
Docker build needs no sibling path). To co-develop locally, add a [patch]
pointing the three deps back at the sibling working tree.
github.com/JamesKane/decodingus-shared (separate repo)
du-domain/ pure types/enums/IDs + JSONB payload structs + the merge algorithm, no IO
du-atproto/ AT Protocol identity/crypto + OAuth client (PKCE/DPoP/metadata)
du-bio/ genomics: callable-loci (BED), liftover (UCSC chain), VCF reader
rust/ (this repo — AppView/server-specific)
crates/
du-db/ SQLx pool + per-aggregate query modules + versioning/merge/graft/naming/ybrowse engines
du-external/ OpenAlex / ENA / NCBI / AWS SES / Secrets clients
du-web/ Axum app: routes, Askama templates, i18n, HTMX, auth, OAuth, JSON API
du-jobs/ tokio scheduler + scheduled jobs + the Jetstream reporting-mirror consumer
du-migrate/ legacy → new-schema ETL + the `decodingus-tree-init` tree builder (ISOGG/decoding-us/FTDNA graft, Y + mt)
migrations/ redesigned schema (0001–0022)
locales/ en / es / fr message catalogs
docs/ STATUS pointers, Scala↔Rust diff, AT-Proto OAuth findings
scripts/ test-db.sh (Apple container), mock-legacy.sql
Dockerfile, compose.yaml, .env.example
Postgres schemas: core, tree, genomics, pubs, ident, fed, ibd,
social, support, billing, source. Key de-sprawl moves:
- 3 biosample tables → 1
core.biosample(asourceenum discriminator +source_attrsJSONB). - Deprecated child tables folded into JSONB on their parents (variant aliases & coordinates, sequence-file checksums/locations, alignment coverage, original haplogroups, per-revision tree metadata).
- The legacy second "metadata" database collapses into the
fedschema. - Scattered
at_uri/at_cidcolumns → one consistentatprotoJSONB column. - Tree is temporal: no
parent_id; hierarchy lives intree.haplogroup_relationshipwith bitemporalvalid_from/valid_until. - Universal variant model: one
core.variantper physical SNP site;canonical_name(nullable — unnamed variants are identified by coordinates),aliases/coordinates/evidenceJSONB,naming_status,mutation_type. Coordinates carryancestral/derived(the reference genome ≠ phylogenetic root); recurrence (homoplasy) is modeled per-link ontree.haplogroup_variant(ancestral_allele/derived_allele), so forward / back-mutation / recurrent occurrences are representable (mig 0021). - PostGIS (
geometry(Point,4326)),citext, native enums, GIN/GiST/expression indexes on queried JSONB paths.
| Area | Routes |
|---|---|
| Home / about / contact / reputation / terms / privacy / cookies / FAQ | / /about /contact /reputation /terms /privacy /cookies /faq |
Per-sample report (public, is_public-gated) |
/sample/:slug (identity, Y/mt haplogroup pathways, origin map, sequencing/coverage, ancestry; curator is_public toggle) |
| Variant browser | /variants (+ fragments; JSONB alias/rs-id search) |
| Y/MT tree — two SVG cladograms (horizontal + vertical) | /ytree /mtree (breadcrumb re-root, orientation toggle, name/variant search, SNP sidebar, backbone/recent coloring) |
| References + per-publication biosamples; suggest-a-paper | /references (+ report), /references/submit (public DOI → candidate queue) |
| Biosample map (PostGIS → Leaflet GeoJSON) | /biosamples/map /biosamples/geo-data |
| Coverage benchmarks + per-lab drill-down | /coverage-benchmarks /coverage/labs |
| Navigator downloads — installers resolved from GitHub Releases | /download (per-platform builds, cached 30 min); /download/{windows,macos,linux} permanent redirects to the current installer |
| Profile (view + display-name update); contact (reCAPTCHA) | /profile /contact |
| sitemap / robots / health; cookie-consent banner | /sitemap.xml /robots.txt /health /cookie-consent |
| Public JSON API + OpenAPI 3 / Swagger UI | /api, /api/v1/* (see below) |
Y/MT tree, coverage benchmarks, references + biosamples, biosample studies,
variant search/detail/by-haplogroup, variant CSV + GFF3 export, genome
regions, STR signature + prediction, branch age, the per-sample report
(/samples/:slug), and federated population reports
(/reports/{coverage,ancestry,haplogroups}).
AT Protocol OAuth (/login/atproto, dev public-client path); legacy
signed-cookie sessions for dev. Curator RBAC guard. The curator dashboard
(/curator) links a full suite:
| Tool | Route | What |
|---|---|---|
| Haplogroups | /curator/haplogroups |
CRUD + structural ops: reparent / merge-into-parent / split (direct temporal edits, cycle/name guards) |
| Variants | /curator/variants |
CRUD; alias/coordinate JSONB editing |
| Genome regions | /curator/regions |
CRUD (coordinates/properties JSONB) |
| Curation proposals | /curator/proposals |
review/promote Navigator-submitted branch proposals → catalog |
| Publication candidates | /curator/publications |
review OpenAlex discoveries → promote to references; status/search/sort filters, retract an accept back to rejected |
| Change-sets | /curator/change-sets |
tree-versioning lifecycle + diff + per-change review/apply |
| Merge review | /curator/reviews |
resolve SNP-graft flags / merge ambiguities via the wip_* staging tables (accept-anchor / reparent / merge / defer) |
| Variant naming | /curator/naming |
the DU naming authority: queue + mint DUxxxxx + lifecycle |
| Reconcile flags | /curator/reconcile-flags |
merge YBrowse synonym clusters split across catalog variants |
A separate management API for machine/curator callers lives under
/manage/* (change-set lifecycle, /manage/haplogroups/merge[/preview],
/manage/curation/proposals X-API-Key intake) — deliberately outside the public
/api/v1.
- Change-set lifecycle (DRAFT → READY_FOR_REVIEW → UNDER_REVIEW → APPLIED/DISCARDED), per-change review, diff, and a temporal apply engine (CREATE/UPDATE/DELETE/REPARENT/VARIANT_EDIT) — including a WIP pass that enacts curator merge-review resolutions.
- Tree merge (Identify-Match-Graft) — a pure
du-domain::mergere-implementation against curated fixtures (the legacy was buggy): subtree-scoped matching, ambiguity-flagged-not-guessed, materialized into a reviewable change-set. - SNP-anchored graft (
du-db::snp_graft) — reconciles external source trees (decoding-us, FTDNA) into the ISOGG foundation by SNP plurality: enrich matches, graft truly-novel branches, flag the rest for curator review. A--reattachmode anchors FTDNA's complete-topology "bushes" via vetted MATCH dispositions when their backbone ancestor is flagged. A recurrent-link scrub (scrub_recurrent_links) prunes homoplasic / ASR-scatter defining-variant links to each variant's primary lineage. The mtDNA tree is FTDNA-only (single RSRS root), loaded as its own foundation (--ftdna-foundation). Result: a single-root Y tree (ISOGG-named backbone + decoding-us + full FTDNA depth) + an RSRS-rooted mt tree.
DecodingUs owns the DU Y-variant prefix. core.du_variant_name_seq +
core.next_du_name(), a curator naming queue (UNNAMED→PENDING_REVIEW→NAMED,
mint-on-assign with same-coordinate dedup), and a GFF3 propagation export
(/api/v1/variants/export.gff).
snps_hg38.gff3 (~3M SNP lines, full snapshot, no deltas) is streamed into the
verbatim source.ybrowse_snp mirror; du-db::ybrowse::reconcile then
derives core.variant so curator decisions survive every re-ingest:
synonyms fold deterministically (strand-canonical key; INDELs VCF-trim-normalized;
MNPs left intact), existing catalog variants match by name or coordinate and
are enriched in place (canonical/naming_status locked), and clusters split
across multiple existing rows are flagged for /curator/reconcile-flags.
A long-lived Jetstream consumer mirrors Navigator's published anonymized
computed-summary records into dedicated fed.* reporting tables (PII-bearing
records keep typed anonymized columns only). Reports aggregate via query-time SQL.
The inbound credential-holding firehose / PDS-fleet model is dropped;
curators submit branch proposals through the machine-auth intake endpoint.
du-bio (BED callable-loci, UCSC chain liftover, VCF). Y-STR per-branch modal
signatures + STR→branch prediction + STR-variance age; a combined branch-age
estimate (McDonald 2021: SNP-Poisson + STR + genealogical/aDNA anchor terms,
inverse-variance combined) gap-filling tmrca_ybp.
db-heartbeat, ybrowse-variant-ingest (GFF3 → mirror → reconcile),
publication-update (OpenAlex), publication-discovery,
publication-pubmed-update (NCBI), ena-study-enrichment,
branch-age-recompute (STR + combined age) — plus the Jetstream reporting-mirror
consumer. Error-isolated; each registers only when its env config is present.
Legacy → new schema, preserving PKs and sample_guid so FKs carry over 1:1;
idempotent; runs target migrations then the transformers + a reconciliation pass.
Covers the full production surface — catalog (donors, biosamples, variants, tree,
studies, publications), ident/auth (users, RBAC, AT-Protocol OAuth/PDS, consent,
audit), and genomics (labs, instruments, test types, libraries/files, alignment +
pangenome coverage, genotype data, pangenome graph). Validated against a real
production dump — all aggregates reconcile. --skip-tree omits the legacy
haplogroup tree (it's built ISOGG-founded by decodingus-tree-init instead;
biosamples carry their haplogroup names as JSON and resolve at read time).
decodingus-tree-init builds the Y tree (ISOGG foundation + graft decoding-us +
graft FTDNA + scrub) and the mt tree (FTDNA foundation).
- Rust (stable) —
cargo. - A container runtime for the local database.
test-db.shprefers Applecontainer(Docker-less, the default on Apple Silicon) and falls back to Docker only if Applecontainerisn't installed. With Applecontainer, set it up once (and again whenever the service is stopped):(Already running Postgres elsewhere? Setcontainer system start # starts the service; installs the Linux kernel on first runDATABASE_URLandtest-db.shuses it as-is, starting no container at all.)
# Start Postgres (PostGIS) and print the DATABASE_URL to export:
eval "$(./scripts/test-db.sh up)" # Apple container gives it its own IP
# Run the web server (connects + applies migrations on startup):
DATABASE_URL=... APP_SECRET=<32+ chars> cargo run -p du-web # serves on :9000 (PORT to change)Apple container assigns each container its own IP (no localhost port
forwarding), so test-db.sh discovers it and emits the right DATABASE_URL
(e.g. postgres://postgres:dev@192.168.64.2:5432/decodingus). Stop it with
./scripts/test-db.sh down.
Gotcha: if a committed migration changes, recreate the dev DB — SQLx errors on a checksum mismatch.
HASH=$(cargo run -q -p du-web --bin decodingus -- hash-password 'yourpassword')
# then insert ident.users + ident.user_login_info(provider_id='credentials',
# provider_key='<handle>', password_hash=$HASH) + ident.user_roles('TreeCurator').eval "$(./scripts/test-db.sh up)"
cargo test --workspaceIntegration tests are gated on DATABASE_URL: with it set they run against the
live PostGIS (migrations, JSONB round-trips, query modules, the apply/merge/graft/
reconcile engines); without it they skip and the suite stays green. The i18n test
enforces that es/fr cover every English key.
The production source is a self-managed Postgres on EC2.
decodingus-migrate \
--legacy "postgres://user:pass@ec2-host:5432/decodingus?sslmode=require" \
--target "$DATABASE_URL" # runs transformers + reconciliation
decodingus-migrate --legacy ... --target ... --verify # counts only
decodingus-migrate --legacy ... --target ... --skip-tree # skip the tree (build via tree-init)For the cutover, run --skip-tree first, then decodingus-tree-init (below):
the tree is built ISOGG-founded rather than migrated, and tree-init's foundation
load needs the tree namespace empty.
⚠️ The transformerSELECTs encode the production column layout — validate against the live EC2 schema (or a current-schema dump) before the production run.
# Y tree: ISOGG foundation → graft decoding-us → graft FTDNA (reattach) → scrub:
decodingus-tree-init --isogg /path/isogg_full_tree.json --apply
decodingus-tree-init --merge-prod https://decoding-us.com/api/v1/y-tree --snp-graft --graft --apply
decodingus-tree-init --ftdna /path/ftdna_ytree.json --graft --reattach --apply
decodingus-tree-init --scrub-recurrent --apply
# mt tree: FTDNA is the sole source (single RSRS root) — load as the foundation:
decodingus-tree-init --ftdna /path/ftdna_mttree.json --ftdna-foundation --dna MT --apply
# (any step without --apply is a dry-run; --stage-review routes flags to /curator/reviews)
# YBrowse variant ingest (mirror + reconcile); deploy-time, large file:
YBROWSE_GFF=/path/snps_hg38.gff3 [YBROWSE_CHAIN_GRCH37=… YBROWSE_CHAIN_HS1=…] cargo run -p du-jobsMulti-stage Dockerfile builds a single binary on a slim runtime (no JRE, no C
deps); compose.yaml runs it with postgis/postgis. SQLX_OFFLINE=true for
DB-less builds. The shared crates are git deps (no sibling path needed in the
build context).
Done (✅): redesigned schema + temporal tree; du-db aggregates; public read
surface + JSON API + OpenAPI; auth + the full curator suite; tree versioning +
merge + SNP-graft + curator merge-review; the multi-source tree build
(ISOGG-founded Y + decoding-us + FTDNA graft/reattach + recurrent-link scrub; mt
tree from FTDNA) + ancestral-state / recurrence modeling; the variant naming
authority; YBrowse GFF3 ingestion (mirror + reconcile, synonym/strand/INDEL
handling); federated reporting mirror + reports; STR signature/prediction +
combined branch age; du-bio core; the scheduled-job suite; the full production
ETL (verified against a real prod dump, --skip-tree cutover option); shared
crates extracted to decodingus-shared (git deps); the public per-sample report
(/sample/:slug, is_public gate, mig 0022) over a unified core+fed read model;
static/footer pages reconciled with the legacy content (App Passwords removed).
Remaining, in scope (⬜):
- AT-Protocol OAuth — live handshake. Client wiring is built and verified
to the consent page against a local PDS; the confidential
private_key_jwtround-trip is the Edge joint test (seedocs/atproto-oauth-findings.md). - ETL cutover — execution. ETL +
--skip-treeverified end-to-end against a real prod dump; what's left is running it for real (freeze prod read-only → dump → prepare locally → ship to AWS → flip) and alias-aware name-resolution for biosample→haplogroup (mt has no ISOGG-style alias source; ~15% of mt names need PhyloTree-version mapping). - IBD matching — the AppView is the only component that can spot IBD
introduction candidates across the federation (mine
fed.*→ dual-consent → coordinate the Edge hand-off → persist match state). Placeholder tables (ibd); logic forward. - Social layer — messaging/consent threads, notifications, blocks, public
feed, reputation, group projects (
social). Underpins IBD consent/notify and stands alone; logic forward. - Sequencer-lab inference — instrument-ID → lab lookup API (lets Edge nodes skip a data-entry step) + consensus discovery + curator review. Lab tables exist; logic forward.
- Discovery automation — the curator review/promote half is built; the automated half (private-variant capture, consensus, auto-reassignment) is forward work.
- Multi-test-type completion — taxonomy + chip ingest exist; marker coverage / confidence scoring tables are forward work.
- Region management API + bootstrap-from-CHM13 (the S3/CHM13 pipeline; the region CRUD UI already exists).
Out of scope / not in production (➖): inbound PDS firehose + fleet, patronage/billing, manual sample ingestion (Navigator does it), AppView→PDS backfeed (superseded by the outbound mirror / notify-fetch direction), server-side BAM/CRAM. Several have placeholder tables but no logic. (IBD, social, and sequencer-lab inference were re-scoped IN — the AppView is their federation coordinator.)