GitHub - chrispy90/waymark

GitHub

19 min read Original article ↗

Waymark: stories with a sense of place

MIT license Node 22.13 or newer Zero dependencies No API keys Docker ready

Write a story. Scroll it. Publish it anywhere.
Waymark is a self-hosted editor for scroll-driven map stories and the maps inside them.
Build maps with live government data and analysis, reuse them across stories, then share a link or export a website.

Quick start · Features · Saved maps · Export · Self-hosting · Story format

Scrolling through the Waymark welcome page

⚡ Start in a minute

You only need Node.js 22.13 or newer. There's nothing to npm install.

git clone https://github.com/chrispy90/waymark.git
cd waymark
npm start

Then open http://localhost:5180.

  1. Create your account. The first visit makes you the owner.
  2. Answer a few questions. Pick what you're making and a look, add a title and your places, and Waymark sets up the story for you.
  3. Click anything to edit it. When you're happy with it, press Publish.

Tip

Windows: if PowerShell says "running scripts is disabled", run node server.js instead. It does the same thing. Docker: docker compose up -d also works, and keeps your data in a named volume.

✨ What you can make

A map that moves with the story

The map stays put while story cards scroll past, and it glides to each card's place. You can tilt it, rotate it, switch layers, or swap it for a photo or video on any card. Select words in a card and press Map link to make them send the map somewhere else; click a link in the text to change it, move it to other words, or remove it.

A moving map flying between places as the reader scrolls

Scroll effects, built in

Effects from the best long-form sites, available as blocks. Add one and it works on phones too. Each effect switches off for readers who ask their device to reduce motion.

You also get chapters that recolor the page, big numbers that count up, and parallax, zoom and wipe-in photos and covers.

Live data from NOAA, NASA, USGS and more

Open Browse public data and click Add. Every layer arrives styled, with popups, a legend and credit to its source. No keys and no sign-ups.

The public data catalog

Source Layers
USGS Earthquakes (live) · stream gauges by state
NOAA Weather warnings (live) · radar (live) · tide stations · tsunamis · significant earthquakes · volcanoes · plate boundaries
NASA Natural events (EONET) · daily satellite imagery · Earth at night
Census · NPS · USFS · EPA · NIFC States and counties · park units · national forests · Superfund sites · active wildfires and perimeters
Natural Earth Countries · cities · rivers
ArcGIS Online Search thousands more public layers

Live layers refresh every time someone opens the story, so a published map stays current.

This week's earthquakes and tectonic plate boundaries on a dark map

3D Terrain, Elevation HUD & 3D City Buildings

Turn on realistic 3D elevation with a single click in any story map, moving map or saved map:

  • Free global DEM terrain & hillshading: powered by Mapzen Terrarium elevation tiles with dynamic vertical exaggeration (1.0× to 3.0×) and atmospheric sky horizon.
  • 3D city building footprints: toggle extruded 3D structures (fill-extrusion) from OpenFreeMap vector building footprints with true-to-height scaling and ambient shadows.
  • Camera Tilt & Pitch HUD: live interactive tilt angle indicator in the map status bar with real-time pitch readouts (up to 85°).

3D Polygon & Hexbin Extrusions + Bivariate Mapping

Display complex multivariate geospatial relationships directly on the map:

  • 3D Attribute Extrusions: extrude any 2D polygon layer or hexbin summary into 3D height prisms scaled proportionally to any numeric attribute (e.g. population, total sales, earthquake count).
  • 3×3 Bivariate Choropleth: map two continuous variables simultaneously (e.g. household income vs climate flood risk) with quantile breaks and an interactive 3×3 matrix legend.

Remote Sensing, STAC Satellite Finder & Time Travel

Professional Earth Observation and spectral analytics without leaving the browser:

  • Spectral Index Catalog: one-click live overlays for NASA MODIS NDVI Vegetation Health, False-Color Infrared 7-2-1 (burn scar & flood detection), Sea Surface Temperature (GHRSST), Aerosol Optical Depth (wildfire smoke & dust), Snow Cover NDSI, and USGS 3DEP Shaded Relief & National Hydrography.
  • Live STAC Satellite Scene Finder: search Copernicus Sentinel-2 L2A scenes around your map viewport using Element84 Earth Search STAC API. Filter by cloud cover %, inspect scene metadata (MGRS tile, sun elevation, telemetry), and stream 10-meter Cloud-Optimized GeoTIFFs (COG) directly into the map.
  • Satellite Time-Travel Before & After: compare multi-date satellite imagery side by side with a draggable swipe divider to detect wildfire burn scars, urban sprawl, drought depletion, or glacial retreat.

