Lockstep

Lockstep

Docker app from smiei's Repository

Overview

Lockstep is a self-hosted tracker for Pokémon Nuzlocke runs, built around SoulLink: 2-4 players or solo. Log every catch route by route; a link forms once every player has caught, and one death takes the whole link to the Memorial. Level caps, a battle scout against each player's team, Species and Shiny Clause variants, and the type chart, stats, catch rates and move names of the generation you play. Gen 1-5, many ROM hacks (Radical Red, Unbound, Renegade Platinum, ...) and Pokémon Infinite Fusion with fusions. Live on every device at the table, installable as an app, in five languages. No account, no cloud. Pokémon sprites, badges and trainer pictures are downloaded on the first start, never shipped in the image. Unofficial, non-commercial fan project.

Lockstep

SoulLink & Nuzlocke tracker

A Nuzlocke tracker built around SoulLink: 2–4 players, linked deaths, the rules of your generation.
Free, self-hosted, and live on every device at the table.

Release Image: ghcr.io/smiei/lockstep License: MIT

https://github.com/user-attachments/assets/f7191534-8d36-472e-bc7f-660101b234cd

New to the format?

A Nuzlocke lets you catch only the first Pokémon in each area, and a fainted Pokémon is dead. In a SoulLink, several players run at the same time and each area's catches are linked: one dies, its partners die too.

Contents

What sets it apart

You know the rules. This is what the tracker handles so you don't have to.

SoulLink, the way you actually play it

  • 2–4 players or solo, each with their own catches per route. Every device at the table sees each change live.
  • A link forms only once every player has caught on that route. If one player's encounter fled or was knocked out, the pair never formed: the survivor stays in the box and never shows up as a link.
  • Marking a link dead takes all of its Pokémon, and records who lost theirs and how ("lost by Red · Lt. Surge's Raichu"). The run's Memorial keeps the list.
  • Your team is linked too. A team slot holds the whole link, and new catches fill free slots on their own.

Clauses that know the difference

  • Species Clause in three variants:
    • across all players;
    • per player;
    • per player, but with a formed link locking its families for everyone.
  • Evolution families, statics and fusions are handled.
  • Shiny Clause: a shiny never locks its family.
  • The clauses only warn, they never block. Your group's house rules win.
  • Rule presets carry a ruleset, including your written rules, from one run to the next.

Accurate to the generation you're playing

  • The type chart, base stats, catch rates and move names your game actually had: Gen 1's single Special, no Fairy type before Gen 6, pre-buff stats, old move names.
  • Catch rates use that generation's formula and ball list, Apricorn Balls included, up to Sword/Shield's and Scarlet/Violet's level and badge rules.
  • Evolution overrides for randomizers and ROM hacks: impossible, easier or time-based evolutions.

Planning the next fight

  • Level caps for every gym leader, rival and Elite Four member, with the next cap always in view.
  • Battle scout: the opponent's moves by level and its matchup against each player's team.
  • Team coverage (weaknesses and offensive gaps), judged at your last level cap rather than at level 100.
  • Base-stat totals now and fully evolved, with a note when an evolution is possible within the cap.

And also

  • Pokémon Infinite Fusion (Classic and Remix): fusions are one battle unit, and a fused catch locks both families.
  • Custom routes for areas a game pack doesn't list.
  • Free-team mode for runs that aren't Nuzlockes.
  • Blind flight, which hides every calculated hint, optionally locked on for the whole run.
  • An installable phone app in 5 languages: English, German, French, Spanish and Italian. German and English are reviewed by hand; French, Spanish and Italian are machine-translated. The names of Pokémon and of every official game's places are the official ones in each language. ROM hacks keep their English names, as in the game.
  • Backups of one run or all of them.
  • No account, no cloud: it runs on your PC or home server.

Screenshots

Team tab: the six team slots as specimen trays, each linked pair with its label, types, base stats and evolutions Encounter tab: one catch per route and player
Battle & Catch: the opponent's attack types and how your team matches up Overview: the next level cap, progress, badges and per-player figures

Memorial: who was lost, by whom, how and when

The tracker on a phone: Encounter, Team and Overview

Pokémon Infinite Fusion

Lockstep tracks Pokémon Infinite Fusion runs, Classic and Remix, fusions included.

