Skip to content

Storage and Blobs

Pablo Martínez López edited this page Nov 10, 2026 · 20 revisions

Storage and Blobs

English · Español

Everything the app persists locally is split in two places: app_data_dir (the SQLite library library.db, the content-addressed image store blobs/, downloaded resources, the game in progress and the online identity) and app_cache_dir (derived files that can be deleted at any time). Decks depend on app_data_dir; nothing depends on app_cache_dir.

Code: src-tauri/src/store/ (db.rs, decks.rs, blobs.rs, etiquetas.rs, colecciones.rs, presets.rs, settings.rs, listas.rs, partidas.rs, migrations/*.sql), src-tauri/src/mesa/guardada.rs (the game in progress), src-tauri/src/state.rs (Paths, AppState::open), src-tauri/src/render.rs (render cache) and src/stores/deck-store.ts (the screen side).

Directory ownership rule

Decision: deck images live in the blob store under app_data_dir/blobs/, not in the cache. Why: decks reference images by SHA-256, so the images are user data; the operating system or the user may clear a cache directory whenever they like, and that must never break a deck. The cache (app_cache_dir/renders/) only holds derived renders (thumbnails, design previews) that are recomputed from blobs on demand.

AppState::open(paths) (state.rs) creates both directories, opens library.db, runs store::decks::rellenar_refs once, builds BlobStore::new(data/blobs) and Recursos::abrir(data), and loads (or creates) the online identity key with red::identidad::cargar_o_crear(data). Paths { data, cache } is filled in src-tauri/src/lib.rs from Tauri's app_data_dir() / app_cache_dir(), through red::perfil::carpeta (with a debug profile, perfiles/<name>/ inside each).

SQLite: library.db

Db (store/db.rs) wraps a single rusqlite::Connection in a Mutex (rusqlite is not Sync; long jobs release the lock between steps). On open it sets journal_mode=WAL, foreign_keys=ON, synchronous=NORMAL, then runs migrations. Db::in_memory() is used by tests.

Migrations

Migrations are include_str! SQL files listed in order in MIGRACIONES (store/db.rs) and tracked in schema_migrations (version, applied_at) (created with CREATE TABLE IF NOT EXISTS before the loop). Each runs in its own transaction, together with its schema_migrations row. Rule: migrations only ever append; a published one is never edited.

# File Adds
1 0001_init.sql decks, deck_cards (+ indexes deck_cards_orden, deck_cards_front, deck_cards_back), blobs, presets (+ presets_kind), app_settings, scryfall_cards (+ scryfall_nombre, scryfall_impresion).
2 0002_blob_refs.sql blob_refs (owner_kind, owner_id, blob) + blob_refs_blob: image references that are neither a card front nor back (designer art, emblems, "before" images).
3 0003_biblioteca.sql Library organisation: columns decks.pinned, decks.notes, decks.opened_at, decks.cover_card_id; tables tags (+ unique tags_name on name COLLATE NOCASE) and deck_tags (+ deck_tags_tag).
4 0004_idioma_deck.sql Column decks.lang_override (deck language chosen by hand; NULL means "computed from the cards").
5 0005_listas_juego.sql Table deck_listas (deck_id, id, nombre, comandantes, cartas, updated_at), primary key (deck_id, id), deck_id referencing decks(id) ON DELETE CASCADE: the game table's game lists (Game Table).
6 0006_partidas.sql Tables partidas (one row per finished game: start and end times, format and mode as JSON values, players and winners as JSON, how it ended, a note, rounds, duration, catalogue version and registro, the compressed record or NULL) and partida_decks (partida_id, jugador, deck_id, puesto), primary key (partida_id, jugador), partida_id referencing partidas(id) ON DELETE CASCADE; indexes partidas_terminada and partida_decks_deck: the game history (Game Table).
7 0007_icono_etiqueta.sql Column tags.icon (TEXT, NULL = no icon): the Iconify id of a library tag's icon (game-icons:crown), see Deck Library and Icons.
8 0008_partida_imagenes.sql Table partida_imagenes (partida_id, sha), primary key (partida_id, sha), partida_id referencing partidas(id) ON DELETE CASCADE, index partida_imagenes_sha: the own images (by SHA-256) a history replay paints, whose copies live in app_data_dir/partidas/imagenes/ (see Game Table).

| 9 | 0009_linaje.sql | Table deck_linaje (deck_id, linaje, actualizado), deck_id the primary key and referencing decks(id) ON DELETE CASCADE, index deck_linaje_linaje; the migration also gives every existing deck its own id as lineage and its updated_at as time: which decks on different computers are «the same deck» (Deck Sharing). | | 10 | 0010_amigos.sql | Tables amigos_mensajes (the decrypted conversation with each friend: amigo = their identity in hex, mio, tipo texto/invitacion, texto, enviado = sender's clock, orden = this device's clock, estado, id_cliente, id_servidor; unique indexes on id_cliente and on id_servidor of received ones, so a resend or a lost ack never duplicates) and amigos_claves (per friend: the encryption key last seen, verificado, cambio, leido_hasta). The server only ever held these messages encrypted (Friends and Messages). |

Test: abrir_dos_veces_aplica_la_migracion_una_sola_vez (db.rs) also runs PRAGMA integrity_check.

Tables

erDiagram
    decks ||--o{ deck_cards : "ON DELETE CASCADE"
    decks ||--o{ deck_tags : "ON DELETE CASCADE"
    tags ||--o{ deck_tags : "ON DELETE CASCADE"
    decks ||--o{ deck_listas : "ON DELETE CASCADE"
    decks ||--o| deck_linaje : "ON DELETE CASCADE"
    decks }o--o{ blobs : "default_back_blob"
    deck_cards }o--o{ blobs : "front_blob / back_blob"
    blob_refs }o--|| blobs : "blob"
    decks ||--o{ blob_refs : "owner_kind=deck"
    presets ||--o{ blob_refs : "owner_kind=preset"
    decks ||--o{ blob_refs : "owner_kind=mesa (game table mat)"
    partidas ||--o{ partida_decks : "ON DELETE CASCADE"
    partidas ||--o{ partida_imagenes : "ON DELETE CASCADE"
    decks {
        text id PK
        text name
        text decklist_text
        text settings_json
        text default_back_json
        text default_back_blob
        int schema_version
        int created_at
        int updated_at
        int pinned
        text notes
        int opened_at
        text cover_card_id
        text lang_override
    }
    deck_cards {
        text id
        text deck_id FK
        int position
        text front_blob
        text back_blob
        text card_json
    }
    blobs {
        text hash PK
        text ext
        text mime
        int width
        int height
        int bytes
        text origin
        int created_at
    }
    blob_refs {
        text owner_kind
        text owner_id
        text blob
    }
    presets {
        text id PK
        text kind
        text name
        text json
        int updated_at
    }
    tags {
        text id PK
        text name
        text color
        text icon
        int created_at
    }
    deck_tags {
        text deck_id
        text tag_id
    }
    deck_listas {
        text deck_id
        text id
        text nombre
        text comandantes
        text cartas
        int updated_at
    }
    app_settings {
        text key PK
        text value
    }
    scryfall_cards {
        text id PK
        text oracle_id
        text name
        text lang
        text set_code
        text collector_number
        text json
        int fetched_at
    }
    partidas {
        text id PK
        int empezada
        int terminada
        text formato
        text modo
        text jugadores
        text ganadores
        text como
        text nota
        int rondas
        int duracion_ms
        int version_catalogo
        blob registro
    }
    partida_decks {
        text partida_id FK
        int jugador
        text deck_id
        int puesto
    }
    deck_linaje {
        text deck_id PK
        text linaje
        int actualizado
    }
    partida_imagenes {
        text partida_id FK
        text sha
    }
Loading

Notes on the tables:

  • deck_cards primary key is (deck_id, id); front_blob / back_blob are denormalised copies of the card's image hashes so the garbage collector and the library can find used images without parsing card_json. The full DeckCard is card_json. A duplicate card id inside one deck becomes InvalidInput with motivo = duplicate_card_id.
  • decks.default_back_json is the serialized FaceImage; default_back_blob is its hash (for the same reason).
  • presets.kind is one of frontOffsets, backOffsets, print (store/presets.rs, saved by name: same kind and name replaces) or collection (store/colecciones.rs, saved by id so a rename does not create a second collection; a collection that no longer deserializes, for example from a future version, is skipped by list).
  • scryfall_cards is a metadata cache (TTL one week, scryfall/cache.rs; prices refresh after 24 h). Note that despite being a cache it lives inside library.db in app_data_dir, because statistics and the library work offline from it (commands/stats.rs, biblioteca.rs).
  • deck_listas (store/listas.rs) keeps the game lists of the game table: comandantes and cartas are JSON arrays of copy ids of the deck (DeckCard.id). They live apart from the deck, like tags, because decks::save rewrites the deck on every autosave and would overwrite them; ids no longer in the deck are only warned about when the list is checked. guardar upserts by (deck_id, id) (a new id when empty) with the name trimmed and capped at 60 characters.
  • deck_linaje (store/linaje.rs) says which decks are the same deck on different computers. linaje is an id shared by every copy: a new deck takes its own id (decks::save inserts the row), a duplicate gets another one, and one imported or received keeps the one it came with. actualizado is the time (ms) of the version's last save, on the computer where it was made: decks::save and decks::rename refresh it, and it travels in the .pbdeck (Biblioteca.linaje / actualizado). A deck with no row (from before the migration, or inserted by hand in a test) counts as having its own id as lineage (linaje::de, linaje::decks_de). It lives apart from the deck like tags and game lists, and writing it does not change updated_at.
  • partidas and partida_decks (store/partidas.rs) are the history of the game table. registro is the PartidaGuardada of the game (the same gzip JSON as the saved game, mesa/guardada.rs) or NULL when whoever saved it never had the whole game (a guest online). partida_decks has no foreign key to decks on purpose: deleting a deck keeps its games, with the deck name written inside the players' JSON. Saving a game with an id that exists replaces it (and its partida_decks); playing never touches decks.updated_at. Each player in the JSON also carries the play list used (listaId, lista, cartasLista), written from deck_listas when the game is saved; rows saved before that read without them.
  • partida_imagenes (store/partidas.rs) lists, per game with a replay, the hashes of the own images it paints (NuevaPartida.imagenes). It is not a blob_refs owner: these images are not blobs but table images (sanitised JPEGs, other players' included), copied out of the cache to app_data_dir/partidas/imagenes/. partidas::borrar deletes the game in one transaction and returns the hashes that no other row still uses, so the command can delete those copies; a hash shared by two games stays until the last one goes.
  • tags.color is one of COLORES (gray red orange amber green teal sky blue violet pink); an unknown colour falls back to gray; tag names are trimmed, capped at 40 characters, unique case-insensitively, and create returns the existing tag if the name already exists. tags.icon is set apart with etiquetas::icono (tag_icon_set), which only accepts a valid Iconify id (iconos::id_valido) or NULL; update keeps it.
  • Dates are milliseconds since 1970 (store::ahora_ms).

What touches updated_at

decks::save and decks::rename set updated_at. Pin, notes (max 4000 chars), cover card, language override, tags, game lists and "opened at" deliberately do not (etiquetas.rs, decks::marcar_abierto), so organising the library does not reorder it by modification date. Test: favorito_notas_y_portada_no_cambian_la_fecha.

Key operations (store/decks.rs)

Function Behaviour
create(db, name, default_back) New uuid, default PrintSettings, DECK_SCHEMA_VERSION, then save. The command layer (commands/library.rs crear_deck) passes the generic back.
open(db, id) Reads the deck row and all card_json ordered by position; NotFound if missing, Db if JSON no longer deserializes. deck_open also calls marcar_abierto; deck_peek does not.
save(db, deck) One transaction. First validates that every referenced image exists in blobs (fronts, backs, default back, and designer images from refs_del_deck); a miss is InvalidInput, motivo = blob_missing. Then upserts the decks row (keeps created_at and the library columns), deletes and reinserts all deck_cards in order, and rewrites the deck's blob_refs. Returns the save time.
list(db) DeckSummary per deck for the library: card count, cover (chosen card if still present with an image, otherwise the first card with an image), tags. Ordered by updated_at DESC.
duplicate(db, id, name) Opens, assigns new uuids to the deck and every card, saves, then copies notes, cover (remapped to the new card id), language override and tags (not the favourite). Images are shared, not copied.
delete(db, id) Deletes the deck (cascade removes cards, tags and game lists), its blob_refs (owner_kind = 'deck'), its game table mat reference (owner_kind = 'mesa') and its mat setting (mesa.tapete.<id>). Image files are not removed; garbage collection does that later.
rellenar_refs(db) One-time backfill of blob_refs from existing decks, guarded by the app_settings key blob_refs_v1 (an empty table is not a valid marker because a deck without designs leaves it empty).

Tests: guardar_y_reabrir_135_cartas_da_lo_mismo, no_se_guarda_una_carta_con_una_imagen_que_no_existe, dos_cartas_con_el_mismo_id_es_un_error_claro, duplicar_renombrar_y_borrar, la_limpieza_respeta_el_arte_de_los_disenos, el_relleno_de_referencias_se_hace_una_vez.

The blob store

BlobStore (store/blobs.rs) is a content-addressed image store.

  • Layout: app_data_dir/blobs/<first two hex chars>/<sha256>.<ext> (BlobStore::path_of). ext is jpg, png or webp (proxy_core::imaging::probe::Kind).
  • Identity is the SHA-256 of the file bytes, lowercase hex, 64 chars (es_huella validates this and is reused to validate paths coming from packages and the image protocol). Two decks with the same card share one file.
  • Metadata row in blobs: ext, mime, width, height, bytes, origin, created_at.

API

Function Behaviour
put_bytes(db, bytes, origin) -> FaceImage Probes the format (ErrorCode::Unsupported if it is not a supported image; corrupt gives ErrorCode::Image with motivo = corrupt), rejects any side above LADO_MAXIMO = 12000 px (Image, motivo = too_large), hashes, writes atomically if absent, INSERT OR IGNOREs the row. If the blob already existed, the returned origin is the one it first entered with.
import_files(db, paths) Reads user files and put_bytes them as Upload. It copies, never links, so deleting the original cannot break a deck.
generic_back(db) Inserts the bundled generic back (assets/reverso-generico.png, 745x1040, origin Generated) and records its hash in app_settings under generic_back_blob.
locate(db, hash) Path and Kind for a known blob (looks up ext in blobs); None for malformed hashes.
gc(db, ocupado) See below.
escribir_atomico(dest, bytes) Writes to a .part temp in the same folder (its name is unique per write: process id and a counter, because the table preparation writes in parallel and two copies of the same card, or two decks sharing it, write the same destination at once; with a fixed name one renamed the other's temp and failed with «no such file», shown as «cannot write to disk»; test escribir_a_la_vez_el_mismo_destino_no_falla), sync_all, then renames; a crash never leaves a half file under the final name. Reused for most downloads in the app.

Garbage collection

BlobStore::gc deletes blobs that nothing uses and that are older than 24 hours (GRACIA_MS). A blob is kept if any of these holds:

  1. deck_cards.front_blob or back_blob equals its hash.
  2. decks.default_back_blob equals it.
  3. A row in blob_refs points to it (designer art, designer emblem, "before" images, collection emblems, game table mats, the dice image, the online avatar).
  4. It is the generic back (app_settings.generic_back_blob).
  5. It was created less than 24 hours ago.

Why the grace period: an image may belong to a download whose deck has not been saved yet. Why ocupado: gc returns ErrorCode::Busy while a resolve/export job is running (AppState::ocupado()), because the job may hold hashes that no deck row references yet. ocupado() is also true while a game is open on the table (Game Table). The only caller is blob_gc (commands/library.rs), triggered from the «Liberar espacio» dialog (src/features/library/liberar-dialog.tsx); there is no automatic GC. The file is removed first (a missing file is fine), then the blobs row.

blob_refs owners: owner_kind = 'deck' (rewritten on every decks::save from CardDesign::blobs()), 'preset' (a collection's emblem, written in colecciones::save, removed in colecciones::delete) and 'mesa' (the game table mat: owner_id is the deck id or general; written and removed by mesa/tapete.rs::guardar, and removed with its deck in decks::delete; with owner_id = 'dados', the image of the player's dice, written by mesa/dados.rs::guardar) and 'red' (owner_id = 'avatar', the online avatar, written and removed by red/identidad.rs::guardar). The mat cannot use 'deck' because decks::save rewrites a deck's 'deck' references on every save and would drop it. Test: la_limpieza_solo_borra_lo_huerfano_y_viejo.

Gotcha: any new place that stores a blob hash must be reflected in one of the four protections above, or GC will eventually delete the image and the deck will fail to save or print (save refuses to store a reference to a missing blob, so you will usually notice at the next save).

Optimizing images in place (optimizar.rs)

«Liberar espacio» can also convert the images of chosen decks, with the rules of Deck Package Format, and delete the originals from this device. It cannot be undone, so the order is what protects the data:

  1. Every JPEG is made and stored first (optimizar). No deck has changed yet: cancelling or a failure here leaves the library as it was, and what is left over is collected by gc.
  2. Each deck is rewritten in one transaction (decks::save_imagenes), which keeps updated_at and the lineage time: the content is the same, and otherwise another device would see a newer version.
  3. BlobStore::borrar_sin_usar deletes an original only if nothing uses it (usos: another deck, a collection, the table, the generic back), without gc's 24 h grace because these are known to have just stopped being needed.

estimar counts only what really frees up: an original another deck still uses stays, and its JPEG takes space too. An image where it does not pay off (transparency, flat colours) is recorded in optimizar.descartadas and not offered again, because finding out means reading it. It refuses with Busy during a job or a game. The screen does not allow it from the Design view (a card being edited holds hashes of its own), and an open deck goes through prestarDeck with conservar: false, since undo would point at deleted images. Saved games are unaffected: the table uses its own images (mesa/imagenes.rs).

Render cache (app_cache_dir/renders/)

AppState::carpeta_renders() (render.rs) is the only cache directory managed by the app UI. Contents, all derived and regenerated on demand:

Path under renders/ Content Written by
<key>.jpg Processed preview thumbnails (key from RenderSpec::key()) AppState::miniatura (render.rs)
t/<hash>.jpg Search-result thumbnails fetched from Scryfall or other sources protocol.rs
d/<hash>-<width>.png Designer card previews diseno/vista.rs imagen
c/<hash>-<width>.png, c/<hash>.json Designer card "layer without art" and the art-slot info diseno/vista.rs capa
b/<blob>-<side>.jpg or .png Downscaled art for live dragging diseno/vista.rs arte_reducido
m/<sha256 or s-<id>-<face>>-<width>.jpg Game table images reduced to the width the screen asks for mesa/imagenes.rs servir

PDF rendering has no disk cache ("it weighs too much", render_para_pdf). The commands cache_stats / cache_clear (commands/library.rs) measure and remove_dir_all exactly this folder; they are exposed in the decklist panel (src/features/decklist/decklist-panel.tsx) with a confirmation text. Test: vaciar_la_cache_no_toca_las_imagenes_de_los_decks.

app_settings (key/value in SQLite)

Accessed through store/settings.rs (get, set, upsert) and, from the screen, the generic commands app_setting_get / app_setting_set (src/lib/ipc.ts). Keys seen in the code:

Key Written by Meaning
last_deck_id src/stores/deck-store.ts Deck opened at start-up.
generic_back_blob BlobStore::generic_back Hash of the generic back; protects it from GC.
blob_refs_v1 decks::rellenar_refs Marks that blob_refs was backfilled.
carpeta_importar, carpeta_exportar commands/carpetas.rs Last folder used by import / export file dialogs (kept separately).
mesa.tapete.<deck id>, mesa.tapete mesa/tapete.rs The game table mat of a deck, or the general one: JSON { blob, velo, desenfoque }. The deck key is removed in decks::delete.
mesa.dados mesa/dados.rs The look of the player's dice and coin (AspectoDados, JSON).
red.apodo, red.avatar red/identidad.rs The device's name when passing decks (empty: the system's, pasar::nombre_equipo) and the avatar blob hash.
amigos.activo, amigos.cursor, amigos.hola, amigos.codigo amigos/ Friends: whether the person accepted using the server ("1"), the sync cursor, the fingerprint of the last hello and this device's friend code. The last three are emptied on leaving or importing another identity (Friends and Messages).

Other UI preferences (theme, skin, language, panel sizes, library view) are not here; they are localStorage keys with the prefix proxybuilder. (for example proxybuilder.lang, proxybuilder.theme, proxybuilder.preferencias, proxybuilder.biblioteca), see Settings-and-Shell and Themes-and-Skins.

Per-deck autosave and undo (src/stores/deck-store.ts)

The open deck lives in the zustand store useDeck (deck, guardado, errorGuardado, version, pasado, futuro). See also State-Stores.

  • Single entry point. Every user change goes through aplicar(cambio): it pushes a snapshot (cards, settings, defaultBack; not the name or list text) onto pasado (capped at MAX_HISTORIAL = 100), clears futuro, sets guardado = "dirty" and bumps version. setCartas and setAjustes are wrappers. No screen calls deck_save itself.
  • Changes that are not undoable. setSinHistorial (name, decklistText) marks dirty without history. refrescarCartas(antes, nuevas) replaces cards (for example regenerated design thumbnails) without history and only for cards that are still the same object as in antes, so a concurrent user edit wins.
  • Undo/redo. deshacer / rehacer swap snapshots between pasado and futuro and mark dirty (so undo is also saved). cargar(deck) resets history.
  • Autosave. iniciarAutoguardado(retardoMs = 800) (started in src/features/shell/use-deck-inicial.ts) subscribes to version changes while dirty and debounces an 800 ms timer. One save in flight at a time: if another change arrives mid-save, it chains another save afterwards with the latest deck. After a save, the state is "saved" only if version did not move meanwhile, otherwise it stays "dirty". On failure the state is "error" (errorGuardado set); the next change re-arms the save.
  • Flushing. guardarAhora() saves immediately if not already saved; it is called before opening another deck, duplicating/exporting from the library and before an in-app update installs.
  • Start-up. abrirDeckInicial(nombre) reads last_deck_id, tries deck_open, and falls back to deck_create; then writes last_deck_id.
  • Rust side. deck_save -> decks::save rewrites the whole deck in one transaction (see above), so a save is atomic: either the whole new deck or the previous one.

Tests: src/stores/__tests__/deck-store.test.ts (five quick changes save once, undo x3 restores and redo replays, error then retry, no-history changes), deck-store-abrir.test.ts.

Data directory layout

Everything below is relative to the platform's app_data_dir or app_cache_dir (resolved by Tauri; the paths are not hard-coded). "Safe to delete" means no deck data is lost; the entry is recreated or re-downloaded when needed.

app_data_dir

Path Content Created by Safe to delete?
library.db (+ -wal, -shm) SQLite library: decks, cards, tags, presets (including collections), settings, scryfall_cards store/db.rs No. Deleting it deletes every deck. Quit the app first; copy the three files together.
blobs/<xx>/<sha256>.<ext> Deck images (content-addressed) store/blobs.rs No. Decks would reference missing images. Use the in-app "free unused images" instead.
modelos/ Two OCR models (text-detection.rten, text-recognition.rten, ~12 MB), downloaded on first OCR use ocr.rs Yes (re-downloaded when OCR is used again).
temas/<id>.json, temas/fuentes/<hash>.woff2 Installed tweakcn themes (converted to CSS) and their Google Fonts temas.rs Yes, but those themes disappear until reinstalled.
simbolos/ (indice.json + SVGs) Scryfall mana/set symbols, renewed when the index is older than 30 days diseno/recursos.rs Yes (re-downloaded).
fuentes/<sha256>.<ttf|otf>, fuentes/licencias/ Fonts imported by the user or from the free-fonts catalogue, with their licences diseno/recursos.rs, diseno/letras_libres.rs Only if you accept designs that use them falling back to another font. User-imported fonts are not recoverable.
marcos/<id>/ Frame packs: original (Card Conjurer frames, downloaded), downloadable packs such as Balatro, and packs imported by the user diseno/original.rs, diseno/descargables.rs, diseno/recursos.rs Downloaded ones: yes (re-downloaded and SHA-256 checked). User-imported packs: no backup elsewhere.
packs-extras/<id>/ Extra content of downloadable packs (backs, tokens, ready-made cards), kept apart from the pack diseno/descargables.rs Yes (re-downloaded).
partidas/actual.json.gz The game in progress on the table (PartidaGuardada): catalogue version, config, prepared decks, initial game, events, save time and the best-of score, as gzip JSON written atomically. Written when the game opens, every 50 events (CADA), after 20 s with unsaved events (CADA_MS) and when forced (mesa_guardar, which the screen calls when leaving the tab or closing the window; mesa_salir with guardar; mesa_siguiente_partida). Not written by a guest online (the host keeps the game) or during a replay. Deleted by mesa_salir without guardar (not when leaving a replay). Finished games go to the partidas table instead mesa/guardada.rs, commands/mesa.rs Yes, but the game in progress is lost. A file from a newer catalogue is refused with Unsupported (motivo: version).
partidas/imagenes/<xx>/<sha256>.jpg Copies of the own table images that the replays of the game history paint (table partida_imagenes), written after a finished game is saved; a missing cache image (app_cache_dir/mesa/) is restored from here mesa/imagenes.rs (guardar_para_historial, servir, olvidar_de_historial), store/partidas.rs Only together with the history games: without them those replays show empty cards. Each copy is deleted with the last game that uses it.
identidad/clave The installation's iroh key (32 bytes, ed25519, mode 0600, written atomically): the player's identity online and on the friends server (it signs its requests and derives the message encryption key, see Friends and Messages). Importing an identity keeps the previous key in identidad/clave.anterior. On Android it is sealed instead: identidad/clave.sellada holds the key encrypted with an AES-GCM key from the Android Keystore (Cofre, android::cofre), so a copy of the file is useless off the phone. A key found unsealed is sealed at startup, and the plain file is deleted only after checking that what was sealed opens and gives the same key; if the Keystore fails it stays as it was rather than losing the identity (friends depend on it). The desktop keeps the file with user-only permissions. red/identidad.rs Only if you accept being a new player (friends would have to add you again). Export it first (Settings > Game table) to keep it.
amigos/textos-avisos.json, amigos/avisados.json Android only: the notification templates the screen last sent (amigos_textos) and the ids of pending messages already notified, for the background check with the app closed amigos/fondo.rs Yes: the templates come back when the app opens; at worst a pending message is notified once more.
perfiles/<name>/ A whole separate data directory for a debug instance (PB_PERFIL; never in release) red/perfil.rs Yes.
letras-cjk/<code>.ttf, letras-cjk/Noto-CJK-OFL.txt Noto Sans JP/KR/SC/TC for designed cards in Japanese, Korean and Chinese (ja, ko, zhs, zht; about 10–18 MB each) and their OFL licence, downloaded the first time a deck uses one of those design languages diseno/letras_cjk.rs, diseno/recursos.rs (guardar_cjk, leer_cjk) Yes (re-downloaded and SHA-256 checked when the language is chosen again; meanwhile those cards show the missing-fonts warning).
iconos/elegidos.json The Iconify icons the user chose for something stored (a tag icon), with their SVG data, so they show offline iconos.rs Yes, but chosen icons need the network again to show.

app_cache_dir

Path Content Created by Safe to delete?
renders/ (+ t/, d/, c/, b/, m/) Derived thumbnails, designer previews and reduced game table images render.rs, protocol.rs, diseno/vista.rs, mesa/imagenes.rs Yes (button in the decklist panel).
mesa/ (<xx>/<sha256>.jpg, s/<id>-<face>.jpg, i/<key>.json) Game table card faces prepared at 745 px (ANCHO_MESA, JPEG 85), Scryfall prints, mats reduced to 1920 px wide (ANCHO_TAPETE, index key t-<blob>; the mat itself stays in blobs/), and the index from preparation key to image mesa/imagenes.rs, mesa/preparar.rs, mesa/tapete.rs Yes (prepared again when a game starts or is resumed; the images of history replays come back from partidas/imagenes/).
mesa/s/<scryfall id>-<face>.jpg Scryfall faces shown by «Originals in my language» (and the ones an online game downloads from Scryfall), as downloaded mesa/imagenes.rs, mesa/originales.rs Yes (downloaded again when shown).
mesa/r/<xx>/<sha256>.jpg In an online game, the bytes of other players' own images as received (the ones whose SHA-256 matches, to pass on); the sanitised copy shown on screen goes to mesa/<xx>/ red/imagenes.rs Yes (asked for again when shown).
scryfall-card-names.json, scryfall-set-codes.json Catalogue of Scryfall card names and set codes for local OCR matching, renewed weekly scryfall/nombres.rs Yes (re-downloaded when needed; needs network).
cockatrice-tokens.xml Cockatrice token database (only the tokens Scryfall lacks are used), renewed weekly fuentes/cockatrice.rs Yes.
pasar/<uuid>/deck.pbdeck The package being sent or received while an exchange runs (Deck Sharing); the folder is deleted when the exchange ends or is cancelled, and what is left is removed when «Recibir» opens or a send starts pasar/mod.rs (Temporal) Yes.
fichero/ On Android, copies of the content:// files the person picks (an import is read from a copy because a ZIP needs Seek) and the temporary files written before copying or sharing; a copy is deleted when released and anything older than 24 h is swept fichero.rs Yes.
actualizacion/ On Android, the APK the updater downloaded before handing it to the system installer; removed when the app is up to date actualizar.rs Yes.
iconos/ (colecciones.json, <prefix>.json) The Iconify catalogue for the icon picker (collection list and the names of each collection), renewed after 7 days iconos.rs Yes (re-downloaded when the picker opens).
mtgjson/nombres.sqlite Optional offline card names in other languages, built from MTGJSON fuentes/mtgjson.rs Yes (a Settings action also removes it).
perfiles/<name>/ (+ webview/) The cache of a debug instance (PB_PERFIL), with its own WebView data directory so instances do not share localStorage red/perfil.rs, lib.rs (crear_ventana) Yes.

Other locations

  • Log file proxy-builder.log in Tauri's app_log_dir (registro.rs): every network request is logged there so you can check that opening a saved deck downloads nothing.
  • Window geometry is kept by tauri-plugin-window-state (registered in lib.rs, with the visibility flag excluded). Inferred: it writes into the app config directory; not read in this repo.
  • The updater's downloaded package and signature handling are described in Releases-and-Auto-Updates.

Passing decks

Nothing about an exchange is stored apart from the decks it creates and their lineage (deck_linaje). The code, its secret and the nearby announcement live in memory for 15 minutes; the package in transit is in app_cache_dir/pasar/. The iroh key it uses is the installation's (identidad/clave), the same as online play.

Online play

  • app_settings: red.apodo (nickname) and red.avatar (a blob hash, protected by a blob_refs row of kind red/avatar). The look of the dice is mesa.dados (its image, mesa/dados).
  • The friends server keeps public keys, friendships and encrypted messages until they are acknowledged; the decrypted conversations live only in this database (amigos_mensajes, amigos_claves, migration 0010). See Friends and Messages.

Failure modes

Situation Result
Saving a deck whose card references an image not in blobs InvalidInput (motivo = blob_missing, blob = <hash>); the autosave state becomes "error".
Two cards with the same id in one deck InvalidInput (motivo = duplicate_card_id).
Unsupported or corrupt image bytes Unsupported / Image (motivo = corrupt).
Image larger than 12000 px on a side Image (motivo = too_large).
blob_gc while a job runs or a game is open Busy.
optimizar_lanzar while a job runs or a game is open Busy; nothing changes.
Stored JSON that no longer deserializes Db (with detalle).
Poisoned DB mutex Internal.

Error codes are defined in src-tauri/src/error.rs; see IPC-and-Error-Handling.

Where to start when extending

  • New persisted column: add 000N_*.sql, append it to MIGRACIONES, never edit old files. Remember the ALTER TABLE ... ADD COLUMN default must satisfy existing rows.
  • New data that references an image: store its hash in a protected place (see GC list) and add it to refs_del_deck / CardDesign::blobs().
  • New downloaded resource: choose app_data_dir if features depend on it staying, app_cache_dir if it is purely re-creatable; write with escribir_atomico.

See also

Documentation · Documentación

Language: English · Español

Use and build · Uso y desarrollo

Architecture · Arquitectura

Data · Datos

Getting cards · Conseguir cartas

Layout, images, PDF · Maquetación, imagen y PDF

Card designer · Diseñador de cartas

Game table · Mesa de juego

Mobile · Móvil

Frontend · Frontend

Reference · Referencia

Clone this wiki locally