# How Public Collection Views Are Shared Using a Unique Friend ID While Protecting Privacy in TCG Pocket Collection Tracker

> Learn how TCG Pocket Collection Tracker shares public views with a unique friend ID, protecting user privacy by avoiding email exposure. Accessible via shareable URLs.

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

---

**TL;DR:** The TCG Pocket Collection Tracker generates a random 16-digit `friend_id` for each user and exposes only this identifier—never email addresses—through a dedicated Supabase view that powers read-only public collection pages accessible via shareable URLs.

The marcelpanse/tcg-pocket-collection-tracker repository implements a privacy-centric sharing mechanism that allows users to showcase their card collections publicly without revealing sensitive account information. By leveraging cryptographically random friend identifiers and a carefully scoped database view, the system ensures that email addresses remain completely isolated from public access while enabling seamless collection sharing through URL-based routing.

## Privacy-First Architecture Overview

The system employs a three-layer privacy architecture that strictly separates public collection data from sensitive user information. At the storage layer, the `accounts` table maintains the mapping between private emails and public identifiers, while the presentation layer relies exclusively on the `friend_id` for all external sharing.

The **user model** stores a randomly generated 16-digit `friend_id` alongside the private email address, ensuring no derivable relationship exists between the identifier and the user's personal information. The **Supabase view** `public_card_amounts_collection` explicitly selects only `friend_id` and card-related fields, permanently excluding the `email` column from the queryable dataset. Finally, the **frontend layer** enforces strict parameter validation through the `getPublicCollection` function, which rejects requests lacking a valid `friend_id` and never references authenticated user emails when rendering public views in components like [`Collection.tsx`](https://github.com/marcelpanse/tcg-pocket-collection-tracker/blob/main/Collection.tsx) and [`TradeWith.tsx`](https://github.com/marcelpanse/tcg-pocket-collection-tracker/blob/main/TradeWith.tsx).

## The Public Collection Service Layer

