Repository navigation
Architecture
English · Español
Proxy Builder is a Tauri 2 desktop app: a React/TypeScript front end that only draws screens and holds screen state, an app crate (proxy-builder) that owns I/O (SQLite, disk, network, Tauri), and a pure crate (proxy-core) that owns every deterministic computation (parsing, page geometry, image processing, PDF, statistics, card-design engine). This page is the whole-system view; each box links to the page that goes deep.
| Layer | Where | Owns | Must not |
|---|---|---|---|
| React front end | src/ |
Rendering, screen state (Zustand stores), i18n, themes | Compute geometry, talk to the network, call invoke directly |
App crate proxy-builder (lib proxy_builder_lib) |
src-tauri/src/ |
Tauri wiring, commands, SQLite library, blob store, Scryfall and other sources, caches, jobs, pbimg:// protocol, updater |
Hold pure logic that could be tested without Tauri |
Core crate proxy-core
|
src-tauri/crates/proxy-core/src/ |
model, decklist, layout, imaging, pdf, stats, diseno (card designer engine), paper, units
|
Know about Tauri, the network, the database, or AppError
|
Game engine crate mesa
|
src-tauri/crates/mesa/src/ |
The game table: model, action catalogue, deterministic randomness, visibility, undo and replay (see Game Engine) | Same as proxy-core, plus any clock or outside randomness |
| Room server (separate repository) |
server/ (ignored by the app) |
Identity without accounts, playgroups, invitations, rooms, signalling, host election and migration, the relay callout (see Online Play) | Carry the game itself: games are P2P between the apps |
The Rust crates are members of one Cargo workspace declared in src-tauri/Cargo.toml (members = [".", "crates/proxy-core", "crates/mesa"]). proxy-core has its own error enums (LayoutError, ImagingError, PdfError, ProbeError, DisenoError, PackError, LineError) and the app converts them to AppError with From impls (see IPC and Error Handling).
flowchart TB
subgraph WV["WebView (React 19, no network)"]
UI["features/* (decklist, preview, export, variants, library, disenar, ...)"]
ST["stores/* (deck-store, plan-store, ui-store, trabajo-store, actualizacion-store, edicion-store)"]
IPC["lib/ipc.ts: call() and on()"]
IMG["lib/image-url.ts: pbimg:// URLs"]
UI --> ST
UI --> IPC
UI --> IMG
end
subgraph APP["App crate proxy-builder (src-tauri/src)"]
LIB["lib.rs: run(), plugins, setup, generate_handler!"]
CMD["commands/*.rs (108 commands)"]
PROTO["protocol.rs + render.rs (pbimg://)"]
STATE["state.rs: AppState"]
STORE["store/*: Db (SQLite), BlobStore, settings, presets"]
SCRY["scryfall/*: client, cache, resolve, nombres"]
FUE["fuentes/*: Externo allow-list, Cockatrice, MTGJSON, MTG.io"]
DIS["diseno/*: designer app layer"]
MISC["package.rs, ocr.rs, originales.rs, temas.rs, actualizar.rs, biblioteca.rs"]
LIB --> CMD
LIB --> PROTO
CMD --> STATE
PROTO --> STATE
STATE --> STORE
STATE --> SCRY
STATE --> FUE
STATE --> DIS
end
subgraph CORE["Core crate proxy-core (pure)"]
MODEL["model, units, paper"]
DL["decklist"]
LAY["layout: PagePlan"]
IM["imaging"]
PDF["pdf"]
STA["stats"]
DSN["diseno: engine"]
end
IPC -- "invoke / events" --> CMD
IMG -- "GET pbimg://localhost/..." --> PROTO
CMD --> CORE
PROTO --> CORE
SCRY -- HTTPS --> NET1["api.scryfall.com, cards.scryfall.io"]
FUE -- "HTTPS, allow-list only" --> NET2["Cockatrice, MTGJSON, MTG.io, tweakcn, GitHub"]
STORE --> DISK1[("app_data_dir")]
PROTO --> DISK2[("app_cache_dir")]
These come from CLAUDE.md; each is an invariant that code review and CI enforce or that the layout of the code depends on.
| Decision | Why | Where it shows |
|---|---|---|
| All heavy work lives in Rust: list parsing, Scryfall, downloads, caches, page geometry, image processing, PDF, statistics. React only draws and holds screen state. | One implementation, testable with cargo test, and the WebView never blocks on CPU work or touches the network. |
proxy-core modules; src/lib/ipc.ts as the only bridge |
One geometry for screen and paper. PagePlan (millimetres) is computed once by proxy_core::layout::plan and consumed by both the preview and the PDF composer. The screen never recomputes positions. |
What you see is what prints. A second layout implementation would drift. |
commands/layout.rs → compute_page_plan; stores/plan-store.ts → usePlan; commands/export.rs → exportar calls plan(deck); proxy-core/src/pdf/mod.rs doc: "does not compute positions, it draws the PagePlan" |
One image pipeline for screen and paper. Thumbnails and PDF images go through the same render() with a RenderSpec. |
Same reason; preview fidelity. |
src-tauri/src/render.rs → spec_de_miniatura, spec_de_casilla; proxy-core/src/imaging/pipeline.rs
|
Deck images live in the blob store (app_data_dir/blobs/, addressed by SHA-256), never in the cache. The cache (app_cache_dir/renders/) holds only derivatives and can be deleted at will. |
Decks depend on their images; the OS may clear caches. cache_clear must never lose user data. |
store/blobs.rs → BlobStore; render.rs → AppState::carpeta_renders; commands/library.rs → cache_clear (test asserts renders is under paths.cache and not under paths.data) |
The preview unmounts images of pages that are not visible (IntersectionObserver whose root is the ScrollArea viewport). |
Only visible cards are rendered by Rust and held by WebKit. The document itself never scrolls, so a document-rooted observer would consider everything visible. |
src/features/preview/visibilidad.tsx → useVisible, RaizScroll
|
proxy-core is pure: no Tauri, no network, no database. |
Fast tests (cargo test -p proxy-core), clear boundary. |
proxy-core/src/lib.rs doc comment; Cargo.toml of the crate (no tauri, reqwest, rusqlite) |
lib.rs only boots and registers; each area has commands/<area>.rs. |
An earlier project's lib.rs grew to ~12k lines; the file is kept small on purpose. |
src-tauri/src/lib.rs (about 200 lines, doc comment says so) |
Commands return Result<T, AppError> ({ code, params }); the screen translates with useLang().mensaje(e). Rust never writes prose for the user. |
Language can change without Rust changes; one place for texts. |
src-tauri/src/error.rs; src/lib/errores.ts
|
Shared types come from Rust: #[derive(TS)] #[ts(export)] and pnpm bindings write src/bindings/. Never edited by hand. |
Front/back type drift is impossible to ship silently. |
.cargo/config.toml sets TS_RS_EXPORT_DIR
|
The screen talks to Rust only through call() in src/lib/ipc.ts. |
The Commands interface makes the compiler check command names and argument/return types. |
src/lib/ipc.ts |
| The WebView never goes to the internet; all network is done by Rust. | Single place for rate limiting, allow-lists, redaction, logging; strict CSP. |
tauri.conf.json CSP (connect-src is only ipc:); scryfall/client.rs; fuentes::Externo
|
Everything outside Scryfall is downloaded through fuentes::Externo: closed allow-list of site and path, every redirect re-checked, size caps, images always decoded and re-encoded. |
Limits the blast radius of a compromised or changed third-party site. | External Sources |
| Changes to the maintainer's tree arrive as uncommitted edits; the cloud workflow never pushes. | Process rule, not code. | See Conventions and Testing and CI |
There is one OS process (the Tauri app) with three execution contexts:
-
Rust main process. Tauri event loop, plugin handlers,
AppState. Sync#[tauri::command]functions run on the Tauri side;asynccommands run on Tauri's async runtime (tokio). Long work is moved totauri::async_runtime::spawn(network-bound jobs:resolve_decklist,identify_batch) orspawn_blocking(CPU-bound:export_pdf). Within a job, rayon thread pools do per-image parallel work (the PDF export builds its own pool of at most 8 threads incommands/export.rs→exportar). -
The WebView (WebKitGTK on Linux, WebView2 on Windows, WKWebView on macOS). Loads
dist/(or the Vite dev server on port 1420). Its CSP forbids any network except IPC (connect-src: ipc: http://ipc.localhost). Images come from the custompbimg:scheme. -
Worker threads/pools inside Rust (rayon,
spawn_blocking, therenderssemaphore that bounds concurrent thumbnail renders to the number of cores,AppState.renders).
The WebView and Rust communicate through exactly three channels:
| Channel | Direction | Mechanism | Page |
|---|---|---|---|
| Commands | WebView to Rust, request/response |
invoke wrapped by call()
|
IPC and Error Handling |
| Events | Rust to WebView, fire-and-forget |
app.emit("<job>://progress" | "<job>://done", payload) listened by on()
|
IPC and Error Handling |
| Image protocol | WebView to Rust, GET |
pbimg:// (http://pbimg.localhost/ on Windows), served by protocol.rs
|
Rust App Layer |
sequenceDiagram
actor U as User
participant P as Decklist panel (acciones.ts)
participant IPC as ipc.ts call/on
participant R as Rust commands
participant SC as Scryfall client + BlobStore
participant DS as deck-store
participant PS as plan-store
participant PC as proxy-core
participant PV as Preview (slot-view.tsx)
participant PR as pbimg protocol
U->>P: pastes list, presses Load
P->>IPC: call parse_decklist {text}
IPC->>R: invoke
R->>PC: decklist::parse
PC-->>P: ParsedDecklist
P->>IPC: lanzarTrabajo(resolve://progress, resolve://done)
IPC->>R: call resolve_decklist {list, options}
R-->>P: jobId (immediately)
R->>SC: /cards/collection batches, language, tokens
SC->>SC: images to blobs/ (SHA-256)
R-->>P: event resolve://progress {jobId, progress}
R-->>P: event resolve://done {jobId, result, error}
P->>DS: aplicar(cards) (history entry, version++)
DS->>PS: subscribe fires (100 ms debounce)
PS->>IPC: call compute_page_plan {deck}
IPC->>R: invoke
R->>PC: layout::plan(deck)
PC-->>PS: PagePlan (mm)
PS-->>PV: pages and slots
PV->>PV: useVisible(page) true
PV->>PR: GET pbimg://localhost/r/{blob}/{params}/{width}
PR->>PC: imaging::pipeline::render (cached in app_cache_dir/renders)
PR-->>PV: JPEG
DS->>IPC: call deck_save (800 ms after last change)
U->>P: Export PDF, picks a path (dialog plugin)
P->>IPC: lanzarTrabajo(export://progress, export://done)
IPC->>R: call export_pdf {deck snapshot, path}
R-->>P: jobId
R->>PC: layout::plan, render every distinct image (rayon), pdf::compose
R-->>P: export://progress {phase: prepare / compose / write}
R-->>P: export://done {jobId, result, error}
Key points visible in the diagram:
-
resolve_decklistandexport_pdfreturn ajobIdat once and finish by event; the front end listens before launching (src/lib/trabajos.ts→lanzarTrabajo) so a fast job cannot finish unseen. - The export sends the deck snapshot from the store, not the one in SQLite, because autosave may lag by up to 800 ms (
src/features/export/acciones.ts→exportarA). - The preview and the PDF run the same
render(); the preview asks for a width fromANCHOS = [146, 244, 488, 672, 745, 1000], the PDF asks for the DPI of the chosen quality.
Deeper pages: Decklist Parsing, Scryfall Integration, Layout Engine, Live Preview, Imaging and PDF, Export.
The app asks Tauri for directories in lib.rs setup and stores them in state::Paths { data, cache }; the log directory comes from app_log_dir().
| Location | Content | Source | Deletable without data loss? |
|---|---|---|---|
app_data_dir/library.db |
SQLite (WAL): decks, deck_cards, blobs, blob_refs, presets, app_settings, scryfall_cards, tags, deck_tags; migrations 0001–0004 in src-tauri/src/store/migrations/
|
state.rs → AppState::open, store/db.rs → Db
|
No |
app_data_dir/blobs/<2 hex>/<sha256>.<ext> |
Every image used by a deck (Scryfall PNGs, uploads, generated, designed) |
store/blobs.rs → BlobStore
|
No |
app_data_dir/fuentes/, marcos/, simbolos/
|
Designer resources: imported fonts, frame packs (including downloaded packs and the "Original" frames), mana symbols |
diseno/recursos.rs → Recursos
|
Re-downloadable, but user imports are lost |
app_data_dir/letras-cjk/ |
Noto Sans JP/KR/SC/TC (<code>.ttf) and their OFL licence, downloaded the first time a deck uses Japanese, Korean or Chinese as design language |
diseno/letras_cjk.rs, read by diseno/recursos.rs
|
Yes (re-downloaded and SHA-256 checked) |
app_data_dir/modelos/ |
OCR models, downloaded the first time OCR is used | ocr.rs |
Yes (re-downloaded) |
app_data_dir/temas/ |
Installed tweakcn themes (<id>.json) and their fonts (fuentes/<hash>.woff2) |
temas.rs |
Themes uninstalled |
app_cache_dir/renders/ |
Thumbnail JPEGs keyed by RenderSpec::key(); t/ search-result thumbnails; b/ reduced artwork |
render.rs, protocol.rs, diseno/vista.rs
|
Yes (cache_clear) |
app_cache_dir/scryfall-card-names.json, scryfall-set-codes.json
|
Scryfall name and set catalogues, renewed weekly | scryfall/nombres.rs |
Yes |
app_cache_dir/cockatrice-tokens.xml; app_cache_dir/mtgjson/nombres.sqlite
|
Token catalogue and the multi-language name index |
fuentes/cockatrice.rs, fuentes/mtgjson.rs
|
Yes |
app_log_dir/proxy-builder.log |
One line per Scryfall request (method, host, path, status) |
registro.rs, scryfall/client.rs
|
Yes |
Browser storage (localStorage) |
UI language key proxybuilder.lang, other per-viewer conveniences |
src/components/language-provider.tsx |
Yes |
Note that the Scryfall card data cache is not in app_cache_dir: it is the scryfall_cards table inside library.db (scryfall/cache.rs, TTL one week), so statistics and the library work offline. Because cache_clear only empties renders/, that table survives it.
See Storage and Blobs for tables and garbage collection.
Versions are the declared ranges in the manifests; the resolved versions in src-tauri/Cargo.lock are given where they matter.
| Area | Technology | Declared | Resolved / note |
|---|---|---|---|
| Shell | Tauri | tauri = "2" |
2.12.3 |
| Plugins |
tauri-plugin-dialog, -opener, -updater, -window-state
|
"2" |
2.8.2, 2.7.1, 2.14.0, 2.5.0 (window-state desktop only) |
| Types | ts-rs |
"12" |
12.0.1 |
| Database |
rusqlite with bundled SQLite |
"0.40" |
0.40.2 |
| HTTP |
reqwest (rustls, no OpenSSL; CI asserts cargo tree -i openssl is empty) |
"0.13" |
0.13.5 |
| Async |
tokio (rt, sync, time, macros) |
"1" |
1.53.2 |
| Parallelism | rayon |
"1.12" |
|
krilla (raster-images, simple-text) |
"0.8.2" |
0.8.2 | |
| Images |
image, fast_image_resize (Lanczos3, SIMD) |
0.25.10, 6.1.0
|
|
| Card rendering |
parley (text), tiny-skia, resvg, skrifa
|
0.12, 0.12, 0.48, 0.44
|
0.12.0, 0.12.0, 0.48.1 |
| OCR |
ocrs + rten
|
0.13, 0.26
|
0.13.1, 0.26.0 |
| Packages |
zip (deflate only) for .pbdeck
|
"9" |
|
| Logging |
tracing, tracing-subscriber (fmt) |
0.1, 0.3
|
|
| Rust edition / MSRV | 2021, rust-version = "1.92"
|
||
| Front end | React, React DOM | ^19.1.0 |
|
| State | Zustand | ^5.0.15 |
|
| UI kit | Radix UI (radix-ui ^1.5.0), lucide-react ^1.17.0, sonner ^2.0.7, react-resizable-panels ^4.11.2, @dnd-kit/core ^6.3.1
|
||
| Styling | Tailwind CSS 4 (@tailwindcss/vite), tw-animate-css
|
^4.3.0 |
|
| Build | Vite ^7.3.6, TypeScript ~5.8.3
|
||
| Tests / lint | Vitest ^5.0.1 (happy-dom), Biome 2.4.13, knip ^6.34.0
|
||
| Package manager | pnpm 12.14.0 (packageManager), minimumReleaseAge: 1440 in pnpm-workspace.yaml
|
||
| Node | >=22 |
| Concern | Page |
|---|---|
| Bridge, commands, errors, bindings, jobs | IPC and Error Handling |
lib.rs, AppState, pbimg://, render.rs, capabilities, window, CSP |
Rust App Layer |
Deck, DeckCard, PrintSettings and friends |
Deck Model |
| SQLite, blob store, GC | Storage and Blobs |
.pbdeck files |
Deck Package Format |
| Parsing the pasted list | Decklist Parsing |
| Scryfall client, cache, resolve | Scryfall Integration |
Cockatrice, MTGJSON, MTG.io, Externo
|
External Sources |
| OCR and linking own cards | Card Identification and OCR |
PagePlan |
Layout Engine |
| Image pipeline and PDF | Imaging and PDF |
| Export button, presets, calibration | Export |
Preview panes and pbimg:// URL building |
Live Preview |
| Statistics dialog | Statistics |
| Card designer | Designer Overview, Designer Engine, Designer Composition, Frame Packs, Designer App Layer, Designer UI |
| Front end structure, stores, i18n, themes, settings | Frontend Architecture, State Stores, Internationalization, Themes and Skins, Settings and Shell |
| Feature screens | Decklist Panel, Add Card and Variants, Set Browser, Deck Library, Card Editor Dialog |
| Dev setup, CI, releases, conventions | Development Setup, Testing and CI, Releases and Auto Updates, Conventions |
-
A new backend capability: put the pure part in
proxy-core(with its own error enum), the I/O part in the app crate, addcommands/<area>.rs, register it inlib.rs'sgenerate_handler!, add its signature toCommandsinsrc/lib/ipc.ts, runpnpm bindings. Checklist in IPC and Error Handling. -
A new long job: allocate with
state.jobs.alta(), holdstate.trabajo()for its duration, emit<name>://progressand<name>://done, add both toEventsinipc.ts, launch from the screen withlanzarTrabajo. -
A new image URL shape: extend
Rutaandleer_rutainprotocol.rs(with a rejection test) and add a builder insrc/lib/image-url.ts.
Use and build · Uso y desarrollo
- User-Guide · Guía de usuario
- Development-Setup · Preparación del entorno de desarrollo
- Testing-and-CI · Tests y CI
- Releases-and-Auto-Updates · Versiones y actualizaciones automáticas
- Conventions · Convenciones
Architecture · Arquitectura
- Architecture · Arquitectura
- IPC-and-Error-Handling · IPC y manejo de errores
- Rust-App-Layer · Capa de aplicación Rust
Data · Datos
- Deck-Model · Modelo del mazo
- Storage-and-Blobs · Almacenamiento y blobs
- Deck-Package-Format · Formato del paquete de mazo
- Deck-Library · Biblioteca de mazos
Getting cards · Conseguir cartas
- Decklist-Parsing · Parseo de la lista del mazo
- Scryfall-Integration · Integración con Scryfall
- External-Sources · Fuentes externas
- Card-Identification-and-OCR · Identificación de cartas y OCR
Layout, images, PDF · Maquetación, imagen y PDF
- Layout-Engine · Motor de maquetación
- Imaging-and-PDF · Imagen y PDF
- Export · Exportación
- Live-Preview · Vista previa en vivo
- Statistics · Estadísticas
Card designer · Diseñador de cartas
- Designer-Overview · Visión general del diseñador
- Designer-Engine · Motor del diseñador
- Designer-Composition · Composición del diseñador
- Frame-Packs · Packs de marcos
- Designer-App-Layer · Capa de aplicación del diseñador
- Designer-UI · Interfaz del diseñador
- Card-Editor-Dialog · Diálogo Editar carta
Game table · Mesa de juego
Frontend · Frontend
- Frontend-Architecture · Arquitectura del frontend
- State-Stores · Stores de estado
- Internationalization · Internacionalización
- Themes-and-Skins · Temas y skins
- Settings-and-Shell · Ajustes y shell
- Tutorials · Tutoriales
- Decklist-Panel · Panel de la lista del mazo
- Add-Card-and-Variants · Añadir carta y variantes
- Set-Browser · Explorador de sets
Reference · Referencia