Universal GIS Ingest: KML, GPX, Photos, GeoJSON & CSV

Drop any spatial file directly onto the map or Data panel:

  • KML & KMZ: parses Google Earth placemarks, folders, lines, polygons, and ExtendedData attributes.
  • GPX: reads GPS waypoints, tracks, and routes with automatic distance, elevation gain/loss, and timestamp stats.
  • Geotagged JPEG Photos: extracts camera EXIF GPS coordinates and timestamps; multiple photos automatically merge into an interactive photo-points layer with popup image previews.
  • Elevation Profile & Slope Analysis Tool: sample polylines or points against the global DEM in the Analyze toolbox to compute total distance, elevation gain/loss, and slope grade percentages (%).
  • $ELEVATION_M in Field Calculator: access elevation directly in attribute tables and calculate derived metrics.

Filter it, then analyze it

Filters narrow a layer with plain rules, like magnitude ≥ 5, in the last 7 days or state = Texas. They apply to the map, the insights, the analysis and the export.

Analyze previews every result on a map before you keep it. Results become ordinary layers, and you can re-run them on fresh data. The 23 tools are grouped the way desktop GIS toolboxes group them:

Group Tools
Summarize Hex bins (count, sum or average) · Spatial density clusters (DBSCAN) · Count in areas · Dissolve (by a field, e.g. counties into states)
Proximity Buffer · Multi-ring trade areas (concentric catchment bands) · Hub & spoke desire lines · Distance to nearest · Within distance · Voronoi (Thiessen) areas
Overlay Clip · Erase · Intersect · Join by location · Select by location (touch, inside, contain, stay clear) · Merge
Terrain Elevation profile & slope (samples polylines along global DEM for distance, gain/loss, and grade %)
Manage data Join attributes by field · Center points · Outline (convex hull) · Simplify · Add geometry attributes (area, length, coordinates) · Filter

You can also show points as a heatmap or clusters, or use proportional symbols: size points, areas or lines by a number field (circle area grows with the value, and it works together with color). Give a layer a time field and its maps get a time slider with play, showing everything up to a moment or a trailing day, week or month. Readers can scrub it too, and bookmarks and story cards remember where it was set. Bring your own data: GeoJSON, KML, GPX, geotagged photos, CSV, Hosted Feature Layers, or any public ArcGIS layer.

Hosted feature layers & editable attribute tables

Create reusable, server-backed GeoJSON feature layers (like ArcGIS Online Hosted Feature Layers) from the Layers tab on the home screen, from inside the map or story editor, or by promoting any drawn, uploaded, or analyzed layer in one click.

  • Editable attribute tables & schema studio: click any cell to edit values inline, or add, rename, and delete columns (string, number, date, boolean). Add or delete feature rows and see live column statistics (Sum, Avg, Min, Max) in the header.
  • Field Calculator: compute values across all features using column math (+, -, *, /, % of, % change), automatic spatial geometry measurements ($AREA_KM2, $AREA_ACRES, $LEN_KM, $LAT, $LON), rankings (1..N), or text templates.
  • Source attribution & live service URLs: attach source attribution/credits to a hosted layer so it automatically appears on map legends and credits, copy its live /api/layers/:id/geojson endpoint, or export any table to GeoJSON or CSV.

Interactive charts & executive tables

Add a Chart & table block to any story — or click Add chart to story directly from a layer's Insights histogram — to present data alongside your narrative:

  • Four views: horizontal Bars, vertical Columns, SVG Donut rings with total callouts, or sortable Executive Tables.
  • Live layer aggregation or custom rows: group and aggregate (count, sum, avg, min, max) any layer in your story on the fly, or enter custom rows with prefixes and suffixes (such as $ or %).

Make a map once, use it in every story

Maps on the home screen are maps you build on their own, in a full-screen map editor, and then use in as many stories as you like. Edit the map once and every story that uses it updates, published stories included.

  • Layers: live public data, hosted feature layers, your files, or links. Reorder them by dragging, switch them on and off, and zoom to each.
  • Per-layer settings: color, proportional symbols, a time field for the time slider, labels, popups, source attribution, filters, visible zoom range and insights, plus an editable attribute table & schema studio you can sort, search, calculate and edit. Click a row number to zoom to that feature.
  • Tools: 22 analysis tools (multi-ring trade areas, hub & spoke desire lines, spatial density clusters, attribute table joins, clip, erase, intersect, dissolve, spatial join, buffer, hex bins, Voronoi and more), a time slider, drawing, measuring, place search, a live coordinate & zoom HUD, a basemap gallery and the legend.
  • Bookmarks: named views that also remember which layers are on, with a note for each.