Team tab of an Infinite Fusion SoulLink: each tray holds both players' fusions with their fused names, types, stats and sprites Pokédex card of a fusion: fusion sprite, fused types, base stats and weaknesses, with head and body as their own cards

Encounter tab: two catches entered as wild fusions with "Caught as a fusion"

  • Fuse on the Team tab: two of a player's catches become one battle unit. Swap head and body, or split them again.
  • Real fusion values: name, types and base stats are worked out from the pair the way the game does it. The sprite is the community's hand-drawn fusion art, or the generated one where nobody has drawn it yet.
  • Wild fusions: a catch can already be a fusion ("Caught as a fusion" on the Encounter tab), or every catch is, with the randomizer rule "Fuse everything".
  • SoulLink with fusions: a pairing stays tied to its routes, so a death takes everything linked to it, across fusions. A fused catch locks both families for the Species Clause; you can switch that off.
  • The game's own data: stats, types, catch rates, moves, TM compatibility, the trainers' level caps, and the capture formula with all 40 of the game's balls.

Details: features.

Quick start

Pick whatever fits you best. All four give you the same app at http://localhost:3000, or at your server's address.

Windows app (one download)

  1. Download Lockstep-Setup-<version>.exe from the latest release and run it. No admin rights needed; there is also a portable zip.
  2. Your browser opens with a short waiting page while the first start downloads the Pokémon sprites (a few minutes); it switches to Lockstep by itself. Lockstep then keeps running in the system tray.
  3. Friends on the same Wi-Fi open the address shown under Address for other devices in the tray menu. Allow access when the Windows firewall asks.

The installer is not code-signed yet, so your browser or Windows may warn about it even though it is safe:

  • Browser ("may contain a virus", "not commonly downloaded"): keep or allow the download.
  • Windows SmartScreen ("Windows protected your PC"): click More info → Run anyway.

Docker (home servers, NAS, Unraid)

services:
  lockstep:
    image: ghcr.io/smiei/lockstep:latest
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=file:/app/db/nuzlocke.db
      # - DEFAULT_SHELL_COLOR=blue                  # optional: green (default), red, blue or yellow
      # - DEFAULT_LANGUAGE=en                       # optional: de, en, fr, es or it
    volumes:
      - db:/app/db
      - sprites:/app/public/pokemon-sprites
      - trainers:/app/public/trainers
      - covers:/app/public/covers
    restart: unless-stopped

volumes:
  db:
  sprites:
  trainers:
  covers:

Save that as compose.yaml and run docker compose up -d. The first start downloads the Pokémon sprites, trainer pictures and game covers, which takes a few minutes.

Pinokio (one click, Windows / macOS / Linux)

  1. Install Pinokio.
  2. On the Explore page, paste https://github.com/smiei/lockstep-pinokio into the GitHub or Hugging Face URL field and click Go.
  3. Click Install, then Start.

Without Docker (Node.js 20.19+)

git clone https://github.com/smiei/lockstep.git
cd lockstep
npm install
npm run setup
npm start

→ Self-hosting guide: volumes, updating, backups, HTTPS for installing the app on a phone, trainer pictures, and a custom app icon.

Supported games

Generation Games
1 Red / Blue / Yellow
2 Gold / Silver / Crystal
3 Ruby, Sapphire, FireRed / LeafGreen, Emerald
4 Diamond / Pearl, Platinum, HeartGold, SoulSilver
5 Black, White, Black 2 / White 2
6 X / Y, Omega Ruby, Alpha Sapphire
7 Sun, Moon, Ultra Sun, Ultra Moon
8 Sword, Shield, Brilliant Diamond / Shining Pearl (no TM list: the data source has none for it)
9 Scarlet / Violet
Fan game Pokémon Infinite Fusion (Classic and Remix), with fusions and the game's own data
ROM hacks Radical Red, Unbound, Run & Bun, Renegade Platinum, Sacred Gold, Storm Silver, Blaze Black, Volt White, Blaze Black 2 / Volt White 2 Redux, Emerald Kaizo, Inclement Emerald, Rising Ruby, Sinking Sapphire, Blazing Emerald, Glazed, New Generations, with each hack's own stats, types and moves, Pokémon up to Gen 9 and regional forms; named in English, as in the game

