Overview
Viridian Vault is an internal web platform for a small Pokémon TCG resale operation. It tracks inventory, keeps market prices synced via the PokeWallet API, and surfaces cost basis vs. market value so you can see unrealized profit at both the item and portfolio level.
The core engineering problem is identity: inventory is entered in human terms (name, set, number), but pricing requires a provider product ID. Viridian Vault solves that with a progressive multi-stage resolution cascade and an admin workflow for ambiguous matches.
Problem & Context
Manual price lookups don’t scale, and show-event workflows are time-sensitive. The owners needed a system that could:
- Resolve inventory items to provider IDs reliably
- Fetch prices without violating rate limits
- Cache and snapshot prices for trend tracking
- Stay internal-only with real access control
Constraints
- External API rate limits require cache-first design.
- Internal-only access (no self-registration) via allowlist.
- All writes go through server route handlers; no direct client mutations.
Approach & Design Decisions
- Persistent candidates: multi-match resolution results are stored in a
resolution_candidatestable so ambiguity survives navigation and supports async admin resolution. - Cache-first pricing: reads hit
price_current; refresh happens only when stale, in batches sized for provider limits. - RLS as a hard gate: forced row-level security with policies checking
allowed_users. - Incremental migrations: versioned SQL migrations document the schema and policy evolution.
Implementation Highlights
- Resolution cascade v2: 4-stage fallback (set_code+number → title+set_code+number → title+number → title-only), now with set_code first for higher hit rate on singles.
- Typed discriminated union outputs (resolved/candidates/not_found) for debuggability;
post_filter_countadded in AttemptLog. - Price cache TTL and snapshot system with reason tags (DAILY/MANUAL/SHOW/TRANSACTION_EVENT).
- Card image fetch as bytes → Supabase Storage for singles.
- Pipeline audit hardening (v1.6.0): atomic price writes, quota tracking, thundering-herd guard, image fetch hardening, TTL centralization, set-mapping TTL.
Results & Evaluation
Per workflow-core progress (last synced 2026-04-22):
- v1.0–v1.5 (through 2026-04-01): inventory CRUD, PokeWallet-centered pricing, RLS, dashboard, admin resolution — v1.5.0 completed the pipeline redesign and doc overhaul (universal PokeWallet provider strategy).
- v1.6.x (completed 2026-04-17): pipeline audit (7 tickets, A–L) — atomic price writes, ≤3 bulk DB calls per refresh cycle, quota tracking, thundering-herd guard, image fetch hardening, TTL centralization, and set-mapping TTL. PokeWallet resolution v2: 4-stage fallback restructured with
set_code+numberfirst, critical API corrections (json.results,X-API-Keyheader), pagination fallback, and card number normalization. - v1.9.0 Resolve Items hub (in progress, 2026-04-22): unifying the 4-step settings workflow (set mappings, singles resolve/repair, price refresh, images) into
/admin/resolve-items; spec + design + implementation plan written. - v2.0 Show mode (in progress): spec approved 2026-04-02; dual-mode search (PokeWallet for PURCHASE/TRADE IN, inventory DB for SALE/TRADE OUT), ~60s sale flow, offline queue v1; branch
feature/show-mode. - Spec’d: v1.7 sealed migration (JustTCG → PokeWallet), v1.8 background hourly sync (
POST /api/sync/hourly+ Supabase pg_cron / pg_net). - v2.1 Analytics and v2.2 Binder scanner are on the roadmap; binder MVP not started.
Operational baselines:
- Bulk resolution processes ~200 items in ~30–40 seconds (batched).
- Inventory reads remain cache-first; daily snapshot cron at 03:00 UTC.
TODO: production inventory scale and long-term resolution success rates (not published).
Tradeoffs & Limitations
- Internal-only; demo and repo links omitted.
- Provider resolution is probabilistic; the admin workflow is essential for long-tail edge cases.