Then walk readers through it: one click turns the bookmarks into moving-map cards, so a story can show the raw data, then the analysis, then the result, one card at a time. Any map in a story can also use a saved map, showing one of its bookmarks or a view you frame.

The map editor with layers, two bookmarks and layer settings

Draw right on the map

Press Draw on any map in the editor and sketch points, lines, freehand routes, areas, boxes, circles and text labels right on it. Give each shape its own colors, fill, line width, dashes or dots, and an arrowhead, plus a name and a description that readers see when they click it. Distances and areas are measured as you draw. Select a shape to move, restyle or duplicate it, trace over any other layer, and reopen a saved drawing or Hosted Feature Layer whenever you like.

The Draw workspace with a selected area, a dashed arrow and the style panel

Tours from your phone photos

Stop by stop tours have their own tour builder: every stop numbered on a big map, a list with photo thumbnails, and the selected stop's photo, name and text side by side. Drop a batch of photos and each becomes a stop where it was taken, in the order you took it, named after the file. Photos without a location are flagged and placed with a click. Add stops by searching or clicking the map, drag markers to move them, and drag the list to reorder.

The tour builder with numbered stops on a map, a list of stops with photos, and the selected stop's details

An editor that gets out of the way

Click anything in the live preview to edit it. You also get drag to reorder, undo and redo, a command palette (Ctrl/⌘+K), a phone preview and autosave. Drafts stay private until you publish again.

Clicking a chapter in the preview to edit its title and color

Make it loud: the Brutalist look

Switch any story to Brutalist for raw monospace and heavy display type, hard black edges and offset shadows, acid-yellow stickers, a ticker headline, outlined words that fill in as you scroll, and numbered chapters. Or start from the Brutalist zine template.

A story in the brutalist theme on a laptop and two phones

Every story works on a phone

Three stories on phones: a satellite map, a statement block, and a stop-by-stop walk

Start with a few questions, not a demo to delete

New story asks what you're making, how it should look, its title and cover, and where it goes. Then it builds the story around your places and chapters, with short prompts where your words go. A checklist in the editor walks you through the rest: cover photo, your own text, photos, a phone check and publishing.

The New story wizard: story type, look, places, and the editor checklist

Kind of story What you get
A journey A moving map with one card per place, plus an overview of the whole route
A walking tour Numbered stops with room for a photo and a description each. Build it in the tour builder: drop your phone photos and each one lands on the map where it was taken
An illustrated essay Your chapters as full-screen titles, with space to write and a photo
A data story One big map ready for live or uploaded data, with numbers and context
Start from scratch A title and an empty page

Every kind comes in eight looks. Each one sets the type, colors, chapter colors, map style and scroll animation. You can switch later under Story → Look & feel, and restyle existing chapters and maps to match in one click.

The same statement block in the Classic, Night, Brutalist, Atlas and Aurora looks

Look Style
Classic Warm paper and serif type. The original Waymark look
Night Dark and cinematic, so photos and maps glow
Brutalist Raw type, hard edges, loud color and a ticker headline
Atlas An old atlas: parchment, engraved type and small caps, sepia maps in a double frame with a compass rose, and chapters numbered as plates
Aurora Dark glass and drifting light, glowing gradient type, frosted cards and bento-style numbers. Points and lines on its maps glow like fireflies
Editorial Magazine journalism: warm newsprint ivory, deep espresso ink, crimson double-rules, high-contrast Playfair Display headlines, drop-cap intros and pull-quotes
Blueprint Architectural CAD & telemetry: deep Prussian navy with a dual-scale coordinate grid, electric cyan and amber accents, Space Mono headers, corner reticle brackets and glowing map features
Fog A thick fog storm: banks of fog drift behind and in front of the story, gusts tear across the screen and lightning flickers inside the murk. The reader's pointer carries a lantern that clears the fog around it, headings and photos come into focus as they scroll in, statement words sharpen as they light up, and markers glow like lanterns. A weather readout in the top bar counts visibility up from 40 m to 10 km as the fog lifts toward the end of the story

Prefer to learn from something finished? Explore an example opens a complete story to take apart: the moving map, walk, essay, Brutalist zine and Down the Potomac samples. Any story can also use Before & after, a block with two maps in one view and a draggable divider, or Chart & table, an interactive breakdown of any layer or dataset.

