Channels
A channel is anywhere a card can be live. SlabTrack tracks every card's
channel state in one authoritative table — card_channels — so the system
always knows whether a card is on ToT, on a Storefront, in a Showcase Break, or sitting
quiet in your personal collection.
Why channels exist
Without a unified channel system, you'd have a card listed on ToT AND a Storefront AND eBay simultaneously, two buyers could pay for it, and you'd be issuing refunds and apologies. The channel layer prevents this by being the one place that knows where a card is.
The channels
| Slug | Name | Behavior |
|---|---|---|
personal | Personal collection | Default. Card is private, not for sale anywhere. |
thisorthat | ThisOrThat | Live on ToT for Flip / Pull plays. Buyer pays variable price, always gets a card. |
storefront | Storefront | Live on a vendor's storefront (the operator's own or a peer's). Direct-buy at fixed price. |
showcase | Showcase Breaks | Reserved for an open showcase break. Card ships to whoever wins their spot. |
repack | Repack Pack | Locked into a sealed pack with other cards. Pack sells as a unit. |
ebay | eBay | Listed on eBay. Auto-pulled when sold or relisted. |
whatnot | Whatnot | Tagged for Whatnot live auction. Manual operator workflow. |
Some channels are exclusive (a card on Showcase can't also be on ToT — the showcase
locks the card). Others allow dual listing (Storefront + ToT can both list the same
card if DUAL_LISTING_ENABLED=true, with sale-locks coordinating to prevent double-sale).
Check each channel's adapter in backend/services/channels/registry.js for the canonical
exclusive flag.
The card_channels table
The schema (Postgres):
CREATE TABLE card_channels (
id SERIAL PRIMARY KEY,
card_id INTEGER REFERENCES cards(id),
user_id INTEGER REFERENCES users(id),
workspace_id INTEGER REFERENCES workspaces(id),
channel_slug TEXT NOT NULL, -- 'thisorthat', 'storefront', etc.
status TEXT NOT NULL, -- 'active' | 'inactive' | 'sold'
external_ref TEXT, -- satellite-side ID (ToT listing id, etc.)
metadata JSONB, -- per-channel context
engaged_at TIMESTAMP DEFAULT NOW(),
disengaged_at TIMESTAMP
);
The four state transitions
Engage
A card moves from personal to a channel. Triggered by:
- Strategist commit ("send 50 cards to ToT")
- Manual action in Triage or Lot Builder
- Storefront publish ("publish these cards to my shop")
Inserts a card_channels row with status='active' and notifies the satellite via webhook.
Disengage
A card pulls back from a channel without selling. Triggered by:
- Operator manually de-listing
- Bulk wipe (
/clean-slatein Strategist) - Cross-channel sale (the OTHER channels disengage when a card sells on one)
Sets status='inactive' and fires a disengage webhook to the satellite.
Sale
A card sells. The selling channel sets status='sold' on its own row and fires bulk-disengage
webhooks to every OTHER active channel for that card so they pull their listings.
Reconcile
If state drifts (manual delete on ToT, network failure, etc.), the Sync Center's reconciler asks each satellite for its canonical active-card list and clears stale rows.
Where channels show up in the UI
- Command Bridge (
/bridge): visualizes every card's channel state, conflict detection - Strategist: shows current channel chips next to each card row
- Card detail page: badge row at the top ("On ToT · On Storefront")
- Sync Center: drift between SlabTrack's view and each satellite's actual state
Code references
- Channel registry:
backend/services/channels/registry.js - Per-channel adapters:
backend/services/channels/{thisorthat,storefront,showcase,repack,ebay}.js - Engagement endpoint:
POST /api/curator/execute(single-card) orPOST /api/curator/strategist/commit(batch) - Sync reconciler:
backend/services/sync-reconciler.js