HipSTR compiled to WebAssembly, with a browser UI mirroring the desktop app.
▶ Try it: https://hipstr.pages.dev
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.
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.
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/hipstrBuild against a local working copy instead of the pinned commit:
HIPSTR_LOCAL=/path/to/HipSTR pnpm wasm:vendorDeliberately bottom-up, so each failure has one plausible cause:
pnpm wasm:build— get it compiling. htslib first, then cephes, then HipSTR.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 againstdemodata/results/hipstr_output_demo.vcf.- Browser — swap NODEFS for WORKERFS and confirm the same VCF.
- Port the tabs —
FilesTab->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.
| 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 |
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:smokeThree 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 aconsole.warn. - The file is CRLF, leaving
\ron the last field. - SE33 has a reference copy number of 25.5;
parseIntmade 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.
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.)
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.
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.
| 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) |
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.
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.