How Public Collection Views Are Shared Using a Unique Friend ID While Protecting Privacy in TCG Pocket Collection Tracker
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 and TradeWith.tsx.
The Public Collection Service Layer
Located in 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.
// 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. This hook isolates public collection queries from authenticated user data and implements aggressive caching since public collections are immutable snapshots.
// 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 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.
// 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.
-- 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 that improves readability without compromising security.
// 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:
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_idfor each user that bears no mathematical relationship to their email address or account creation date. - Supabase view
public_card_amounts_collectionexplicitly excludes theemailcolumn, ensuring the database layer physically cannot return sensitive identifiers for public queries. - The
getPublicCollectionservice function enforces mandatoryfriend_idvalidation 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: Infinityoptimizes 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →