Skip to content

Repository files navigation

HipSTR on the web

HipSTR compiled to WebAssembly, with a browser UI mirroring the desktop app.

Nothing to install. Pick your BAM/CRAM files, a BED of STR regions and a reference FASTA, and HipSTR runs in the browser tab. You can also skip the run and just load an existing HipSTR VCF to browse, plot and export the calls.

Prefer a desktop app? HipSTR-UI is the Electron version this UI mirrors, with installers for Windows, macOS and Linux — see its Releases.

Nothing is uploaded. The reference FASTA, BAM/CRAM files and BED stay on the user's machine. The wasm module mounts the picked File objects read-only and htslib pulls only the byte ranges faidx and the alignment index ask for — a few hundred KB per run, even against a 3 GB reference. There is no server, so there is no genomic data to hold, no consent flow and no data-residency question.

Layout

packages/hipstr-wasm/   emcc toolchain -> hipstr.wasm + a Web Worker runner
apps/web/               Vite + React UI

Two packages because the boundary is real: the wasm side uses a different toolchain (emcc, Docker), rebuilds rarely, and emits versioned binary artifacts that should stay out of the Vite dev loop.

Getting started

pnpm install
pnpm dev            # UI at http://localhost:5173 (wasm not required to run)

To build the wasm module (emcc is not needed on the host — it runs inside a pinned emscripten/emsdk container):

pnpm wasm:vendor    # clone HipSTR at the pinned commit
pnpm wasm:build     # emcc -> tsc -> copy assets into apps/web/public/hipstr

Build against a local working copy instead of the pinned commit:

HIPSTR_LOCAL=/path/to/HipSTR pnpm wasm:vendor

Build order

Deliberately bottom-up, so each failure has one plausible cause:

  1. pnpm wasm:build — get it compiling. htslib first, then cephes, then HipSTR.
  2. pnpm wasm:smoke — run the demo CRAMs through NODEFS in Node. No worker, no browser, no React: a failure here is the compiler or htslib and nothing else. Compare against demodata/results/hipstr_output_demo.vcf.
  3. Browser — swap NODEFS for WORKERFS and confirm the same VCF.
  4. Port the tabsFilesTab -> BedTab -> ExecutionTab -> the result tabs.

Step 4 is the cheap part. In the desktop app ResultsTab (701 lines) and IsoallelesTab (469) have only three platform calls each, and VisualizeTab, ParametersTab and everything in lib/ have none.

Status

Piece State
emcc build working — 1.0 MB wasm + 100 KB JS glue
Full genotyping run validated against native — identical output
index command generated .crai is byte-identical to samtools'
WORKERFS mount + /work symlink layer verified in-browser: sidecar lands in MEMFS, /data untouched
apps/web — all seven tabs ported working

Validation

Run against strhub-demo-data (5 BAM slices, 92 STR loci, full GRCh38):

native arm64 wasm
exit 0 0
runtime 21.7 s 22.0 s
variant records 76 76

Excluding the ##command / ##reference headers (which record input paths, and those differ by construction), the two VCFs are identical — same MD5. The only genuine numeric difference is signed zero: 10 of 76 records carry FS:-0.00 on one platform and FS:0.00 on the other. Same value, different sign bit, from floating-point association order — not a correctness difference.

Reproduce with:

HIPSTR_FASTA=$D/GRCh38_full_analysis_set_plus_decoy_hla.fa \
HIPSTR_BED=$D/data/regions.bed \
HIPSTR_SAMPLES=$D/data \
HIPSTR_EXTRA="--use-unpaired --min-reads 1" \
pnpm wasm:smoke

Porting notes

Three bugs in the desktop code surfaced when the ported parsers met the strhub-demo-data panel. All are fixed here, and all are latent in the desktop app too:

parseBed was too strict. HipSTR reads its region file with std::istringstream >> (src/region.cpp readRegions), which is whitespace-delimited and tolerant of CRLF, and it reads the reference copy number as a double. The desktop version split on \t and \n and used parseInt. Against this panel that meant:

  • 14 of 93 lines separate the last two columns with a space, so marker names came out as "D11S2368 N/A" and matched nothing in the VCF — those markers vanished from the results table with only a console.warn.
  • The file is CRLF, leaving \r on the last field.
  • SE33 has a reference copy number of 25.5; parseInt made it 25 and shifted every SE33 allele by half a repeat unit. SE33 is a standard forensic marker, so this is a real miscall, not a formatting nit.

