Skip to main content
The Argument Analysis Tool uses a hierarchical data model in Cloud Firestore that organizes all data under user-specific document trees. This structure ensures data privacy, enables efficient queries, and simplifies authorization.

Collection Hierarchy

The Firestore database follows a strict hierarchical structure:
/users/{userId}                              # Root: User profile
  ├── /argumentMaps/{mapId}                  # Subcollection: Argument maps
  └── /sources/{sourceId}                    # Subcollection: Sources (future)
All user data is nested under /users/{userId}. This path-based organization is critical for the security model, as it allows ownership verification without database reads.

Visual Data Structure

Firestore

└── users/
    ├── alice/
    │   ├── (profile fields)
    │   ├── argumentMaps/
    │   │   ├── map1
    │   │   ├── map2
    │   │   └── map3
    │   └── sources/
    │       ├── source1
    │       └── source2

    └── bob/
        ├── (profile fields)
        ├── argumentMaps/
        │   └── map1
        └── sources/
            └── source1

Collection Schemas

Users Collection: /users/{userId}

The root document for each user stores their profile information.

Schema

FieldTypeRequiredDescription
idstringUser’s Firebase Auth UID (must match document ID)
emailstringUser’s email address
displayNamestringUser’s display name (from auth provider)
photoURLstringUser’s profile photo URL
createdAtTimestampAccount creation timestamp
lastLoginAtTimestampLast login timestamp

Example Document

{
  "id": "alice-uid-123",
  "email": "[email protected]",
  "displayName": "Alice Johnson",
  "photoURL": "https://example.com/photos/alice.jpg",
  "createdAt": {
    "_seconds": 1704067200,
    "_nanoseconds": 0
  },
  "lastLoginAt": {
    "_seconds": 1709510400,
    "_nanoseconds": 0
  }
}

TypeScript Interface

import type { Timestamp } from "firebase/firestore";

export interface UserProfile {
  id: string;
  email: string;
  displayName?: string;
  photoURL?: string;
  createdAt: Timestamp;
  lastLoginAt?: Timestamp;
}

Security Rules

match /users/{userId} {
  allow get: if isOwner(userId);
  allow list: if false;  // Listing disabled for privacy
  allow create: if isOwner(userId) && isCreatingOwnProfile(userId);
  allow update: if isExistingOwner(userId) && isUserIdImmutable();
  allow delete: if isExistingOwner(userId);
}
The id field must match the document ID ({userId} in the path). This is enforced by the isCreatingOwnProfile() security rule.

Argument Maps: /users/{userId}/argumentMaps/{mapId}

Stores argument analysis results generated by the AI system.

Schema

FieldTypeRequiredDescription
idstringUnique map identifier (auto-generated)
userIdstringOwner’s UID (must match path {userId})
namestringArgument topic or URL (first 150 chars for text)
creationDateTimestampWhen the map was created
jsonDatastringSerialized AnalysisResult object
lastModifiedTimestampLast update timestamp
tagsstring[]User-defined tags for organization

Example Document

{
  "id": "map-abc123",
  "userId": "alice-uid-123",
  "name": "Climate Change Arguments",
  "creationDate": {
    "_seconds": 1709510400,
    "_nanoseconds": 0
  },
  "jsonData": "{\"blueprint\":[...],\"summary\":\"...\",\"analysis\":\"...\",\"socialPulse\":\"...\",\"tweets\":[...]}",
  "tags": ["environment", "science"]
}

TypeScript Interface

import type { Timestamp } from "firebase/firestore";

export type ArgumentMapDocument = {
  id: string;
  userId: string;
  name: string;
  creationDate: Timestamp;
  jsonData: string;  // Serialized AnalysisResult
  lastModified?: Timestamp;
  tags?: string[];
};

Security Rules

match /users/{userId}/argumentMaps/{argumentMapId} {
  allow get: if isOwner(userId);
  allow list: if isOwner(userId);
  allow create: if isOwner(userId) && isCreatingOwnSubcollectionDoc(userId);
  allow update: if isExistingOwner(userId) && isOwnerIdImmutable();
  allow delete: if isExistingOwner(userId);
}
The userId field is required and must match the {userId} in the path. This is enforced by the isCreatingOwnSubcollectionDoc() security rule.

Sources: /users/{userId}/sources/{sourceId}

Stores source documents (URLs, PDFs, text) that users analyze. This collection is defined in the security rules but not yet fully implemented in the application.

Schema (Proposed)

FieldTypeRequiredDescription
idstringUnique source identifier
userIdstringOwner’s UID
typestringSource type: "url", "text", "pdf"
urlstringURL if type is "url"
contentstringRaw text content
titlestringSource title or description
createdAtTimestampCreation timestamp
scrapedAtTimestampWhen URL was last scraped

Example Document

