Generate every constitutional isomer of a molecular formula, and practise finding them yourself.
The page is a front end for Surge, which it runs itself: surge is carried as WebAssembly and enumerates in a worker of the browser, so there is no service to call and nothing to install.
| Address | What it is |
|---|---|
/ |
The generator: a formula in, every isomer out, with the restrictions surge understands. |
/exercises |
The exercises: draw every isomer of a formula yourself, with hints and a correction. |
/fragments |
The motifs a hint is built from, and how often each appears in a formula. |
/news |
What the tool has learnt to do, newest first. |
Surge is the only thing that decides what an isomer is: the number to find in an exercise is enumerated at run time, never hard-coded, so an exercise and its correction can never disagree.
Under the formula, Export the structures opens a dialog that writes what
was generated as SMILES, as openchemlib idCodes, or as an SDF — a molfile per
structure with invented coordinates, carrying SMILES, ID_CODE, MF and
MW as fields, assembled by
sdf-creator. Each one can be copied
or downloaded, under a name of one's choosing.
An exercise is a molecular formula. The student draws a structure and there is nothing to press: as soon as the drawing holds the atoms of the formula it is checked, and it is kept when it is one of the isomers. Two structures that differ only by stereochemistry, by explicit hydrogens, or by which Kekulé form was drawn are the same answer. What was found is kept in the browser, per formula, and survives a reload.
The first hints read the formula — the degree of unsaturation, the families each heteroatom opens up, the reminder that only connectivity counts.
The ones after them compare the answers with the structures already found. Both sides are searched for a library of motifs — a three-membered ring, an ether, a terminal double bond, a quaternary carbon, a spiro atom — and what the answers hold but the student never drew becomes the hint:
8 answers hold an ether, and none of yours does. The oxygen can sit inside the skeleton rather than at its edge, joining two carbons.
A motif whose answers are only half found is counted out instead ("you have 3 of the 8 answers that hold an ether"), and the detail of a motif waits until the motif itself has been drawn: the three-membered ring is named before the nitrogen sitting in it.
Every motif is an openchemlib query fragment stored as an idCode and matched by
substructure search, so nothing about an answer is written down: /fragments
draws the whole library, the sentence each motif produces, and — given a
formula — how many of its isomers hold it.
Everything is in the address, so there is nothing to log in to and nothing to save:
| Address | What the student gets |
|---|---|
/exercises |
The 23 exercises of the cheminfo course. |
/exercises?formulas=C4H10O,C5H12,C3H8 |
Exactly those formulas, in that order. |
/exercises?formulas=C4H10O&exercise=C4H10O |
That set, opened on that exercise. |
/exercises?set=https://example.org/isomers.json |
A set the teacher hosts themselves. |
/exercises?formulas=C4H10O&embed=1 |
The same without the header, to be framed. |
A hosted set is a JSON document. Only exercises is required; the page
counts the isomers itself.
{
"title": "Series 3 — constitutional isomers",
"description": "Draw every isomer. Stereochemistry is not taken into account.",
"exercises": [{ "mf": "C4H10O" }, { "mf": "C5H10" }, { "mf": "C4H8O" }]
}It is fetched by the browser, so it has to be served with a permissive
Access-Control-Allow-Origin.
The colour of an exercise is its difficulty, and it is never written down either: it is read off the number of isomers — up to 5 green, up to 15 orange, more than that red — so a set named in a link is coloured like the one shipped with the tool.
Every page carries a Share button, top right. It builds the address of the page as it currently stands — the formula, the restrictions, the exercises chosen out of the set — and the iframe that frames it in another site. Two parameters configure the page rather than feed it:
| Parameter | What it does |
|---|---|
embed |
Drops the header and the navigation, so only the activity shows through a frame. |
hide |
Switches parts of the page off, comma separated. An unknown name is ignored. |
What hide understands, per page:
| Page | Names |
|---|---|
/ |
options, substructure, lists, about |
/exercises |
list, hints, answers, clear |
lists is the export of the results, kept under its old name so a link written
before the dialog existed still switches it off.
Hidden is not disabled: what a link carries still applies. hide=options runs
the search under the restrictions of the link without letting the visitor
change them, and a framed generator never shows the limit and the timeout at
all — it runs on the ones the link names.
The generator writes its search in the address, so a link reproduces it and runs it on arrival:
https://surge.cheminfo.org/?mf=C4H6&disallowTripleBonds=1&limit=200
learn.cheminfo.org opens a tile either framed in
the page or in a new tab. For a framed tile, add embed=1 so the header of
this application does not repeat the one of the course:
https://surge.cheminfo.org/exercises?formulas=C5H12,C6H14,C4H10O&embed=1
<iframe
src="https://surge.cheminfo.org/exercises?formulas=C4H10O&embed=1&hide=list,answers"
width="100%"
height="800"
style="border: 1px solid #d3d8de; border-radius: 8px"
title="Surge — Exercises"
></iframe>Everything runs in the page. The restrictions surge understands — triple bonds,
planarity, ring counts, the nine substructure filters — are the options of the
generator's fold, and they reach surge's own command line unchanged;
aromaticity filtering (surge's -R) is on by default, so the Kekulé structures
of one aromatic ring count once. The enumeration itself is done in a worker, so
a formula that takes seconds never freezes what is being drawn, and nothing a
student types leaves their browser.
git clone https://github.com/cheminfo/surge
cd surge
npm install
npm run devnpm test # unit tests, type-check, eslint, prettier
npm run test-e2e # Playwright, against the dev servercp .env.example .env
# uncomment one COMPOSE_FILE line, set TUNNEL_TOKEN for the Cloudflare mode
docker compose up -dThree modes, selected by COMPOSE_FILE in .env:
| File | Exposure |
|---|---|
compose.yaml |
publishes PORT on the host (the default) |
compose.traefik.yaml |
behind Traefik on surge.cheminfo.org |
compose.cloudflared.yaml |
behind a Cloudflare Tunnel, no published port |
One image serves the built pages, surge included: there is no service behind it to reach. The container runs read-only with every capability dropped; the pages it hands out are laid onto a tmpfs at startup, which is the only writable place in it.
Never deploy by hand with git pull && docker compose up -d --build: the build
overwrites the running tag in place while git pull moves the source under it,
so there is nothing left to roll back to. Deployment is the global deploy script
installed on the server, which is why every compose file resolves
${IMAGE_NAME}:${IMAGE_TAG} and why .deploy is gitignored.
TRACKING_SCRIPT holds the snippet the analytics provider hands out, whole. The
build writes one file per address so each page is titled and described as
itself, and the container puts the snippet at the end of the <head> of every
one of them at startup — so a visitor is counted wherever they arrived. A
npm run dev run — where Vite serves the page — loads nothing, so development
never reaches the counter.
The site does not assume it owns the root of a host, and the build does not decide where it is mounted. Every asset is written relative, so one image — one build, one tag — serves both of these at once:
| Address | BASE_PATH |
|---|---|
https://surge.cheminfo.org/ |
unset, or / |
https://www.cheminfo.org/surge/ |
/surge/ |
BASE_PATH is read when the container starts: the entrypoint stamps it
into the <base> of every page it serves, and the page reads its own mount
back off document.baseURI. So mounting the tool somewhere else is a line in
.env and a restart — never a rebuild, and never a second image:
BASE_PATH=/surge/ docker compose up -dA shared host strips the prefix before forwarding (Traefik's StripPrefix), so
the container behind it never knows it is mounted.
SITE_URL is a different thing and is read at build time: it is the
address the site names as its own, for the canonical link, the social card
and the sitemap. Every deployment claims that one address, so a mirror points a
crawler back at the original instead of competing with it for one search
result. Pass it only when publishing the tool as a site of its own elsewhere:
SITE_URL=https://example.org/surge/ npm run build
docker build --build-arg SITE_URL=https://example.org/surge/ .Note that a crawler only reads robots.txt from the root of a host, so a
deployment mounted under a path is covered by whatever answers that root, not
by the file the build writes.
Every variable is documented in .env.example; only PORT has
to be set.
| Variable | Default | What it does |
|---|---|---|
PORT |
31228 |
Port the page is served on; the Vite dev server takes PORT + 1. |
BASE_PATH |
/ |
Where this deployment is mounted, read at container start. /surge/ serves the same image under a path. |
SITE_URL |
https://surge.cheminfo.org/ |
Build time only: the address the site names as its own, for the canonical link, the card and the sitemap. |
TRACKING_SCRIPT |
unset | The analytics <script> tag, put at the end of the <head> of every page served. Unset, nothing is loaded. |
COMPOSE_FILE |
unset | Which deployment mode docker compose uses. |
IMAGE_NAME |
ghcr.io/cheminfo/surge.cheminfo.org |
Image every compose file runs. |
IMAGE_TAG |
latest |
Rewritten by the server's deploy script on each deploy and rollback — never edit by hand. |
TUNNEL_TOKEN |
unset | Cloudflare Tunnel token, for that mode only. |
Released versions and what changed are in CHANGELOG.md.
Two works, and the Cite button in the header hands both over in the style a journal asks for — as a citation, as BibTeX or RIS, or as one file a reference manager imports.
- The isomer generator — every structure this site hands out comes out of surge. McKay, B.D., Yirik, M.A., Steinbeck, C. Surge: a fast open-source chemical graph generator. J Cheminform 14, 24 (2022). https://doi.org/10.1186/s13321-022-00604-9
- Data processing in the browser — the site itself, which runs the generator in the browser. Patiny, L. Unlocking the Potential of Browser-Based Scientific Data Analysis: A 20-Year Journey of Expertise. Chimia 79, 66–69 (2025). https://doi.org/10.2533/chimia.2025.66
Surge is under its own license: https://github.com/StructureGenerator/surge.