Download the report
# Seraph Codex — project report for Claude
Written so you can continue the work without the previous chat. The user is Justyn Murray. He is the only user of this studio. He will judge anything you ship by using it himself. He has already rejected a Windows build. Do not ship another one you have not opened on a real Windows desktop.
You are being asked in because the previous builder can edit the site and compile an exe from a Linux sandbox, but cannot sit in a Windows window and click through it. You can. Use that.
Do not put secrets in git, in the UI, or in a public file. The pin and the long key are worker secrets. You do not have them. He types the pin.
---
## 1. The job in front of you
Build a real Windows program for his decks.
Not a browser. Not Edge. Not WebView2. Not Electron. Not a local web server whose screen is the existing website. He opened that, and he named it: "this is not a program, this is a window extension disguised as a program, not the same thing. practically the same thing as topdecked."
TopDecked is the insult. He means a deck tool that is a web view wearing a title bar. Throw that approach away. Do not improve the wrapper.
What he does want:
- His own application, with its own windows, that he can install or open on Windows.
- A source project he can customize through code: cosmetics and functions. Views and styles are files he can change and rebuild. Two injected JavaScript files next to an exe are not that.
- The program and https://seraphdesigns.net are two doors into one studio. They share deck data. They do not share a UI.
- Offline, the program works alone. Edits are saved on the machine at once.
- Online, deck edits sync. A new deck, an old deck, any change to a deck. Nothing else needs to sync.
- He enters the same pin the site already uses. A wrong pin says so. A right pin opens the program without first downloading every image on the site.
- The first screen is the deck library. Choosing a deck opens it. Every card is reachable: name, mana, type, rules, power and toughness, the section it is in. He can edit.
- If the library is empty, the screen says why. It does not spin.
- Scryfall is called only when he pulls a printed card and its art. One fetch, then the card lives locally. No price polling. No live Scryfall mirror.
- One self-contained exe is enough. Windows may warn once because it is unsigned. He should not have to install .NET.
- The exe is private. Only a valid studio session may download it. A public URL for the exe is a back door. He said that in those words.
Acceptance, in the order he will try it:
1. Double-click the exe. A window opens that is obviously not a browser. No address bar. No site chrome. No loading screen from the website.
2. The pin matches the site. Wrong pin is rejected as a wrong pin.
3. His decks are listed. He can find one by typing.
4. He opens a deck, sees the commander and every section, and opens a card.
5. He edits a card, quits, and opens the program with the network off. The edit is still there.
6. He goes online. That edit is on seraphdesigns.net. An edit he then makes on the site shows up in the program.
7. He can point at a view file and a style file and know those are what he changes.
WPF or WinUI 3 is the right stack, because you can run it. Avalonia is acceptable only if you actually open the window. A web view is not acceptable in any language.
Suggested shape:
- a solution
- a UI project, one window or view per screen
- a library project that owns disk and the domain, with no UI in it
- run from the IDE
When you ship, overwrite the private object only (see section 6). Tell him to use the button inside the studio again. Replacing the file does not update an exe he already launched.
---
## 2. How he works
He designs custom Commander decks and also keeps printed and hybrid lists. He wants the work done, not a plan handed back. He has said not to hand him work you can do yourself.
He is direct when something is wrong. He expects the cause removed, not a patch on the thing he rejected. The WebView program was that kind of mistake. Do not defend it.
He cares about words on cards, balance, and whether a card does something of its own. He does not want filler, reprints, or renamed Wizards cards in the custom lists. He does not want every creature in a deck to be the same ability with a different name.
Visual taste on the site: dark, scholarly, brass and parchment, not a SaaS dashboard. He has rejected brass corner ornaments, a "spotlight" shelf, and a "sit at the table" block. He wants type kept off the edge of panels. Halos that clip text are a bug. He will send a screenshot when something is flush or cut off.
He asked for the Windows work to move to you specifically because desktop interaction is the gap. Do not spend the first session redesigning the website.
---
## 3. What the studio is
Seraph Designs is his private Magic: The Gathering Commander workshop. The public name on the site is Bestiary Codex. The front page title line he asked for is the studio, not a slogan. An older line, "a workshop, not a menu," was removed at his request. Do not put it back.
Three collections, one door:
- Printed. Decks built from real Wizards cards. Pictures and names come from Scryfall when the card is added. He does not want ongoing price updates.
- Custom. Homebrew commanders and full original lists. This is the bulk of the studio. Hundreds of decks. Opening this collection used to lag. Any new client has to stay responsive with that many decks.
- Hybrid. A printed card stays a printed card. A custom card stays in the custom frame. He moved at least one deck here on purpose ("Go-Shintai"). Hybrid decks must show up in the hybrid shelf the same way custom and printed decks show in theirs. This was broken before and he noticed.
A deck moves through stages: printed, ready, wip, import, requests. Shelving a finished deck into the Library is a separate flag. He uses that shelf as a count of finished work.
The site also has a builder. He was unhappy with the first version. The order he wants, once a commander is chosen:
1. Choose the commander first. Do not dump him into a mixed pile.
2. Then build backwards: lands, ramp, card draw, artifacts, enchantments, instants and sorceries, then creatures.
3. Tokens appear from the cards he actually picked, and the token has to be a real token, not an empty placard with only a name.
4. Color identity comes from the commander and must be true. A Boros planeswalker was labeled white, black, red, and blue. That is a bug if it returns.
5. He wants to filter one color out of the commander's colors. Lands included.
6. Hovering a card must not shake the page, and the hover must not be clipped by the deck list.
Custom planeswalkers have a note side with their abilities. Selecting one must show that note side. It did not.
The site has a delete control on a card, an art upload, and a move. He asked for delete because it was missing.
There is an idle effect: after about a minute with no input, miniature cards fall, slowly, like a sparse rain. Only cards that have art. Backs are the normal Magic card back. Size varies so some read as near and some as far. Glints are rare and sit on the card, not beside it. He wanted it more random than a looping pattern. Do not spend time on this unless he asks. It is not the desktop job.
The page background is a fixed library photograph. He wanted the shelves and the table to look fuller or emptier as more decks are shelved. The wrong implementation was a strip of thumbnails down the side. He rejected that. The right idea is the same room, with more or fewer decks on the shelves and the table.
---
## 4. How he judges a custom card
Use this if you generate or edit cards. The program does not have to enforce it. A card that ignores it will be rejected when he reads it.
A card is valuable when it has its own engine and can feed that engine, or when the passive value is already large by the time someone removes it. His examples: Omnath, one land is one 5/5. Koma, every turn is a 3/3.
His own commander, Hashmal, on the Adamantine Court deck, after he edited it:
- Hashmal counts himself.
- Both hits write, and power updates.
- Making tokens passively is allowed. Passive creation and combat creation on the same card is not. One or the other.
- The deck may make many tokens, and those tokens may make more. That stays fair because an opponent can still block. Trample is what turns the tide. Leave room to slow that tide. Do not "fix" the card by putting both engines on it.
He will throw out a pile of creatures that all tap to make the same token, and he will throw out a two-drop that is a 5/7. Mana value and size have to match the role. Variety of function matters more than filling the 24 creature slots.
Custom cards are original. No reprints and no renamed Wizards cards in the custom lists. Printed decks are the opposite: they are real cards, fetched as themselves.
---
## 5. Architecture you must not break
| Piece | Value |
|---|---|
| Site | https://seraphdesigns.net |
| Pages project | Cloudflare Pages, `bestiary-codex` |
| API | https://codex-api.justynmurray01.workers.dev |
| Worker name | `codex-api` |
| Database | D1 binding `DB`, id `3378bafe-da07-4699-a341-1b64e09c173a` |
| Media | R2 binding `MEDIA`, bucket `codex-media` |
| Private | plain text `CODEX_PRIVATE` = `1` |
| Pin | secret `CODEX_PIN` |
| Long key | secret `CODEX_TOKEN` |
| Origins | `CODEX_ORIGIN`: seraphdesigns.net, www, mtg.seraphdesigns.net, bestiary-codex.pages.dev and its preview hosts |
The site is a static front end. Hashed files: `head.*.js` points `CODEX_API` at the worker and names the current `app.*.js` and `store.*.js`. `boot.*.js` shows the pin if `localStorage` has no `codex.token`. The app is one large script, not a framework tree you will enjoy splitting. Change it only when the site itself is the task.
Auth:
- `POST /api/unlock` with `{"pin":"..."}` returns `{token, expires}`.
- 401 `wrong_pin`. 429 when locked out. 503 `no_pin_set` means the secret is gone. That is an emergency.
- Later calls send `Authorization: Bearer <token>`.
- `GET /api/session` returns `{writable, version, readOpen}`. `readOpen` is false. No token means not writable.
- `GET /api/state` is 401 without a writer. With a writer it returns `{version, state}`. `version` is an integer in D1 `meta`. It was past 21000. Do not hardcode it.
- `POST /api/mutate` writes. Body is `{version, ops:[...]}` or `{version, state, authoritative:true}`.
- Wrong version: 409 `conflict`.
- A state push with fewer than 90 percent of the existing decks: 409 `suspicious_wholesale`, even if `authoritative` is true. Leave that guard. An empty cache must never be pushed.
- `authoritative: true` deletes decks, docs, library rows, and paper rows missing from the payload. Cards inside a deck are upserted. A full-state push does not delete one removed card. To delete a card, send the granular op the worker already uses. Read the worker before inventing an op name.
Ops the worker already applies include deck edit, overview, deck art, doc add/edit/delete, library save/delete, gallery add/delete. Card rows are keyed by deck id, section, and entry.
Do not redeploy the worker unless you must. A deploy that drops the D1 binding, the R2 binding, or a secret takes the studio down. Secrets are not in any file. If you upload a script, keep every existing binding. Afterward, check `GET /api/session` still returns JSON, and `POST /api/unlock` with a bad pin still returns 401 `wrong_pin`. 503 means you erased the pin.
`GET /api/state` was large enough to hit the worker CPU limit. The heavy derived blobs (`mechanics`, `assessments`, `autogen`) are no longer in that payload. They load from `/api/blob/<key>` if the site needs them. The desktop program does not need them. Do not add them back to the cold load.
Rate limits, per IP per minute, roughly: `/state` 12, `/mutate` 60, `/unlock` 10, `/version` 240. Poll `/api/version`. Fetch `/api/state` only when the version moved or you are about to push.
Other routes you can ignore for the program: `/prices`, `/pricehist`, `/forge/*`, `/versions`, `/paper`, `/fetch`. The website uses them. The program does not have to.
---
## 6. The private download
This is already how the exe is supposed to be delivered. Keep it.
- Public r2.dev on bucket `codex-download` is off. Leave it off.
- The file is the R2 object `desktop/Codex.exe` in bucket `codex-media`.
- `GET /api/desktop` returns 401 `not_writer` with no token, and streams that object when the bearer is valid. It is an attachment named `Codex.exe`.
- On the site front page, under "On this machine", a button calls that route with the bearer from `localStorage` `codex.token` and saves the blob. There is no raw file URL in the page.
- The old public page `codex-download-3mp.pages.dev` no longer hosts the file. Do not put it back.
When your exe is good, overwrite `codex-media/desktop/Codex.exe` and nothing else.
---
## 7. Deck JSON the program has to read
`GET /api/state` builds each deck like this:
```
{
id, name, title, stage, commanderName,
ci: ["W","U","B","R","G"],
archetype, theme, power, overview, plan, art, back,
sections: { "Commander": ["Name"], "Creatures": ["Name"] },
cards: { "<section>\\u001f<name>": Card },
commanders: [Card],
commander: Card or null,
notes: [{id, ts, title, body}],
matchups: [{i, opponent, verdict, w1, w4, body}],
matchupIntro, gallery
}
```
The card key is the section name, then the character U+001F, then the entry name. `sections` is membership and order. `cards` is the body. Rules text is the field `text`. Also used: `name`, `mana`, `cmc`, `type`, color fields, power and toughness. Do not rename them. The website reads them.
Signed image URLs in a fresh download expire. Do not write those signatures back to D1. The site stores a stable id and signs on the way out. Before you compare or push, strip the host `https://codex-api.justynmurray01.workers.dev` and the query string, and keep `/api/images/<id>`. If you compare raw JSON, every sync looks like an edit, and you will push forever.
Printed decks are in `paper` and `real`. Hybrid tagging lives on custom decks. The first program can open custom decks only. It must not wipe `paper`, `real`, `codex`, or `library`. If you push state, start from the remote document and replace only the decks that changed.
The state root also has `codex` (design documents), `library` (card families), `log`, and `v: 2`.
Local files the rejected build already used. You may keep the paths:
```
%LOCALAPPDATA%\SeraphDesigns\Codex\state.json
%LOCALAPPDATA%\SeraphDesigns\Codex\link.token
%LOCALAPPDATA%\SeraphDesigns\Codex\decks.base.json
```
`link.token` is the bearer from unlock. Never show it.
Sync rules he already agreed to:
- Save on the machine first.
- Offline, say that the edits are staying here. Do not pretend they were sent.
- A deck changed only here is sent up.
- A deck changed only on the site is brought down.
- A deck changed on both since the last sync keeps the machine's copy, and the status line says so.
- Never push a local library that is a small fraction of the remote one.
- The first sync, if the machine has no decks, is a pull. Not a push.
- Remember the canonical JSON of each deck after a good sync, so the next comparison has a base.
---
## 8. What is on disk, and what not to continue
The rejected program is a .NET 8 WinExe. It opens WebView2 to `http://127.0.0.1:<port>/`. An `HttpListener` serves a saved copy of the site and proxies or caches the API. The window is the website. That is the whole program. He used it. The decks were not something he could open. He does not want a revision.
In the `codex-desktop` project:
- `StudioHost.cs` is that server and that WebView host. The csproj excludes it from compile. Leave it excluded.
- `Library.cs` is an unfinished native data layer: pin, deck list, card text, a deck merge. It has a `--self-test` path. It was never given a window, never compiled as the Avalonia app, and never run.
- `App.cs` and `Program.cs` are the start of an Avalonia shell. There is no window class. The packages are declared. Do not ship this half.
- `site-manifest.txt` is a file list for the rejected downloader.
The exe he may already have is the WebView build. Do not keep patching it.
There is no separate git history you should trust for the desktop app. The live site is the Pages project. The live data is the worker.
---
## 9. Site behavior worth knowing if you touch it
You should not need to, for the program. If you do:
- Back from inside a collection returns to the three-shelf door, not the front page.
- Workshop entries are alphabetical.
- Text sits inset from panel edges, on every screen, including inside halos.
- The front page does not show a giant custom-deck count as its header.
- Delete exists on a card.
- The Windows button is the private download in section 6. Do not replace it with a public link.
Hashed assets: if you change `app.js` or `head.js`, hash the filename, point `head` at the new app file, point `index.html` at the new head file, and deploy Pages project `bestiary-codex` on branch `main`. Forgetting the hash means the site keeps serving the old file.
---
## 10. Definition of done
He double-clicks a private exe, gets a program that is not the website, opens his decks, edits a card offline, and sees that edit on the site when he is online again. The source is a normal Windows project he can change. The public internet cannot download the exe. The worker still demands his pin, and a bad deploy has not erased it.