{
  "id": "source-xyz789",
  "userId": "alice-uid-123",
  "type": "url",
  "url": "https://example.com/article",
  "title": "The Impact of Climate Change",
  "createdAt": {
    "_seconds": 1709510400,
    "_nanoseconds": 0
  },
  "scrapedAt": {
    "_seconds": 1709510410,
    "_nanoseconds": 0
  }
}

Security Rules

match /users/{userId}/sources/{sourceId} {
  allow get: if isOwner(userId);
  allow list: if isOwner(userId);
  allow create: if isOwner(userId) && isCreatingOwnSubcollectionDoc(userId);
  allow update: if isExistingOwner(userId) && isOwnerIdImmutable();
  allow delete: if isExistingOwner(userId);
}

Nested Data Structures

The jsonData field in ArgumentMapDocument contains a serialized AnalysisResult object:

AnalysisResult Schema

export type AnalysisResult = {
  blueprint: ArgumentNode[];  // Hierarchical argument structure
  summary: string;            // AI-generated summary
  analysis: string;           // Detailed analysis
  socialPulse: string;        // Social media sentiment
  tweets: Tweet[];            // Related tweets
};

ArgumentNode Schema

export type ArgumentNode = {
  id: string;                 // Unique node ID
  parentId: string | null;    // Parent node ID (null for root)
  type: 'thesis' | 'claim' | 'counterclaim' | 'evidence';
  side: 'for' | 'against';    // Which side of the argument
  content: string;            // The argument text
  sourceText: string;         // Original source text
  source: string;             // Source citation/URL
  fallacies: string[];        // Identified logical fallacies
  logicalRole: string;        // Role in argument structure
};

ArgumentTree Schema

The ArgumentNode can be transformed into a tree structure:
export type ArgumentTree = ArgumentNode & {
  children: ArgumentTree[];   // Nested child arguments
};

Tweet Schema

export type Tweet = {
  id: string;
  text: string;
  author: {
    name: string;
    username: string;
    profile_image_url: string;
  };
  public_metrics: {
    retweet_count: number;
    reply_count: number;
    like_count: number;
    impression_count: number;
  };
  created_at: string;
};

Data Flow Diagram

┌─────────────┐
│   Client    │
│  (Browser)  │
└──────┬──────┘

       │ 1. Submit input + auth token

┌─────────────────────────────────────────┐
│        Server Action                    │
│  (src/lib/actions.ts::handleAnalysis)   │
└──────┬────────────────────┬─────────────┘
       │                    │
       │ 2. Verify token    │ 3. Generate analysis
       ▼                    ▼
┌──────────────┐     ┌─────────────────┐
│ Firebase     │     │   Genkit AI     │
│ Admin Auth   │     │   Flows         │
└──────────────┘     └────────┬────────┘

                              │ 4. Return AnalysisResult

                     ┌─────────────────────┐
                     │  Format & Save      │
                     │  to Firestore       │
                     └──────────┬──────────┘

        5. Write to user's subcollection

    ┌──────────────────────────────────────────────┐
    │  Firestore: /users/{userId}/argumentMaps/    │
    │  {
    │    id: "map-abc123",
    │    userId: "alice-uid-123",
    │    name: "Topic",
    │    creationDate: Timestamp,
    │    jsonData: JSON.stringify(analysisResult)
    │  }
    └──────────────────────────────────────────────┘

Data Operations

Creating an Argument Map

From src/lib/actions.ts:
'use server';

import { getFirebaseAdminApp } from './firebase-admin';
import { FieldValue } from 'firebase-admin/firestore';

export async function handleAnalysis(
  prevState: any,
  formData: FormData
) {
  // ... validation and AI processing ...
  
  // Save to Firestore
  const adminApp = await getFirebaseAdminApp();
  const firestore = adminApp.firestore();
  
  const docRef = firestore
    .collection('users')
    .doc(user.uid)
    .collection('argumentMaps')
    .doc();  // Auto-generate ID
  
  await docRef.set({
    id: docRef.id,
    userId: user.uid,           // Required by security rules
    name: topicForDB,
    creationDate: FieldValue.serverTimestamp(),
    jsonData: JSON.stringify(analysisResult)
  });
}
Security: Server Actions allow:
  • Token verification before database writes
  • Protection against client-side manipulation
  • Centralized error handling
  • Rate limiting and abuse prevention
Cost: Firebase Admin SDK operations don’t count toward client quotas.Reliability: Server-side writes are more reliable than client-side operations.

Querying User’s Argument Maps

Client-side query example:
import { collection, query, orderBy, getDocs } from 'firebase/firestore';
import { initializeFirebase } from '@/firebase';

const { firestore, auth } = initializeFirebase();
const user = auth.currentUser;

if (!user) throw new Error('Not authenticated');

