Skip to main content

Overview

Argument Cartographer uses Firebase Firestore as its primary database, chosen for rapid development, built-in security, real-time capabilities, and managed scaling. The data model follows a strict user-ownership pattern for privacy and security.
Security Model: All user data is isolated under /users/{userId} paths with declarative Firestore Security Rules preventing cross-user access.

Data Architecture

Collection Structure

Root Collections

Purpose: User profile documentsPath: /users/{userId}Schema:
interface UserProfile {
  id: string;                // Matches doc ID
  email: string;             // From Firebase Auth
  displayName: string | null;
  photoURL: string | null;
  createdAt: Timestamp;
  updatedAt: Timestamp;
}
Access Control:
  • Read: Owner only
  • Write: Owner only
  • List: Disabled (prevents user enumeration)
Example:
/users/abc123xyz {
  "id": "abc123xyz",
  "email": "[email protected]",
  "displayName": "Jane Doe",
  "createdAt": "2024-03-01T10:00:00Z"
}

Subcollections

Purpose: User’s saved argument analysesPath: /users/{userId}/argumentMaps/{mapId}Schema:
interface ArgumentMap {
  id: string;
  userId: string;            // Denormalized for queries
  
  // Input
  input: string;             // Original query/URL/text
  
  // Analysis Results
  blueprint: ArgumentNode[];
  summary: string;
  analysis: string;
  credibilityScore: number;
  brutalHonestTake: string;
  keyPoints: string[];
  
  // Social Data
  socialPulse: string;
  tweets: Tweet[];
  
  // Fallacies
  fallacies: DetectedFallacy[];
  
  // Metadata
  createdAt: Timestamp;
  updatedAt: Timestamp;
}
Indexes:
  • (userId, createdAt DESC) - For history queries
Access Control:
  • Read: Owner only
  • Write: Owner only
  • List: Owner can list their own

Nested Data Structures

ArgumentNode

interface ArgumentNode {
  id: string;                   // Unique within blueprint
  parentId: string | null;      // null for thesis
  type: 'thesis' | 'claim' | 'counterclaim' | 'evidence';
  side: 'for' | 'against';
  content: string;              // The argument text
  sourceText: string;           // Verbatim quote from source
  source: string;               // URL
  fallacies: string[];          // Array of fallacy IDs
  logicalRole: string;          // "Primary evidence", etc.
}
Storage: Embedded array within argumentMaps.blueprint Why not separate collection?
  • Tight coupling with analysis
  • Always fetched together
  • Simpler queries
  • Atomic updates

DetectedFallacy

interface DetectedFallacy {
  id: string;
  name: string;                 // "Ad Hominem"
  severity: 'Critical' | 'Major' | 'Minor';
  category: string;             // "Logical", "Rhetorical"
  confidence: number;           // 0-1
  problematicText: string;      // Exact quote
  explanation: string;
  definition: string;
  avoidance: string;
  example: string;
  suggestion: string;
  location?: string;            // Which node (optional)
}
Storage: Embedded array within argumentMaps.fallacies

Tweet

interface Tweet {
  id: string;                   // Twitter tweet ID
  text: string;
  author: {
    name: string;
    username: string;
    profile_image_url: string;
  };
  created_at: string;           // ISO 8601
  public_metrics: {
    retweet_count: number;
    reply_count: number;
    like_count: number;
    impression_count: number;
  };
}
Storage: Embedded array within argumentMaps.tweets

Firestore Security Rules

Declarative access control enforced at the database level.

Core Rules File

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    
    // Helper functions
    function isSignedIn() {
      return request.auth != null;
    }
    
    function isOwner(userId) {
      return isSignedIn() && request.auth.uid == userId;
    }
    
    function isExistingOwner(userId) {
      return isOwner(userId) && resource != null;
    }
    
    // User profiles
    match /users/{userId} {
      allow get: if isOwner(userId);
      allow list: if false;  // No user enumeration
      allow create: if isOwner(userId) && 
                       request.resource.data.id == userId;
      allow update: if isExistingOwner(userId) && 
                       request.resource.data.id == resource.data.id;
      allow delete: if isExistingOwner(userId);
    }
    
    // Argument maps (private)
    match /users/{userId}/argumentMaps/{mapId} {
      allow get, list: if isOwner(userId);
      allow create: if isOwner(userId) && 
                       request.resource.data.userId == userId;
      allow update, delete: if isExistingOwner(userId);
    }
    
    // Sources (private cache)
    match /users/{userId}/sources/{sourceId} {
      allow get, list: if isOwner(userId);
      allow create: if isOwner(userId) && 
                       request.resource.data.userId == userId;
      allow update, delete: if isExistingOwner(userId);
    }
    
    // Radar topics (public read, admin write)
    match /radarTopics/{topicId} {
      allow read: if true;  // Public
      allow write: if false;  // Server-side only
    }
  }
}

Security Principles

1

Default Deny

All operations denied unless explicitly allowed
2

Path-Based Authorization

Ownership determined by {userId} in path - fast, secure
3

No Public Data

User data never readable by other users
4

Immutable IDs

User IDs and document IDs cannot be changed after creation
5