🚀 Publish it anywhere

Publish gives a story a public link like /s/your-story, with a preview image when the link is shared.

Export as a website turns a story into plain files that run on any web server, with no Node, no database and no Waymark.

The export dialog with zip and Tomcat options

Format Use it for
Website folder (.zip) Apache, nginx, IIS, shared hosting, S3, GitHub Pages, Netlify, or just double-click index.html
Tomcat web app (.war) Drop it in Tomcat's webapps/ folder (Tomcat 8.5 to 11)

The export includes the page, your images, a snapshot of your map data (saved maps included) and, optionally, the map library. Every path is relative, so it works in any folder. You can also export from the command line:

node server.js export my-story --war --out /opt/tomcat/webapps/my-story.war
All export options
node server.js stories                       # find the story's link or id
node server.js export my-story               # published version, as a .zip
node server.js export my-story --draft       # the unpublished draft

Options: --war, --draft, --out <file>, --no-images, --no-data and --no-vendor. The last three keep images, data or the map library as links instead of copying them. Only the map tiles and web fonts load from the internet.

🏠 Host it yourself

Your stories, saved maps, accounts and images all live in one data/ folder. Waymark runs anywhere that has Node 22.13+ or Docker and a persistent disk: a VPS, a home server, Fly.io, Railway, Render and so on.

  1. Put it behind HTTPS. Caddy is the easiest: caddy reverse-proxy --from stories.example.com --to localhost:5180
  2. In .env, set APP_URL=https://stories.example.com and COOKIE_SECURE=1.
  3. Once your accounts exist, set ALLOW_SIGNUP=0 to close sign-ups.
  4. Back up data/. It holds everything.
Settings (.env)

Copy .env.example to .env and edit it. Everything is optional.

Setting Default What it does
APP_NAME, APP_TAGLINE Waymark Rename the app (interface, emails, link previews)
PORT 5180 Port to listen on
HOST this computer only Set to 0.0.0.0 to let other devices on your network connect
APP_URL http://localhost:PORT Your site's public address, used in password reset emails
DATA_DIR ./data Where the database (waymark.db) and uploaded images are stored
COOKIE_SECURE off Set to 1 when the site is served over HTTPS
ALLOW_SIGNUP on Set to 0 to stop new sign-ups once your accounts exist
SMTP_* off Email for password resets. See below
Password reset and email

People can reset a forgotten password from the sign-in page. To deliver the emails, set the SMTP_* settings in .env. Any SMTP provider works, for example Postmark, Resend, SendGrid, Mailgun, or a Gmail app password.

Without email set up, the reset link prints in the terminal where the server runs. That's fine for personal or local use. You can also reset passwords yourself from the command line:

npm run users                                 # list accounts
npm run reset-link -- someone@example.com     # print a reset link
npm run set-password -- someone@example.com   # type a new password directly
Other ways to run it
npm run dev Restarts automatically when you edit server code
npm test Runs the test suite against a temporary database
docker compose up -d Runs in Docker, with data kept in a named volume

📚 Under the hood

Waymark is plain JavaScript with no build step and no dependencies. The server uses only Node's built-in modules, including node:sqlite. The browser app is ordinary ES modules. Maps are drawn with MapLibre GL JS on free OpenFreeMap and Sentinel-2 basemaps.

Project layout
server.js              entry point + startup checks
server/index.js        HTTP server: API routes, uploads, published pages, static files
server/db.js           SQLite schema (built-in node:sqlite)
server/auth.js         password hashing, sessions, reset tokens, rate limiting
server/mail.js         minimal SMTP client
server/cli.js          admin commands
server/brand.js        app name and tagline
public/                the web app (plain ES modules, no build step)
  js/render.js         story → page; all block renderers
  js/effects.js        scroll-driven effects engine (one rAF loop, on-screen elements only)
  js/fog.js            weather for the Fog look (fog layers, lantern, gusts, lightning, visibility readout)
  js/ui.js             icons, logo, toasts, menus, dialogs
  js/maps.js           MapLibre helpers: basemaps, views, data layers (incl. heatmap/clusters), legend, popups
  js/analysis.js       geospatial tools and layer statistics (Turf.js, loaded only in the editor)
  js/workbench.js      Add data, Analyze and Draw workspaces
  js/mapeditor.js      the saved-map editor (map.html)
  js/webmap.js         saved maps in stories: builds them into a story before it's shown or exported
  js/table.js          hosted feature layers, editable attribute tables, schema studio and field calculator
  js/layer-form.js     layer settings, shared by the story editor and the map editor
  js/catalog.js        public data catalog (government sources + ArcGIS Online search)
  js/export-core.js    static-site / .war exporter (shared by the editor and `node server.js export`)
  js/export.js         the export dialog
  js/zip.js            minimal zip writer
  js/builder.js        the story editor
  js/wizard.js         the New story wizard; story-kit.js builds stories from its answers and holds the looks
  js/tour.js           the tour builder for Stop by stop; exif.js reads where and when photos were taken
  js/templates.js      finished examples
  map.html             the saved-map editor page
  welcome.html         landing page for signed-out visitors (itself a story)
  stories/, data/      sample story and sample GeoJSON