Found a wrong route or name? Corrections are welcome as an issue.

Playing a ROM hack or another game? A pack is just the route list and the boss level caps, with no encounter tables. You can import your own in the app or contribute it for everyone; see game packs.

Documentation

Version history

Docker tags follow MAJOR.MINOR: latest is always the newest release, and every version stays available under its own tag for rollbacks.

Version What it is
1.0 Pre-PWA state (was latest until then).
1.1 Adds the installable PWA (was pwa).
2.0 Full UI overhaul: design tokens, shared component primitives, loading/error boundaries, toasts, pinned tab strip, keyboard-navigable dropdowns, one modal implementation, 40/44px touch targets.
3.0 Pokémon Infinite Fusion (Classic and Remix: fusions, catches that already are fusions, the game's own species/move/capture data), SoulLink for 3 or 4 players, and per-player Species Clause variants. Backup format v4.
3.1 Runs are addressed by an unguessable access key instead of their numeric id (old ?run=<number> links still open and are rewritten). Accessibility pass from a UI audit: readable type chips, text greys and focus ring, labelled Encounter and Battle & Catch fields, keyboard access to the Pokédex, a title per tab, drawn icons instead of emoji. Runs without Docker via npm run setup and with a one-click Pinokio launcher. Now MIT-licensed.
4.0 Redesign as a natural-history collection (the app is now called Lockstep in its interface): sage ground, specimen trays and labels, a green side rail on desktop, a new logo and fonts. The Team tab becomes a drawer of six trays plus a Box and Memorial column; Overview opens with one run sheet; the Pokédex card is a field-guide entry. Per-device shell colour (green, Poké Ball red, blue, yellow). Docker image for amd64 and arm64, also on GHCR.
4.1 The logo is back in the shell's bottle green (4.0 shipped it on a red tile); the app icon, splash screen and status bar follow.
4.2 The run list opens right under the run box and reads clearly in dark mode (ruled rows, the open run marked). New rule: permanent blind flight, which locks blind flight on so the header button cannot lift it. Clearer Docker volume guide and a by-game reference for trainer pictures.
4.3 Server defaults for the colour (DEFAULT_SHELL_COLOR, icons included) and the browser language (DEFAULT_LANGUAGE); the installed app follows the phone's language. The side-rail logo takes the shell colour, the run list reaches past the rail, the dark-mode button can return to following the device, and "Lock blind flight" got its name.
5.0 The app is now called Lockstep, and so are the repository (smiei/lockstep) and the image, which is published on GitHub's registry only (ghcr.io/smiei/lockstep). A Windows app: one installer, runs in the system tray. 16 ROM hacks (Radical Red, Unbound, Run & Bun, Renegade Platinum, and more) with their own stats, types and moves; Pokémon data up to Gen 9 with regional forms; a hack's own Pokémon as placeholders. Import your own game pack in the app, or contribute one. A start page with every run as a card. Boss-trainer pictures download by themselves. Notes on machine-translated languages and unverified game data. Backup format v5.
5.1 The Encounter tab tells open and finished routes apart at a glance, in dark mode too (a coloured or dashed frame and an outcome badge per route). 66 more boss-trainer pictures for the ROM hacks, and a complete list of the pictures each game expects. A ROM hack's own forms (Sevii, Hoenn) show their species' sprite, marked as a placeholder. Updated dependencies.
5.2 15 more official games: Ruby and Sapphire, Diamond and Pearl, Black 2 and White 2, and every main game from X and Y to Scarlet and Violet, with place, boss and badge names in all five languages. The catch calculator knows Gen 6 to 9: new balls (Beast Ball), the changed ball bonuses, and Sword/Shield's and Scarlet/Violet's level and badge rules; the Gen 5 formula is corrected. Fixed: 18 Pokémon re-typed to Fairy in Gen 6 (Clefairy, Gardevoir, Mawile, ...) lacked the Fairy type in every game and ROM hack with Gen 6+ data.
5.3 Official place names in all five languages for every official game, taken from PokéWiki per game and region (Spanish, Italian and French place names were often English before). ROM hacks keep their English names for places, trainers and badges, as in the game, and say so. Game covers in the game picker, on the start page and on the Overview, downloaded on the first start; your own covers win. The "data unverified" notes are gone.
5.4 The Team tab's Box can list the team too ("Show team"), so one sort ranks every living link by base stats. The desktop side rail folds away with an arrow at its foot. On the start page, runs can be marked as won (a gold Hall of Fame card) or lost (greyed out) and deleted in two clicks. The next level cap shows the team level beside it ("24 / 22") in the rail and the phone's top bar. "Open only" on the Encounter tab keeps the route saved last after a reload. Fixed: the Pokémon picker of an empty entry was cut off, and Box entries overlapped their buttons on narrow screens. HeartGold/SoulSilver route order corrected up to the second badge. Devices in other languages get English. Prisma 7; installs without Docker need Node.js 20.19 or newer.

Acknowledgements

This project stands on the work of others. Thank you!

Licenses and attributions in full: THIRD_PARTY_NOTICES.md.

License

The source code is released under the MIT License. Data and artwork from third parties keep their own terms; see THIRD_PARTY_NOTICES.md.

Disclaimer: This is an unofficial, non-commercial fan project. It is not affiliated with, endorsed by, or connected to Nintendo, Game Freak, Creatures Inc. or The Pokémon Company. Pokémon names, game data and artwork are the property of their respective owners.

This repository and the published Docker image deliberately contain no copyrighted artwork:

  • Pokémon sprites are downloaded from PokéAPI when the app is set up.
  • Infinite Fusion's ball icons come from the game's public repository at container start.
  • Fusion sprites are loaded by the browser straight from the Infinite Fusion community's sprite server and never stored.
  • Trainer pictures are downloaded from Pokémon Showdown when the app is set up, unless you bring your own.
  • Game covers are downloaded from libretro-thumbnails and Wikipedia when the app is set up, unless you bring your own.

Requirements

Internet access on the first start: it downloads the Pokémon sprites, badges and trainer pictures (a few minutes). Installing the app on a phone needs HTTPS, for example through a reverse proxy.

Related apps

Explore more like this

Explore all

Details

Repository
ghcr.io/smiei/lockstep:latest
Last Updated2026-11-02
First Seen2026-11-02

Runtime arguments

Web UI
http://[IP]:[PORT:3000]
Network
bridge
Shell
sh
Privileged
false

Template configuration

WebUIPorttcp

Port of the web interface.

Target
3000
Default
3000
Value
3000
DatabasePathrw

Your runs (SQLite database) and imported game packs. Back this folder up.

Target
/app/db
Default
/mnt/user/appdata/lockstep
Value
/mnt/user/appdata/lockstep
Pokémon spritesPathrw

Downloaded once and kept across updates. Without it, every image update downloads them again (a few minutes).

Target
/app/public/pokemon-sprites
Default
/mnt/user/appdata/lockstep/pokemon-sprites
Value
/mnt/user/appdata/lockstep/pokemon-sprites
Trainer picturesPathrw

Boss-trainer pictures, downloaded once and kept across updates. Your own pictures go here too and are never overwritten.

Target
/app/public/trainers
Default
/mnt/user/appdata/lockstep/trainers
Value
/mnt/user/appdata/lockstep/trainers
Game coversPathrw

Game covers, downloaded once and kept across updates. Your own covers (game id .png or .webp) go here too and are never overwritten.

Target
/app/public/covers
Default
/mnt/user/appdata/lockstep/covers
Value
/mnt/user/appdata/lockstep/covers
Default colourVariable

The colour every device starts with, icons included. Each device can still pick its own in the gear menu.

Target
DEFAULT_SHELL_COLOR
Default
green|red|blue|yellow
Value
green
Default languageVariable

The language a browser tab starts in: de, en, fr, es or it. Empty: each browser's own language. A language picked in the gear menu always wins.

Target
DEFAULT_LANGUAGE
Trainer picture downloadVariable

off skips downloading the boss-trainer pictures from Pokémon Showdown.

Target
TRAINER_SPRITES
Default
on|off
Value
on
Cover downloadVariable

off skips downloading the game covers.

Target
COVERS
Default
on|off
Value
on
DATABASE_URLVariable

Where the database lives inside the container. Leave as is.

Default
file:/app/db/nuzlocke.db
Value
file:/app/db/nuzlocke.db