A free, open-source companion app for Slay the Spire 2. Tracks every run you finish locally on your own machine, and shows you a live feed of other players who are online and looking for a co-op partner right now.
Click the screenshot to try the web version.
Try it in your browser · spirevault.app · Download for macOS
flowchart LR
subgraph clients["Clients"]
Mac["Vault.app<br/>SwiftUI · macOS"]
Web["Web companion<br/>app.spirevault.app"]
Mob["Mobile (iOS)<br/>SwiftUI · iPhone/iPad"]
end
subgraph cf["Cloudflare edge"]
Pages["Pages<br/>static HTML/CSS/JS"]
Worker["Worker · TypeScript<br/>presence · invites · auth · runs"]
KV[("KV namespace<br/>roster · invites · sessions · runs")]
end
Steam[("Steam OpenID 2.0<br/>Valve hosted")]
Disk[("Local save folder<br/>.run files")]
IDB[("IndexedDB<br/>cached runs")]
AI[("OpenAI / Anthropic<br/>(Run Coach · BYO key)")]
Mac -- reads --> Disk
Web -- file picker --> IDB
Web -- cached --> IDB
Mac -- "embeds · WKWebView" --> Web
Mac -- "presence · invites · runs sync" --> Worker
Web -- "presence · invites · runs sync" --> Worker
Mob -- "presence · invites · runs sync" --> Worker
Pages -- serves --> Web
Worker -- verify signature --> Steam
Worker --> KV
Mac -. sign in once .-> Steam
Web -. sign in once .-> Steam
Mob -. sign in once .-> Steam
Mac -. "Run Coach Beta · direct, BYO key" .-> AI
Web -. "Run Coach Beta · direct, BYO key" .-> AI
classDef ext fill:#1a1525,stroke:#5b4080,color:#d8c8ff;
classDef store fill:#161220,stroke:#3d3458,color:#c8c0d8;
class Steam,AI ext
class Disk,IDB store
Single-UI rendering (v0.9.2). The macOS app no longer maintains a
parallel SwiftUI copy of every cloud panel. Stats, Co-op, Community
Highlights, and News are rendered by WKWebView pointed at
app.spirevault.app. The native sidebar drives the embedded tab via a
window.SpireVault bridge; locally-parsed runs from VaultCore are
pushed into the embedded page over the same bridge so the user sees
their real history without any cloud round-trip. Beta and Settings stay
native because they need NSPanel / Keychain / NSOpenPanel.
Single-WebView Steam sign-in (v0.9.3). The full Steam OpenID
round-trip — worker /auth/steam/start → steamcommunity.com →
worker /auth/steam/callback → app.spirevault.app/auth.html — runs
inside the embedded WKWebView, not the user's default browser. The
result: the session cookie + localStorage land in
cfg.websiteDataStore = .default() (visible to the embedded view),
and auth.html posts the verified payload through the JS bridge as
kind: "auth" so SteamAuth.acceptWebSession(...) seats the same
session natively. The sidebar pill, Co-op presence, and every native
API write all light up the moment the embedded view does. The
previous flow (NSWorkspace.open → browser → thevault:// callback)
left the cookie in Safari and, on dual-installs, delivered the deep
link to a second copy of The Vault.app — both fixed in v0.9.3,
plus LSMultipleInstancesProhibited = true for belt-and-braces.
Cross-device run sync (v0.5). When you sign in with Steam, the web companion uploads your parsed run history to a Steam-ID-keyed cloud copy. Open the iOS app on the same Steam account and your runs are already there — no re-import, no QR-code pairing, no separate account. Storage is the merged set across every device that ever uploaded for your Steam ID, deduped by run id, last-write-wins on duplicate ids, capped at 2,000 runs. Guests stay 100% local; no cloud copy is created until you explicitly sign in.
Two clients (native macOS, browser) parse the same canonical history.json
schema and share a stats engine: Swift on macOS, a JavaScript port in the
browser. The server is a single Cloudflare Worker (around 1k lines of
TypeScript) plus one KV namespace; no Durable Objects, no D1, no queues.
Run history never leaves the client. The Worker only stores what a user
explicitly publishes for co-op (Steam ID, persona, status, optional
Discord handle, session token).
Worker layout, KV schema, and deploy steps live in
Backend/README.md. Threat model and what is
explicitly not defended against is in SECURITY.md.
Not a mod. Spire Vault never injects into the game process, loads DLLs, or uses ModTheSpire. It only reads the
.runJSON files Slay the Spire 2 already writes to your save folder, the same way you could open them in a text editor. Run-history readers like this have existed for the original Slay the Spire for years and are accepted by Mega Crit and the community.Not affiliated with Mega Crit Games. Slay the Spire is a trademark of Mega Crit.
STS2's multiplayer is gated through Steam friends, which is the right call. It keeps the experience tight and abuse-free. But it leaves a missing layer: there's no way to find a partner before you Steam-friend them. Today, that means scrolling a Discord with a few hundred users and either finding a level-0 newbie or someone grinding A20 Heart kills.
I built Spire Vault to fill that exact gap, nothing more. It does not host games, route invites, or replace anything Mega Crit built. It just shows you who else is around at your level, and gives you a one-click way to reach out on Steam or Discord. From there, the actual game session goes through Steam friends like normal.
The run-tracker came along for the ride because once I was already parsing my own save files to compute my own skill tier, exposing the rest of the data in a clean UI was a few extra weekends of work.
Every screenshot below is a real capture of the v0.5 web companion
running on app.spirevault.app against sample data — same UI you get
once you sign in and import your .run files.
Share-Run card with real relic icons + card art baked into the canvas. Drop straight into Discord, Reddit, or X.
Click any run row to inspect the full deck and relic loadout.
Characters tab — per-character winrate at a glance.
Recent Runs with filter chips + click-to-inspect detail modal.
Live co-op presence feed at app.spirevault.app
There are two ways in. Both are free, both share the same live presence feed, and you can use them on the same Steam account.
- Grab the latest
Spire-Vault-vX.Y.Z.dmgfrom the Releases page. - Open the DMG and drag The Vault to your Applications folder.
- First launch is ad-hoc signed (I don't pay Apple's $99/yr developer fee just to keep this free), so right-click the app → Open, then click Open in the dialog. macOS only asks once.
That's the whole install. The app auto-detects your STS2 save folder. Co-op is one click away under the Co-op tab when you're ready.
Open https://app.spirevault.app in any modern browser. The web companion has the full feature set — co-op finder, run tracker (point it at your STS2 save folder via the File System Access API), KPI strip, winrate chart, image-rich Share-Run cards, and cross-device sync once you sign in with Steam.
The first import uses a one-time folder picker (browsers require
explicit consent to read local files). After that, the same browser
auto-refreshes silently every 60s when STS2 writes new .run files,
and signed-in users get a Steam-ID-keyed cloud copy that any other
device on the same Steam account can read on next launch.
A native Windows build is on the roadmap but it's a full rewrite (the Mac app is SwiftUI, which is Apple-only), so for now the web companion is the official Windows and Linux path.
Some people ask if I'm trying to replace Steam multiplayer or matchmake for them. I'm not. STS2 multiplayer is friend-gated through Steam — no third-party tool can route those invites. The Vault solves the finding half: who's around, what run do they want, and when does it start.
As of v0.10.0 (May 26, 2026) the Co-op Lobby is the default surface. The four-step flow:
- Sign in with Steam. Standard Steam OpenID, the same flow Steam uses for every other site. Your password never reaches my server.
- Quick Play, or host a room. Quick Play auto-matches you into the
best open room. Hosting takes three taps — goal & timing (character
- ascension target + planned start: now / 15m / 30m / 1h / when full), party shape (size, voice required / optional / quiet, ascension floor), review.
- Bring Discord with you. One click on a hosted room copies a
Discord LFG post that uses native
<t:UNIX:R>and<t:UNIX:t>timestamp tags. Paste it into your channel and Discord renders it as "Starts in 28 minutes" — and re-renders five minutes later as "Starts in 23 minutes" — without anyone editing or re-posting. Each viewer also sees their own local time. No bots, no integrations. - Synced GO. Everyone in the lobby shares the same countdown badge. At T-60s, opted-in users get a chime + browser notification. At T-0, a green pulse + "Launch Steam now" CTA appears on the row. Host can hit "⚡ Start now" to fast-forward.
The Campfire Log ribbon below the hero stats is the persistent layer.
Every party you join gets logged locally; every teammate becomes a
friend with a count and a last-played stamp. Open My Co-op and you
can send any of them a heart (one per teammate per 24 hours, so it
stays meaningful). XP curve is real — party joined: +10, completed: +15,
heart-run goal: +25, heart sent: +5 — and the bar inside the Rank tile
fills smoothly as you accumulate. The whole log is localStorage +
BroadcastChannel for cross-tab sync; no server side yet, by design.
Don't want the lobby surface? Switch to Classic Co-op in the header is one click and persists per-browser. The classic live-roster flow (see who's around, fire a canned invite, copy a Discord handle) is unchanged.
Server-side kill switch. Default-on releases need a bail-out, so
there are three layers: per-browser via ?beta=off / ?beta=kill
URL params, server-pushed via the worker env (COOP_LOBBY_BETA_KILL=1
flips every connected client back to Classic on the next poll, ≤15s
focused / ≤60s hidden — no deploy needed), and code-level via
ENABLE_COOP_LOBBY_BETA = false + redeploy. Classic stays available
under all three.
Total infrastructure cost: $0. The whole thing runs on Cloudflare's free tier. The only fixed cost in this entire project is the $14/year domain, which I'm paying out of pocket because I think solving this problem is worth fourteen bucks.
Run Coach is a small, opt-in floating window that sits over Slay the Spire 2 and answers "what should I play next?" using your own AI key. Lives behind the Beta tab in both the macOS app and the web companion. Off by default, free to never touch.
flowchart LR
User["You<br/>asks the coach"]
Pill["Run Coach overlay<br/>(NSPanel · macOS / Document PiP · web)"]
Save["Live save reader<br/>current_run.save"]
Capture["Screen capture<br/>ScreenCaptureKit (macOS 14+) / getDisplayMedia"]
Provider[("OpenAI / Anthropic<br/>your account")]
User -->|chip tap or ⌥Space| Pill
Save -->|character · HP · gold · deck · relics| Pill
Pill -->|active display, downscaled to ~1280px JPEG| Capture
Capture -->|prompt + context + image + your API key| Provider
Provider -->|"reply text"| Pill
Pill --> User
classDef ext fill:#1a1525,stroke:#5b4080,color:#d8c8ff;
class Provider ext
What it does
- macOS app: a 260×40 pill at the top of your active display that
rides over fullscreen STS2 (
NSPanelwith.canJoinAllSpaces+.fullScreenAuxiliary) and is invisible to screen recordings (NSWindow.SharingType.none). OBS, Zoom, and QuickTime can't see it. Streamer-safe by default. - Web companion: a real native always-on-top OS window via the
Document Picture-in-Picture API (
documentPictureInPicture.requestWindow()). Sits over fullscreen STS2 on Chromium browsers. Safari/Firefox don't ship the API yet — the Beta tab detects the gap and points users at the macOS app. - Both surfaces share the same chat UI: a header pill with the Vault emblem, quick-ask chips (Assist · Card pick · Boss relic · Shop · Path · Event · Fight · Plan), text input, screenshot toggle, and send.
- The macOS overlay reads your live
current_run.savefile continuously and passes your character, HP, gold, deck, and relics to the model automatically. The Path chip, for example, reads the actual map and tells you whether to go left, right, or straight — not a generic "elites are worth taking" non-answer. - On multi-monitor setups, pin which display STS2 runs on via Settings → Game monitor so the overlay always captures the right screen.
How it works
- You add an OpenAI or Anthropic key under Beta features. Stored
in the macOS Keychain on the desktop, in
localStorageon the web. The Vault servers never see the key. - You tap a chip or press the hotkey (⌥Space by default). The overlay
reads your current run state from
current_run.save, captures the active display via ScreenCaptureKit (macOS) orgetDisplayMedia()(web), and downscales it to ~1280px wide as a JPEG. - The app POSTs the prompt + run context + image directly to
api.openai.comorapi.anthropic.com. No proxy, no Vault Worker in the loop. - The reply streams inline. Frames live in memory just long enough to upload — nothing is recorded, nothing is replayed.
What it doesn't do
- No process injection. Run Coach never injects, hooks, or scans
the STS2 process. It reads your
current_run.savefile (the same plaintext JSON STS2 writes itself) and takes a screenshot when you ask — nothing else. - No Vault-hosted AI. There's no subscription, no tier, no Vault proxy. You bring the key, you pay the provider, you control the spend.
- No silent telemetry. A 401 from your provider, a model that doesn't exist, a screen-recording-permission denial — all of that surfaces in the Beta tab's live test panel, never in a remote logger.
The web overlay is a real OS window, which means OBS/QuickTime can see it on the web — it's only the macOS build that's streamer-safe. The Beta tab tells you this up front; if you stream, use the .dmg.
The macOS app ships its own in-app updater. From the menu bar:
- Vault → About The Vault — version, build number, credits, GitHub link.
- Vault → Check for Updates… (⇧⌘U) — fetches the latest GitHub Release, compares against your build, opens Settings if something newer is available.
- Help → What's New — opens the
CHANGELOG.md. - Help → Run Coach (Beta) — How it works — jumps to the Run Coach section above.
Internally that's UpdateService (VaultApp/App/UpdateService.swift) —
it polls api.github.com/repos/c3rooks/SpireVault/releases/latest,
downloads the DMG to ~/Library/Caches/com.coreycrooks.thevault.app/updates/,
verifies size + optional SHA-256 from the release notes, mounts the
DMG with hdiutil, swaps the running .app in place, and relaunches.
We deliberately don't use Sparkle: it expects an EdDSA-signed appcast
- a Developer ID-signed bundle, neither of which fits an ad-hoc-signed free project. The trade-off (relying on HTTPS-to-GitHub + an optional SHA-256 line in the release notes) is documented inline in the service.
The run tracker is fully offline. Nothing about your runs, decks, win rates, or anything else ever leaves your Mac unless you sign into co-op.
When you sign into co-op, the server stores:
- Your verified Steam ID, persona name, and avatar URL (all from the public Steam Web API; nothing private).
- A status, a freeform note, and an optional Discord handle. Whatever you type into the app.
- A session token, valid for 30 days. Sign out and it's gone instantly.
That's the entire list. The server cannot read your save folder, run history, password, email, payment info, or anything else, because it isn't sent and never has been. I documented the full threat model and what's deliberately out of scope in SECURITY.md.
If you want to verify all this yourself, the Worker code in Backend/
is the exact code running in production. Anyone can audit it. Anyone can
fork it and point their own Spire Vault at a private deployment.
If you want to run it without trusting a pre-built binary, or you want to hack on it:
git clone https://github.com/c3rooks/SpireVault.git
cd SpireVault/VaultApp
brew install xcodegen # one-time, generates the .xcodeproj
make runRequirements:
- Xcode 16 or later (macOS 13+ deployment target)
xcodegen(one Homebrew install away)- macOS on Apple Silicon or Intel
The CLI lives at TheVault/ and builds independently with swift build
inside that directory. Useful if you want to dump your run history to
JSON or CSV without launching the full app.
.
├── VaultApp/ Native macOS SwiftUI app (the thing you install)
├── TheVault/ Swift package: VaultCore library + `vault` CLI
├── Backend/ Cloudflare Worker for the co-op presence feed
├── Site/ Marketing landing page (spirevault.app)
├── Web/ Browser companion (app.spirevault.app)
├── SECURITY.md Threat model + what's defended-against
├── CHANGELOG.md What shipped when, what broke along the way
└── RELEASING.md How to cut a new release
VaultApp depends on TheVault for parsing/stats so the two share code
without duplicating it. Site and Web are pure-static Cloudflare Pages
deployments with no build step and no runtime dependency. Backend is a
single Worker, around 1k lines of TypeScript total.
| Component | Hosted on | URL |
|---|---|---|
| macOS app | GitHub Releases | /releases |
| Backend Worker | Cloudflare Workers | vault-coop.coreycrooks.workers.dev |
| Marketing site | Cloudflare Pages | https://spirevault.app |
| Web companion | Cloudflare Pages | https://app.spirevault.app |
I'm Corey Crooks. I play STS2 (Ironclad and Defect, mostly), I write code professionally, and I built this because I got tired of scrolling Discord trying to find a co-op partner.
- Personal site: coreycrooks.com
- GitHub: @c3rooks
- Reddit: u/c3rooks
- For security reports: see SECURITY.md
If you want to talk about a feature, a bug, or why you think one of my
design decisions is wrong, open an issue. If it's a security issue,
please go through the disclosure process in SECURITY.md first.
Issues and PRs welcome. The project is small enough that "open a PR" is the entire workflow. No CLA, no contributor guide novella. If you want to add a feature and aren't sure if I'd merge it, open an issue first and ask. I'd rather say "yes, but go this way" than have you spend a weekend on something I'd close.
MIT. Do whatever you want with it. Fork it, run it private, sell a paid version with extra features, ship a Linux port. All fine. The only thing I ask is keep the privacy posture intact if you fork: local run history stays local, co-op stays opt-in, no surprise telemetry. The community will notice and it'll reflect on the original.
This project is officially partnered with the Slay the Spire 2 LFG Discord Server — a growing community focused on matchmaking, strategy discussion, daily runs, modding, and community events for Slay the Spire 2.
The server also helps support development and beta testing for this companion app through community feedback and testing.
Join the community here: Slay the Spire 2 LFG Discord
To Mega Crit, for making the best card game ever and not being weird about fan tools. To the STS2 Discord regulars who answer "anyone want to co-op?" at 11pm on a Tuesday. You're the reason this exists.