Building CardCore - April 9, 2026 - Theory Boards, Trade QA, and 90 Commits in 48 Hours
90 commits. 48 hours. 30+ tickets. No sleep.
I'm going to be honest - I don't fully remember all of it. The last two days have been a blur of code, bugs, breakthroughs, and caffeine. When I pulled up the git log this morning to write this post, I had to scroll. And scroll. And keep scrolling. Ninety commits across two days. Thirty-plus tickets touched, tested, and shipped to production. An entire feature system built from nothing. A full QA session that ripped apart our trading platform and put it back together stronger.
This is what building in public actually looks like. Not the polished screenshots and launch announcements. The 2 AM debugging sessions. The moment where drag-and-drop works perfectly on desktop and then you pick up your phone and watch it completely fall apart. The gut punch of running a QA session on your own trading platform and finding nine bugs staring back at you.
Here's everything that happened.
Theory Boards - an entire collaborative tier list system in 48 hours
This was the centerpiece. Theory boards didn't exist two days ago. Not as a prototype, not as a stub, not as a "coming soon" placeholder. Zero code. Just a ticket (COREAPP-1008) and an idea - let players collaboratively rank cards into tiers, discuss placements, and share their analysis.
By the time we were done, we'd shipped 30 commits just on this one feature. Thirty. For context, some teams ship 30 commits in a month. We did it in two days for a single feature, and we were working on a dozen other things simultaneously.
The foundation - and the first wall
The initial build landed the core theory board structure - create a board, add cards to tiers, share a public link. Simple enough on paper. But the moment I opened it in a browser, the problems were obvious. Cards were tiny. The entire board was crammed inside our root layout's max-w-7xl container, wasting 40% of the screen on empty margins. On a 1440p monitor it looked like a postage stamp.
So we ripped it out. Broke the board free from the root layout constraints. Switched to CSS auto-fill grids that dynamically size cards to fill available space. Bumped card images by 60% on the shared view. Added 15% side margins so it didn't feel like cards were falling off the edge. Five commits just to make the thing look right at different screen sizes.
Then came drag-and-drop, and this is where the real pain started.
Drag-and-drop - the feature that fights back
We shipped sortable drag-and-drop reordering with auto-scroll, proper card sizing, touch support, and the ability to move cards between tiers. It sounds like a single feature. It was a war.
The first bug was a prefix matching issue in the getTierFromId function. When you have tier IDs like "s-tier" and "s-tier-2", a naive prefix check matches both. Cards were getting silently dropped into the wrong tier, and because the state update looked correct locally, you wouldn't notice until you refreshed the page and saw your carefully placed cards in completely wrong positions.
Then came the snap-back bug. You'd drag a card to a new tier, release it, see it land perfectly - and then watch it teleport back to where it started. The sync system was polling the server for updates and merging the response over local state. But the local drag operation hadn't round-tripped to the server yet, so the merge was treating your drag as "stale" and reverting it. Every single time.
The fix for this was a complete rebuild of the dirty tracking system. We moved from coarse "has anything changed" heuristics to per-card dirty flags. Each card tracks whether it has a pending local change that hasn't been confirmed by the server. The sync merge skips dirty cards. When the server confirms the write, the dirty flag clears. Sounds simple - took six commits to get right because of edge cases around rapid drags, cross-tier moves where the original tier reference was getting lost on re-render, and sync merges that would stomp on an active drag operation mid-flight.
Real-time collaboration - multiplayer tier lists
Once drag-and-drop was stable, we added collaborative real-time editing. Multiple people can now work on the same theory board simultaneously using polling-based sync. This opened up a whole new category of bugs we hadn't anticipated.
Saves were getting lost on rapid drags because the optimistic update and the server write were racing each other. Cross-tier moves weren't persisting because the originalTier property was getting garbage-collected during a React re-render cycle. The activity feed was duplicating events because the append-only event stream wasn't deduplicating on the client side. Cross-tier event labels were showing "? to A" instead of "S to A" because the event was being recorded after the state update removed the original tier reference.
Every one of these bugs was subtle. None of them would show up in a basic click-through test. They only appeared under real collaborative editing - two people dragging cards at the same time, rapid-fire reorganizations, someone moving a card across three tiers in quick succession. The kind of usage patterns that only emerge when people actually care about what they're building.
The full feature set
Beyond the core drag-and-collaboration foundation, we shipped:
- Per-card discussions with placement voting - click any card on a board and open a discussion thread. Other collaborators can vote on whether they agree with the placement. This turns a static tier list into a living debate.
- Activity feed with append-only event stream - every card add, move, removal, and tier change gets logged. You can see exactly who did what and when. The event stream is append-only for data integrity - we never overwrite history.
- Snapshots with comparison view - save the state of your board at any point, then compare two snapshots side by side. See exactly what changed between versions. Perfect for tracking how your tier list evolves as the meta shifts.
- Text export - export your board as clean text for pasting into Discord or Reddit. Automatically formats tiers with card names.
- Stats panel with meta alignment - a 3-column stats panel that shows how your tier rankings align with actual tournament meta data. If you put a card in S-tier and it's performing at a 55% win rate in real tournaments, you'll see that confirmation. If you have it in B-tier and it's actually dominating, you'll see the disconnect.
- Board image export - render your entire board as a shareable image for Discord and social media.
- Considerations tier - a staging area for cards you're still evaluating. Not committed to a tier yet, but worth tracking.
- Prominent Remove from Board action - because the original "remove" button was buried in a submenu and nobody could find it.
The hardening pass at the end caught activity duplication, collision detection issues in the event stream, and a productId string-vs-number type mismatch that was completely blanking the stats panel. JavaScript's type coercion strikes again - Map.get(540390) and Map.get("540390") are not the same thing, and PostgreSQL returns bigint as strings.
Visibility, sharing, and demo readiness
We also built out the full visibility and sharing system. Theory boards have proper public/private controls, shareable URLs, delete confirmation, and a fix for empty meta state messaging. The board was demo-ready by the end of day one - critical because we're heading into convention season and this is the kind of feature that makes people stop at the booth.
Trade platform QA - the brutal truth about shipping fast
On April 8th, we ran a dedicated QA session on the peer-to-peer trading system. I wish I could say it went well. It did not.
We found 9 distinct bugs. Nine. On a feature that's been live for weeks. Some of them were embarrassing. Some of them were the kind of thing that, if a player hit them during a real trade at a convention, would have destroyed trust in the platform.
Here's every single one:
-
COREAPP-1090: Completed trades don't update either player's collection - This is the big one. The entire point of the trading system is that when you complete a trade, the cards move from one collection to another. They weren't. Players were completing trades and then looking at their collection page and seeing... nothing changed. The trade showed as "completed" but the actual card data never synced.
-
COREAPP-1091: IOU terms not visible in trade detail view - If you agreed to IOU terms as part of a trade (common at conventions - "I'll bring the card next week"), those terms were invisible in the trade detail. The data was being saved but the UI wasn't rendering it.
-
COREAPP-1092: Shipping trade type has no tracking/address guidance - For trades that involve shipping cards, there was no UI guidance for entering tracking numbers or addresses. Players were completing shipping trades with no way to communicate logistics.
-
COREAPP-1093: QR code shows for all trade types - The QR verification code is meant for in-person verified trades only. It was showing up for every trade type - shipping, IOU, casual. Confusing and wrong.
-
COREAPP-1094: Willing-to-ship slider desync - Players can set a "willing to ship" preference on their profile. The slider in the profile view and the slider in edit mode were desynced - you'd toggle it in one place and it wouldn't reflect in the other.
-
COREAPP-1095: Want board missing hyperspace/prestige card variants - The "want" board (cards you're looking for in trades) was only showing base variants. If you wanted a Hyperspace or Prestige variant specifically, you couldn't express that in your want list.
-
COREAPP-1096: No quantity selector for multi-copy cards - If you own 4 copies of a card and want to trade 2, there was no way to specify quantity. You were trading all or nothing.
-
COREAPP-1097: IOU settlement notification not received - When an IOU is settled (the other player fulfills their end), neither player got a notification. You had to manually check the trade detail page.
-
COREAPP-1098: Trust score has no explanation - Every trader has a trust score, but there was zero UI explaining what it means, how it's calculated, or why your score is what it is.
All 9 bugs got filed as individual tickets with full reproduction steps. The code fixes shipped same-day. We also added a critical safety feature - a stale-data guard on trade confirmation. If the trade data has been modified since you loaded the page (maybe your trade partner added a card, or market prices shifted), the confirm button now blocks and forces a refresh. No more confirming on outdated information. We also swapped the card picker in trade proposals from pulling from your "binder" to pulling from your full collection of tradeable cards - a UX improvement that came directly from testing real trade flows.
I'm not going to sugarcoat it - finding 9 bugs in your own trading platform hurts. But I'd rather find them in a QA session than have a player find them at a convention booth. That's the whole point of building in public. You show the real numbers, even when they're uncomfortable.
Box EV overhaul - the math was wrong
While theory boards and trading were getting most of the attention, we quietly shipped a significant overhaul to the Box Expected Value calculator. This feature tells players whether a sealed booster box is "worth it" to open based on current card prices.
The math was wrong. In multiple ways.
Prestige and Showcase variants were completely missing from expected value calculations. These are some of the most valuable cards in a box, and they weren't being counted at all. The EV was systematically understating the value of every box.
The Regular Booster label was wrong - it was displaying an incorrect label in the UI, making it confusing to compare different product types.
Card counts were off due to incorrect card_type grouping in the SQL query. Cards were being double-counted or missed depending on their type classification.
And there was a P0 cache key bug - the cache was keyed incorrectly, so when you switched between sets, you could get served stale EV data from a completely different set. The NaN guard was also missing, so if any price data was undefined, the entire calculation would return NaN and display "NaN%" to the user.
We added Carbonite (SWU Set 4) support, wired everything through the effective price system (we have a whole pricing layer that detects market squeezes and presale conditions - never use raw marketPrice), and added test automation for the EV calculations so this category of bug can't silently regress.
TCGPlayer integration - Sprint 3 shipped
This one flew under the radar but it's a big deal for store operations. We shipped Sprint 3 of the TCGPlayer sales channel integration (CC-1019) - order tracking and inventory deduction. When a sale happens on TCGPlayer, CardCore now tracks the order and automatically deducts the sold inventory.
On top of that, we landed the Level-Up Mode minimum market price filter (COREAPP-1040), which lets stores set a floor price for their TCGPlayer listings. Three separate commits to get this right - the initial filter, the code changes to actually enforce it, and then passing it through the config API route so the UI could reflect the setting.
Combined with Sprint 1 and 2 (foundation + CSV export + staff UI), the TCGPlayer integration is now a real, functional sales channel. Stores can manage their TCGPlayer presence from inside CardCore.
Meta intelligence - paper is king
We made a philosophical decision about meta data that I think is important. Paper tournament tiers are now the primary view on the /meta page, with online data from Karabast demoted to a "Lab" section.
Here's why: paper tournament results are the gold standard for competitive card games. When someone top-8s a 64-player Planetary Qualifier, that data means something different than winning 5 games in a row on Karabast at midnight. Both are valuable - online data is great for spotting emerging trends and testing new archetypes - but they shouldn't carry equal weight in tier rankings.
We also parallelized all meta page API fetches and DB enrichment queries, which significantly reduced page load time. And we aligned our SWUMetaStats API parameters with their official documentation to prevent silent data mismatches that were causing some archetypes to show incorrect stats.
Collection fixes - the details that matter
Two collection fixes that seem small but make a real difference in daily use:
Instant rarity and price badges on card add - Previously, when you added a card to your collection, you had to refresh the page to see the rarity badge and price indicator. Now they appear immediately. This was a state management issue where the local cache wasn't being updated with the full card metadata on add.
Correct variant storage - We fixed a bug where subTypeName was being stored instead of variantLabel when adding cards. This caused display issues downstream - cards would show the wrong variant name in your collection, and filtering by variant type would miss cards that were labeled incorrectly.
Tournament and mobile - convention readiness
With April 12th approaching (our next booth event), we shipped targeted fixes for convention-day operations:
Tournament standings recalculation on match edit - If a judge needs to correct a match result mid-round, the standings now properly recalculate. Before this fix, editing a match result would leave the standings frozen at the pre-edit state until the next round was generated. At a 32-player tournament, that's the kind of bug that causes arguments.
Tournament-day safety suite - Match editing guards prevent accidental changes to completed rounds. Prize preview shows organizers what they're about to award before committing. Lifecycle tests validate the full tournament flow from registration through top cut.
Mobile deck editor overhaul - Expandable action buttons that don't crowd the screen on a phone. Clickable card thumbnails in the deck list. Action buttons shrunk from 48px to 36px for phone screens. Set name now shows alongside card number in list view so you can tell which version of a card you're looking at.
Scroll-to-top fix - We had a global scroll-behavior: smooth CSS rule that was breaking the scroll-to-top behavior on page navigation. You'd click a link, navigate to a new page, and find yourself halfway down instead of at the top. One of those bugs that's invisible until you notice it, and then it makes you want to throw your phone.
Karabast deck export - community integration
We shipped a public Karabast-compatible deck JSON export endpoint (COREAPP-1014). This means decks built in CardCore can be exported in a format that Karabast understands, bridging the gap between deck building and online play. Community tool interoperability matters - players shouldn't have to manually rebuild their deck in every app they use.
Deck builder polish - death by a thousand fixes
Across the two days, we also shipped a steady stream of deck builder improvements that don't individually make headlines but collectively transform the experience:
- Fixed Ground vs Space unit count in deck stats (was always showing zero for one of them - a Saboteur keyword was being double-counted as Sentinel)
- Added inline stats (power, HP, arena) to mobile search results so you can evaluate cards without opening the detail page
- Fixed deck names overlapping sort controls on mobile
- Fixed the like button being hidden on mobile shared/public deck views
- Added a No Aspect filter option to the deck builder
- Added Plot and Coordinate keywords to the backfill list
- Fixed "Cade Bane" misspelling and added fuzzy name matching to the card reconciliation system
- Fixed search failing when the name/subname separator was omitted
- Added "Browse Public Decks" navigation to the shared deck view
- Added card preview from the deck list in the editor
The numbers don't lie
Let me put this sprint in perspective. In 48 hours:
- 90 commits pushed to production
- 30+ tickets touched, tested, and shipped
- 30 commits on theory boards alone - an entire collaborative feature system from zero
- 9 trade platform bugs found and fixed in a single QA session
- 4 Box EV bugs fixed including a P0 cache key issue
- 3 TCGPlayer integration commits completing Sprint 3
- 11 mobile-specific fixes for convention readiness
- 42 bug fix commits and 10 feature commits
This is what it looks like when you refuse to ship something half-done. Every one of those 90 commits was tested on production. Every bug fix has a ticket with reproduction steps. Every feature has been clicked through on desktop and mobile.
What's next
The kiosk queue system needs a fix before April 12th (COREAPP-923) - there's an ambiguous column in the queue activity query that will break at the next booth event. The deck browse overhaul is queued up with pricing display, collection percentage indicators, and proper sort and search. And the multi-game expansion epic is mapped out across 34 tickets, waiting for the right sprint to bring Magic: The Gathering onto the platform.
But tonight, I'm going to close the laptop for a few hours and appreciate what just happened. Two days ago, theory boards didn't exist. The trading platform had 9 bugs we didn't know about. Box EV was giving players wrong numbers. Tonight, all of that is fixed, shipped, and live.
That's not a sprint. That's a statement.
90 commits. 48 hours. 30+ tickets closed. Every bug found was a bug fixed. This is what happens when you care about the game as much as the people playing it.