const mapsRef = collection(
  firestore,
  `users/${user.uid}/argumentMaps`
);

const q = query(
  mapsRef,
  orderBy('creationDate', 'desc')
);

const snapshot = await getDocs(q);
const maps = snapshot.docs.map(doc => ({
  ...doc.data(),
  // Parse jsonData back to AnalysisResult
  analysisResult: JSON.parse(doc.data().jsonData)
}));

Updating an Argument Map

import { doc, updateDoc, Timestamp } from 'firebase/firestore';

const mapRef = doc(
  firestore,
  `users/${user.uid}/argumentMaps/${mapId}`
);

await updateDoc(mapRef, {
  name: 'Updated Topic Name',
  lastModified: Timestamp.now(),
  tags: ['environment', 'policy']
});
You cannot update the userId field after creation. This is enforced by the isOwnerIdImmutable() security rule.

Deleting an Argument Map

import { doc, deleteDoc } from 'firebase/firestore';

const mapRef = doc(
  firestore,
  `users/${user.uid}/argumentMaps/${mapId}`
);

await deleteDoc(mapRef);

Indexing Strategy

Firestore automatically creates indexes for simple queries. For the Argument Analysis Tool, the following indexes are recommended:

Single-Field Indexes (Auto-created)

  • users/{userId}/argumentMaps: creationDate (descending)
  • users/{userId}/argumentMaps: name (ascending)
  • users/{userId}/argumentMaps: tags (array-contains)

Composite Indexes (Create if needed)

{
  "collectionGroup": "argumentMaps",
  "queryScope": "COLLECTION",
  "fields": [
    { "fieldPath": "userId", "order": "ASCENDING" },
    { "fieldPath": "tags", "arrayConfig": "CONTAINS" },
    { "fieldPath": "creationDate", "order": "DESCENDING" }
  ]
}
Enables queries like:
query(
  collection(firestore, `users/${userId}/argumentMaps`),
  where('tags', 'array-contains', 'environment'),
  orderBy('creationDate', 'desc')
)
Firestore will automatically prompt you to create composite indexes when you run a query that requires one. Follow the provided link to auto-generate the index.

Data Migration Considerations

Adding New Fields

When adding new fields to existing collections:
  1. Make fields optional in TypeScript interfaces
  2. Provide default values when reading documents
  3. Update security rules if the field affects authorization
// Reading with defaults for new fields
const data = doc.data();
const map: ArgumentMapDocument = {
  ...data,
  tags: data.tags || [],  // Default for new field
  lastModified: data.lastModified || data.creationDate
};

Renaming Collections

Firestore doesn’t support renaming collections. To migrate:
  1. Create new collection structure
  2. Copy data programmatically
  3. Update application code to use new paths
  4. Update security rules
  5. Delete old collection after verification

Best Practices

Always Include userId

Every subcollection document must have a userId field matching the path.

Use Auto-Generated IDs

Let Firestore generate document IDs with .doc() for uniqueness.

Denormalize When Needed

Store frequently accessed data redundantly to avoid multiple reads.

Limit Document Size

Keep documents under 1MB. Use subcollections for large datasets.

Document Size Management

The jsonData field stores the entire AnalysisResult as a serialized string:Pros:
  • Single read operation to get full analysis
  • Atomic writes (all or nothing)
  • Easy to version (entire structure changes together)
Cons:
  • Document size grows with argument complexity
  • Cannot query individual arguments within the map
Alternative: If individual arguments need querying, create a separate arguments subcollection:
/users/{userId}/argumentMaps/{mapId}/arguments/{argId}

Performance Optimization

Read Efficiency

  • Use .get() instead of .list() when you know the document ID
  • Limit query results with .limit(n) to prevent large reads
  • Use pagination with startAfter() for large collections
// Efficient: Get specific document
const mapDoc = await getDoc(
  doc(firestore, `users/${userId}/argumentMaps/${mapId}`)
);

// Efficient: Paginated list
const firstBatch = await getDocs(
  query(
    collection(firestore, `users/${userId}/argumentMaps`),
    orderBy('creationDate', 'desc'),
    limit(10)
  )
);

Write Efficiency

  • Use batch writes for multiple operations
  • Use transactions for read-modify-write operations
  • Avoid hot spots by not updating the same document too frequently
import { writeBatch } from 'firebase/firestore';

const batch = writeBatch(firestore);

batch.set(doc(firestore, `users/${userId}/argumentMaps/${map1Id}`), map1Data);
batch.set(doc(firestore, `users/${userId}/argumentMaps/${map2Id}`), map2Data);
batch.update(doc(firestore, `users/${userId}`), { lastActivityAt: Timestamp.now() });

await batch.commit();  // All or nothing

Next Steps

Security Rules

Learn how data access is protected

Architecture Overview

Understand the overall system design

Build docs developers (and LLMs) love