Located in [`frontend/src/services/collection/collectionService.ts`](https://github.com/marcelpanse/tcg-pocket-collection-tracker/blob/main/frontend/src/services/collection/collectionService.ts), the `getPublicCollection` function serves as the sole entry point for retrieving shared collection data. This function strictly requires a `friendId` parameter and queries the read-only Supabase view that contains no email addresses.

```typescript
// frontend/src/services/collection/collectionService.ts (lines 58-65)
export const getPublicCollection = async (friendId: string): Promise<Map<number, CollectionRow>> => {
  if (!friendId) {
    throw new Error('Friend ID is required to fetch public collection')
  }
  // Queries the Supabase view that contains only public data
  const collection = await fetchCollectionFromAPI(
    'public_card_amounts_collection',   // <-- view without email
    'friend_id',
    friendId
  )
  return new Map(collection.map(row => [row.internal_id, row]))
}

```

The function enforces privacy at the application layer by validating the `friendId` parameter before executing the query. By targeting `public_card_amounts_collection` rather than the underlying tables containing email addresses, the service guarantees that only anonymized collection data traverses the network boundary.

## Frontend Data Fetching and State Management

The React layer implements additional privacy controls through the `usePublicCollection` hook defined in [`frontend/src/services/collection/useCollection.ts`](https://github.com/marcelpanse/tcg-pocket-collection-tracker/blob/main/frontend/src/services/collection/useCollection.ts). This hook isolates public collection queries from authenticated user data and implements aggressive caching since public collections are immutable snapshots.

```typescript
// frontend/src/services/collection/useCollection.ts (lines 24-30)
export function usePublicCollection(friendId: string | undefined) {
  return useQuery({
    queryKey: ['collection', friendId],
    queryFn: () => getPublicCollection(friendId as string),
    enabled: !!friendId,
    staleTime: Infinity,               // cache forever – public data never changes here
  })
}

```

The hook's `enabled` property ensures queries only execute when a valid `friendId` exists, preventing accidental exposure of private collection data. The `staleTime: Infinity` configuration reflects the read-only nature of public views, eliminating unnecessary network requests while maintaining strict separation between public and private data fetching patterns.

## Route-Based Rendering Without Email Exposure

The public collection page at [`frontend/src/pages/collection/Collection.tsx`](https://github.com/marcelpanse/tcg-pocket-collection-tracker/blob/main/frontend/src/pages/collection/Collection.tsx) demonstrates complete isolation from authenticated user contexts. The component extracts the `friendId` exclusively from URL parameters, ensuring that email addresses never enter the public collection rendering pipeline.

```typescript
// frontend/src/pages/collection/Collection.tsx
import { usePublicCollection } from '@/services/collection/useCollection'

const Collection = () => {
  const { friendId } = useParams()          // e.g. /collection/:friendId
  const { data: cards = new Map(), isLoading } = usePublicCollection(friendId)

  // render cards …
}

```

By sourcing the identifier from `useParams` rather than the authenticated session, the component guarantees that public viewers see only the collection associated with the specific `friend_id`, with no mechanism to reverse-lookup the owner's email address or account details.

## Database Privacy Controls via Supabase Views

The critical privacy safeguard resides in the database schema definition of the `public_card_amounts_collection` view. This view implements column-level security by explicitly omitting the `email` field from its projection, joining the `card_amounts` table with the `accounts` table solely to access the `friend_id` foreign key.

```sql
-- Conceptual definition of the Supabase view
CREATE VIEW public_card_amounts_collection AS
SELECT
  friend_id,
  card_id,
  amount_owned,
  internal_id
FROM card_amounts
JOIN accounts ON accounts.email = card_amounts.email;

```

This architectural choice ensures that even if the public API endpoints were compromised, the underlying data structure physically prevents email exposure. The view contains only the 16-digit `friend_id` and quantitative card data, rendering it impossible to correlate public collections with user identities through database queries alone.

## The Sharing Workflow

Users initiate sharing by generating URLs containing their unique `friend_id`. The system provides a formatting utility in [`frontend/src/lib/utils.ts`](https://github.com/marcelpanse/tcg-pocket-collection-tracker/blob/main/frontend/src/lib/utils.ts) that improves readability without compromising security.

```typescript
// frontend/src/lib/utils.ts
export function formatFriendId(friendId: string): string {
  if (!friendId || friendId.length !== 16) return friendId
  return `${friendId.slice(0,4)}-${friendId.slice(4,8)}-${friendId.slice(8,12)}-${friendId.slice(12)}`
}

```

When a user copies their share link, the application constructs a URL using only the `friend_id`:

```typescript
const shareUrl = `${window.location.origin}/collection/${account.friend_id}`

```

Recipients accessing this URL trigger the `usePublicCollection` hook with the extracted parameter, initiating the privacy-preserving data flow described above. The 16-digit identifier provides sufficient entropy to prevent brute-force guessing while remaining user-friendly when formatted with hyphens.

## Summary

- The system generates a **cryptographically random 16-digit `friend_id`** for each user that bears no mathematical relationship to their email address or account creation date.
- **Supabase view `public_card_amounts_collection`** explicitly excludes the `email` column, ensuring the database layer physically cannot return sensitive identifiers for public queries.
- The **`getPublicCollection` service function** enforces mandatory `friend_id` validation and queries only the privacy-scoped view, preventing accidental data leakage.
- **Frontend components** source collection identifiers exclusively from URL parameters via `useParams`, maintaining strict isolation between authenticated user sessions and public collection views.
- **React Query caching** with `staleTime: Infinity` optimizes performance for immutable public data while preserving the privacy boundaries established at the database layer.

## Frequently Asked Questions

### What is the friend_id in the TCG Pocket Collection Tracker?

The `friend_id` is a randomly generated 16-digit string assigned to each user during account creation, stored in the `accounts` table alongside the private email address. This identifier serves as the sole public-facing token for collection sharing, designed to provide sufficient entropy for security while remaining shareable through simple URL copy-paste workflows.

### How does the Supabase view prevent email exposure?

The `public_card_amounts_collection` view defines an explicit column projection that includes only `friend_id`, `card_id`, `amount_owned`, and `internal_id`, deliberately omitting the `email` column present in the underlying `accounts` table. This means any query executed against the view—whether through the frontend service layer or direct database access—physically cannot return email addresses, providing defense-in-depth against API misconfigurations or injection attacks.

### Can someone guess a user's friend_id to access their collection?

The 16-digit format provides 10^16 possible combinations, making brute-force attacks computationally impractical for casual adversaries. Additionally, the system treats the `friend_id` as an opaque token with no sequential relationship to user registration order, preventing enumeration attacks based on account creation timestamps.

### Why does the usePublicCollection hook use staleTime: Infinity?

The `staleTime: Infinity` configuration in the React Query hook reflects the immutable nature of public collection snapshots. Since public views are read-only and the underlying `friend_id` mapping remains constant for a given user, the data never becomes "stale" during a browser session. This optimization reduces network traffic while maintaining the privacy guarantees established by the database view and service layer validation.