# How the Supabase Trades Table Works: Structure and Status Management Explained

> Understand the Supabase trades table structure and how TradeStatus enum manages pending, accepted, and declined card exchanges. Learn about offerer_ended and receiver_ended flags for completion confirmation.

- Repository: [Marcel Panse/tcg-pocket-collection-tracker](https://github.com/marcelpanse/tcg-pocket-collection-tracker)
- Tags: internals
- Published: 2026-03-06

---

**The Supabase `trades` table uses a `TradeStatus` enum with four states—`offered`, `accepted`, `declined`, and `finished`—to track card exchanges between friends, with boolean flags `offerer_ended` and `receiver_ended` ensuring both parties confirm completion before finalizing.**

The `marcelpanse/tcg-pocket-collection-tracker` repository implements a peer-to-peer trading system using Supabase as the backend database. The **Supabase trades table structure** centers on a single row that links two friends via their IDs and tracks the specific cards exchanged, while the **trade status management** workflow orchestrates the entire lifecycle from initial offer through dual-party confirmation.

## Supabase Trades Table Schema and Type Definition

The database schema is reflected in the frontend TypeScript type **`TradeRow`**, defined in [[`frontend/src/types/index.ts`](https://github.com/marcelpanse/tcg-pocket-collection-tracker/blob/main/frontend/src/types/index.ts)](https://github.com/marcelpanse/tcg-pocket-collection-tracker/blob/main/frontend/src/types/index.ts). This interface maps directly to the Supabase table columns:

| Column | Type | Description |
|--------|------|-------------|
| `id` | `number` | Auto-increment primary key identifying the unique trade record |
| `created_at` | `Date` | Timestamp when the trade was initiated |
| `updated_at` | `Date` | Timestamp of the last modification |
| `offering_friend_id` | `string` | Foreign key referencing the friend ID of the user initiating the offer |
| `receiving_friend_id` | `string` | Foreign key referencing the counter-party's friend ID |
| `offer_card_id` | `string` | Identifier of the card the offerer is giving |
| `receiver_card_id` | `string` | Identifier of the card the receiver provides in exchange |
| `offerer_ended` | `boolean` | Flag indicating whether the offering side has marked the trade complete |
| `receiver_ended` | `boolean` | Flag indicating whether the receiving side has marked the trade complete |
| `status` | `TradeStatus` | Current lifecycle stage of the trade |

The **`TradeStatus`** type is defined as a const assertion in the same file, restricting values to the four valid states:

```typescript
const tradeStatuses = ['offered', 'accepted', 'declined', 'finished'] as const
export type TradeStatus = (typeof tradeStatuses)[number]

```

## Trade Lifecycle: How Statuses Are Managed

The application moves trades through discrete states using the `insertTrade` and `updateTrade` functions located in [[`frontend/src/services/trade/tradeService.ts`](https://github.com/marcelpanse/tcg-pocket-collection-tracker/blob/main/frontend/src/services/trade/tradeService.ts)](https://github.com/marcelpanse/tcg-pocket-collection-tracker/blob/main/frontend/src/services/trade/tradeService.ts). Each transition corresponds to a specific user action and database update.

### Creating a Trade: The `offered` Status

When a user initiates a card exchange, the `insertTrade` function creates a new row with `status` set to `'offered'` by default. This represents a pending proposal awaiting the counter-party's response.

```typescript
import { insertTrade } from '@/services/trade/tradeService'

await insertTrade({
  offering_friend_id: myFriendId,
  receiving_friend_id: otherFriendId,
  offer_card_id: 'card123',
  receiver_card_id: 'card456',
  offerer_ended: false,
  receiver_ended: false,
  status: 'offered',
})

```

### Accepting or Declining: `accepted` and `declined` Statuses

The receiving party responds by calling `updateTrade` with the appropriate status. **Accepting** a trade updates the record to `status: 'accepted'`, while **declining** sets `status: 'declined'`. Both actions use the same service method with different payload values.

```typescript
import { updateTrade } from '@/services/trade/tradeService'

// Accept the pending offer
await updateTrade(tradeId, { status: 'accepted' })

// Or decline the offer
await updateTrade(tradeId, { status: 'declined' })

```

### Finalizing Trades: The `finished` Status and Dual Confirmation

Trades reach the terminal `finished` state only after both participants confirm completion. The system requires both `offerer_ended` and `receiver_ended` boolean flags to be `true` before setting `status: 'finished'`. This dual-confirmation pattern prevents premature closure and ensures both parties have fulfilled their obligations.

```typescript
await updateTrade(tradeId, {
  status: 'finished',
  offerer_ended: true,
  receiver_ended: true,
})

```

## Querying Active vs. Completed Trades

The service layer provides two distinct query patterns in [[`frontend/src/services/trade/tradeService.ts`](https://github.com/marcelpanse/tcg-pocket-collection-tracker/blob/main/frontend/src/services/trade/tradeService.ts)](https://github.com/marcelpanse/tcg-pocket-collection-tracker/blob/main/frontend/src/services/trade/tradeService.ts). **`getActiveTrades`** filters out completed exchanges by excluding rows where `status === 'finished'`, returning only ongoing negotiations between specific friend pairs. **`getAllTrades`** returns the complete history, enabling UI components like [[`frontend/src/pages/trade/TradeOffers.tsx`](https://github.com/marcelpanse/tcg-pocket-collection-tracker/blob/main/frontend/src/pages/trade/TradeOffers.tsx)](https://github.com/marcelpanse/tcg-pocket-collection-tracker/blob/main/frontend/src/pages/trade/TradeOffers.tsx) to group and display trades by their current status.

```typescript
import { getActiveTrades } from '@/services/trade/tradeService'

const { trades } = await getActiveTrades(myFriendId, otherFriendId)
// Returns only trades where status !== 'finished'

```

Cache synchronization is handled by React Query hooks in [[`frontend/src/services/trade/useTrade.ts`](https://github.com/marcelpanse/tcg-pocket-collection-tracker/blob/main/frontend/src/services/trade/useTrade.ts)](https://github.com/marcelpanse/tcg-pocket-collection-tracker/blob/main/frontend/src/services/trade/useTrade.ts), which automatically invalidate the `['trades']` query key after any insertion or status modification, ensuring the UI reflects the latest database state without manual refresh.

## Summary

- The **Supabase `trades` table** stores exchange records with nine core columns, including dual foreign keys for both trading parties and two boolean flags for completion tracking.
- **Trade statuses** follow a strict enum of `offered`, `accepted`, `declined`, and `finished`, defined in [`frontend/src/types/index.ts`](https://github.com/marcelpanse/tcg-pocket-collection-tracker/blob/main/frontend/src/types/index.ts).
- The **dual-confirmation system** requires both `offerer_ended` and `receiver_ended` to be `true` before a trade can reach `finished` status, ensuring bilateral agreement.
- **Active trade queries** explicitly filter out `finished` records, while the React Query integration in [`useTrade.ts`](https://github.com/marcelpanse/tcg-pocket-collection-tracker/blob/main/useTrade.ts) maintains real-time UI synchronization through automatic cache invalidation.

## Frequently Asked Questions

### What are the possible values for the TradeStatus enum in the Supabase trades table?

The **`TradeStatus`** type accepts exactly four string literals: `offered`, `accepted`, `declined`, and `finished`. These are defined as a const assertion array in [`frontend/src/types/index.ts`](https://github.com/marcelpanse/tcg-pocket-collection-tracker/blob/main/frontend/src/types/index.ts) and represent the complete lifecycle of a card trade from initiation to completion.

### How does the system prevent a trade from finishing prematurely?

The implementation uses two boolean columns, **`offerer_ended`** and **`receiver_ended`**, as safety checks. Both flags must be set to `true` via `updateTrade` before the `status` can legally transition to `finished`. This ensures neither party can unilaterally close an exchange while the other still considers it active.

### Where is the trades table schema defined in the codebase?

The schema is defined implicitly through the **`TradeRow`** TypeScript interface in [`frontend/src/types/index.ts`](https://github.com/marcelpanse/tcg-pocket-collection-tracker/blob/main/frontend/src/types/index.ts). While Supabase manages the actual PostgreSQL table structure, this interface serves as the source of truth for frontend developers, documenting all columns including `id`, `created_at`, `offering_friend_id`, and the `status` field.

### How does the frontend distinguish between active and completed trades?

The **`getActiveTrades`** function in [`frontend/src/services/trade/tradeService.ts`](https://github.com/marcelpanse/tcg-pocket-collection-tracker/blob/main/frontend/src/services/trade/tradeService.ts) applies a filter where `status !== 'finished'`, returning only pending or in-progress exchanges. For historical views, **`getAllTrades`** retrieves every record regardless of status, allowing components to segment the UI into active negotiations versus completed transactions.