docs/SCHEMA.md         the story and saved-map file formats
test/                  API tests plus unit tests for saved maps, hosted layers, the wizard, photo locations and story formats (node:test)
API

Every request that changes data must send the header X-Waymark: 1. This guards against cross-site request forgery: another site can't add a custom header without a CORS preflight, and the server never allows one.

POST /api/auth/signup {email, password, name} · POST /api/auth/login · POST /api/auth/logout · GET /api/me Accounts
POST /api/auth/forgot {email} · GET /api/auth/reset/:token · POST /api/auth/reset {token, password} · POST /api/auth/password {current, password} Password reset and change
GET /api/stories · POST /api/stories {story} List and create your stories
GET /api/stories/:id · PUT /api/stories/:id {story, version} · DELETE /api/stories/:id Read, save and delete a draft. A save with an outdated version returns 409
POST /api/stories/:id/publish {slug?} · POST /api/stories/:id/unpublish Publish copies the draft to the public copy
GET /api/public/:slug A published story's JSON
GET /api/maps · POST /api/maps {map} · GET PUT {map, version} DELETE /api/maps/:id · GET /api/maps/:id/usage · GET /api/public/maps/:id Saved maps: list, create, read, save, delete, which stories use one, and the copy readers load once a published story uses it
GET /api/layers · POST /api/layers · GET PUT DELETE /api/layers/:id · GET /api/layers/:id/geojson · GET /api/layers/:id/usage · GET /api/public/layers/:id Hosted feature layers: list, create, edit schema/attributes/features, serve as GeoJSON, check story/map usage, and public GeoJSON service endpoint
POST /api/uploads (raw image body) · GET /api/uploads · DELETE /api/uploads/:id Images: JPEG, PNG, WebP or GIF, checked by their bytes. 12 MB limit
Security
  • Passwords and sessions: passwords are hashed with scrypt. Sessions are random tokens stored hashed, in an HttpOnly; SameSite=Lax cookie. Sign-in, sign-up and reset requests are rate-limited.
  • Password reset: reset links are single-use and expire after an hour, and using one signs out every existing session. A reset request gets the same answer whether or not the email has an account. Links are always built from APP_URL, never from the request's Host header.
  • Story content: story text is cleaned against an allow-list of HTML tags. Every page sends a Content-Security-Policy that only runs scripts from this site and unpkg, with no inline scripts.
  • Uploads: uploaded files are checked by their contents, which rules out SVG, and served with nosniff and a default-src 'none' policy.
Story file format

Stories are plain JSON. See docs/SCHEMA.md.

🤝 Contributing

Bug reports, ideas and pull requests are welcome. Start with CONTRIBUTING.md. Please report security problems privately, as described in SECURITY.md. Each release is listed in the changelog.

Background

This project started as a rebuild of the ArcGIS StoryMaps reading and authoring experience. It was written from scratch using only public behavior and open formats. Esri's open-source classic templates (Map Journal, Map Series, Apache-2.0) and mapbox/storytelling (BSD-3) were studied for data models and UX patterns only. No code was copied, and the current closed-source ArcGIS StoryMaps product was not used.

License

Waymark is released under the MIT License.

It loads these open-source libraries from a CDN (an exported story can bundle MapLibre, along with its license): MapLibre GL JS (BSD-3-Clause) and, in the editor only, Turf.js (MIT).

Map tiles and data come from their providers, which are credited on every map: OpenStreetMap contributors and OpenFreeMap (ODbL), Sentinel-2 cloudless by EOX (CC-BY 4.0), and USGS, NOAA, NASA, US Census Bureau, NPS, USDA Forest Service, EPA, NIFC and Natural Earth (public domain). If you host Waymark for others, keep those credits visible.