No Cross-User Queries

Impossible to query other users’ data even with malicious client
Never bypass security rules from server-side code! Even with Firebase Admin SDK, respect the security model for consistency.

Query Patterns

User’s Analysis History

import { collection, query, where, orderBy, limit, getDocs } from 'firebase/firestore';

const getAnalysisHistory = async (userId: string, limitCount = 20) => {
  const ref = collection(db, `users/${userId}/argumentMaps`);
  const q = query(
    ref,
    orderBy('createdAt', 'desc'),
    limit(limitCount)
  );
  
  const snapshot = await getDocs(q);
  return snapshot.docs.map(doc => ({
    id: doc.id,
    ...doc.data()
  }));
};
Index Required: (userId, createdAt DESC)

Radar Topic Feed

const getRadarTopics = async (category?: string) => {
  const ref = collection(db, 'radarTopics');
  const constraints = [
    where('archived', '==', false),
    orderBy('updatedAt', 'desc'),
    limit(50)
  ];
  
  if (category) {
    constraints.unshift(where('categories', 'array-contains', category));
  }
  
  const q = query(ref, ...constraints);
  const snapshot = await getDocs(q);
  return snapshot.docs.map(doc => doc.data());
};
Composite Index: (archived, categories, updatedAt DESC)

Real-Time Listeners (Future)

import { onSnapshot } from 'firebase/firestore';

const subscribeToAnalysis = (userId: string, mapId: string, callback) => {
  const ref = doc(db, `users/${userId}/argumentMaps/${mapId}`);
  
  return onSnapshot(ref, (snapshot) => {
    if (snapshot.exists()) {
      callback(snapshot.data());
    }
  });
};

// Usage
const unsubscribe = subscribeToAnalysis(userId, mapId, (data) => {
  console.log('Analysis updated:', data);
});

// Cleanup
unsubscribe();

Data Lifecycle

Creation Flow

1

User Submits Analysis

Client sends request via Server Action
2

Server Processes

Genkit flow generates complete analysis
3

Firestore Write

Server Action saves to /users/{userId}/argumentMaps/{autoId}
const docRef = await addDoc(
  collection(db, `users/${userId}/argumentMaps`),
  {
    ...analysisResult,
    userId,
    createdAt: serverTimestamp(),
  }
);
4

Return to Client

Document ID and data returned to client for display

Update Flow (Future)

Re-analyze existing topic with fresh data:
const refreshAnalysis = async (userId: string, mapId: string) => {
  // Re-run analysis flow
  const freshResult = await generateArgumentBlueprint({ 
    input: existingAnalysis.input 
  });
  
  // Update document
  await updateDoc(
    doc(db, `users/${userId}/argumentMaps/${mapId}`),
    {
      ...freshResult,
      updatedAt: serverTimestamp(),
    }
  );
};

Deletion Policy

Users can delete their analyses:
await deleteDoc(doc(db, `users/${userId}/argumentMaps/${mapId}`));
Cascade: Sources are NOT deleted (may be reused)

Performance Optimization

Indexing Strategy

Required Composite Indexes:
  1. User Analysis History
    • Collection: argumentMaps (collection group)
    • Fields: userId ASC, createdAt DESC
    • Query: User history sorted by recency
  2. Radar Feed Filtering
    • Collection: radarTopics
    • Fields: archived ASC, categories ASC, updatedAt DESC
    • Query: Active topics by category
  3. Featured Topics
    • Collection: radarTopics
    • Fields: featured ASC, credibilityScore DESC
    • Query: Top featured topics
Firestore will prompt you to create indexes when queries fail. Click the link in the error to auto-generate.

Denormalization

Pattern: Duplicate userId in subcollection documents Benefit: Enables collection group queries without joins
// Instead of relying on path alone
/users/{userId}/argumentMaps/{mapId}

// Also store userId in document
{
  "id": "map123",
  "userId": "user456",  // <-- Denormalized
  "blueprint": [...]
}

Batch Operations

Update multiple documents atomically:
const batch = writeBatch(db);

// Update multiple analyses
analysisIds.forEach(id => {
  const ref = doc(db, `users/${userId}/argumentMaps/${id}`);
  batch.update(ref, { archived: true });
});

await batch.commit(); // Atomic - all or nothing
Limits: 500 operations per batch

Cost Optimization

Read Costs

Firestore Pricing (as of 2024):
  • Reads: $0.06 per 100K
  • Writes: $0.18 per 100K
  • Deletes: $0.02 per 100K
Optimization Strategies:
  1. Pagination: Limit queries to 20-50 results
  2. Client Caching: Cache data in React state/localStorage
  3. Selective Fetching: Only fetch what’s displayed
  4. Real-Time Sparingly: Use snapshots only when necessary

Write Costs

Expensive operations:
  • Creating analyses (inevitable)
  • Updating Radar topics (batched)
  • Logging (minimize production logs)
Tip: Batch related writes together

Next Steps

External Integrations

How external APIs interact with the data layer

AI Orchestration

How AI generates the data stored in Firestore

Installation

Set up Firebase for your own instance

Configuration

Configure Firestore and security rules

Build docs developers (and LLMs) love