BGZF is not one gzip stream. HipSTR writes --str-vcf as BGZF: concatenated gzip members (5 even for this small panel). Two plausible decoders get it wrong: fflate's gunzipSync returns 0 bytes — silently, so the results table would just be empty — and DecompressionStream("gzip") over the whole buffer throws "Junk found after end of compressed data" in the browser while succeeding under Node, so it passes a Node test and fails in the app. src/lib/gzip.ts walks the BGZF block boundaries (exact, from the BC extra subfield) and decodes each member separately. Verified in a real browser: 227,355 bytes over 5 blocks, matching zlib.

Known papercut, kept as-is: getSamplesAndMarkersMap calls confirm() when the VCF marker count differs from the BED. HipSTR skips loci with too few reads, so that is the normal case, not an exceptional one — the dialog fires on almost every run. Left alone to keep the port faithful; worth revisiting.

Note for the UI: read groups

HipSTR takes sample identity from @RG header lines and refuses to start without them. Sliced BAMs frequently have them stripped — every file in strhub-demo-data does — in which case --bam-samps and --bam-libs are required and must line up positionally with --bams.

The desktop app exposes both as free-text parameters, so a user with such files hits a hard stop and has to know the flag. The web app now fills them in automatically from the alignment filenames (sampleNamesAtom), and the Execution tab shows the names it will use; anything set in the Parameters tab still overrides them.

(That dataset also needs --use-unpaired --min-reads 1 to produce calls at all: the slices drop mate pairs, and with defaults every locus falls under the 100-read minimum. Native behaves exactly the same way — it is a property of the data, not of the build.)

Relationship to the desktop app

HipSTR-UI is the Electron app this one mirrors: the same tab-by-tab workflow, built for forensic analysts, students and researchers who need to run HipSTR or read its results without the command line. It ships pre-built HipSTR binaries for Windows, macOS and Linux — grab an installer from its Releases page.

Which to use:

HipSTR-UI (desktop) this (web)
Install an installer per OS nothing — open a URL
Where HipSTR runs native binary on your machine WebAssembly in the tab
Your data stays local stays local
Platforms Windows, macOS, Linux any modern browser

For contributors: the desktop app is a UI reference only. No code is shared. The parts worth sharing (lib/, i18n/) are small and stable enough to copy, and the parts that look shareable (ExecutionTab, FilesTab) mean genuinely different things in a browser and should diverge.

Deploying

The build output is a static site — no server, no runtime dependencies. The wasm module is built single-threaded (-pthread is deliberately dropped, see scripts/build.sh), so there is no SharedArrayBuffer and therefore no COOP/COEP header requirement. Any static host works.

hipstr.js and hipstr.wasm are committed under apps/web/public/hipstr/, so a deploy never needs Docker, emcc, or access to the vendored source:

pnpm install
pnpm --filter @hipstr/wasm run build:ts    # types only — plain tsc
pnpm --filter @hipstr/web build            # -> apps/web/dist  (~1.6 MB)

Rebuild the wasm with pnpm wasm:build and commit the result when the pinned HipSTR ref changes.

Cloudflare Pages

Setting Value
Build command pnpm install && pnpm --filter @hipstr/wasm run build:ts && pnpm --filter @hipstr/web build
Output directory apps/web/dist
Root directory (repo root)
Node version from .node-version (20)

A note on serving path

DEFAULT_WASM_URL in packages/hipstr-wasm/src/index.ts is the root-absolute /hipstr/hipstr.js. The site must therefore be served from a root path. Cloudflare Pages, Netlify and Vercel all give you one. A GitHub Pages project site (user.github.io/repo/) does not — going that route means setting Vite's base and deriving the URL from import.meta.env.BASE_URL instead.

License

Copyright (C) 2026 Jesus Ayala

This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License, version 2, as published by the Free Software Foundation. See LICENSE.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

hipstr.wasm is compiled from HipSTR by Thomas Willems (https://github.com/tfwillems/HipSTR), licensed GNU GPL v2. This project builds the Tfronta/HipSTR2 fork at the ref pinned in packages/hipstr-wasm/scripts/vendor.sh. Because the distributed wasm module is a derivative work of HipSTR, the site as served is GPL v2; the corresponding source is this repository together with the pinned upstream ref.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages