npm package: @seaverse/data-service-sdk
Description: SDK for SeaVerse Data Service - Firestore token management with three-tier permission model
Author: SeaVerse Team
Latest version: 0.11.1

# @seaverse/data-service-sdk

SeaVerse Data Service SDK for accessing Firestore with secure token management and three-tier permission model.

---

## 🤖 **For LLM: START HERE!**

### 🚀 5-Minute Quick Start

**👉 Simplest Way**: Check **[LLM-FIRST.md](./LLM-FIRST.md)** - Complete HTML page examples (copy & paste ready)

**⚡ Core API Quick Reference** (Only 4 methods to remember):

| What You Want to Do | Use Which API | Code Example |
|---------------------|---------------|--------------|
| **Write Public Data** | `helper.addToPublicData()` | `await helper.addToPublicData('posts', {title: 'Hello'})` |
| **Read Public Data** | `helper.getPublicData()` | `const posts = await helper.getPublicData('posts')` |
| **Write Private Data** | `helper.addToUserData()` | `await helper.addToUserData('notes', {text: 'Secret'})` |
| **Read Private Data** | `helper.getUserData()` | `const notes = await helper.getUserData('notes')` |

**✨ Key Benefits**:
- ✅ `helper` automatically handles required fields (`_appId`, `_createdAt`, `_createdBy`)
- ✅ Automatically uses correct Firestore paths
- ✅ Automatically uses server timestamps
- ✅ Single import source (no need to import from multiple packages)

**🎯 Quick Links**:
- **[LLM-FIRST.md](./LLM-FIRST.md)** - Complete HTML examples (todo list, message board)
- **[LLM-QUICK-START.md](./LLM-QUICK-START.md)** - Code snippets and common mistakes

---

## 📖 For Developers

## Features

- 🔐 Secure Firestore token generation for authenticated users and guests
- 🎯 Three-tier permission model (publicRead, publicData, userData)
- 🚀 Direct Firestore access - no proxy server needed
- 🔒 Automatic data isolation by app_id
- 📝 TypeScript support with full type definitions
- 🤖 LLM-friendly documentation with clear examples
- 🛡️ Path helper functions to prevent permission-denied errors
- ✅ Built-in validation (reserved fields, document size)
- 🗑️ Soft delete support (mark as deleted without removing)

## Three-Tier Permission Model

SeaVerse organizes your Firestore data into three permission levels:

| Permission Level | Path Pattern | Read Access | Write Access | Use Case |
|-----------------|--------------|-------------|--------------|----------|
| **publicRead** | `appData/{app_id}/publicRead/_data/{collection}/{docId}` | All authenticated users | Admin only | System configs, announcements, static content |
| **publicData** | `appData/{app_id}/publicData/_data/{collection}/{docId}` | All authenticated users | All authenticated users | User posts, comments, shared content |
| **userData** | `appData/{app_id}/userData/{user_id}/{collection}/{docId}` | Owner only | Owner only | User settings, private notes, personal data |

### 🚨 CRITICAL: Firestore Path Rules (For LLM)

**Firestore path segments MUST follow these rules:**

1. **Collection paths = ODD number of segments** (1, 3, 5, 7...)
   - Example: `appData/my-app/publicData/_data/posts` (5 segments) ✅ CORRECT!
   - Firestore requires odd-numbered segments for collections

2. **Document paths = EVEN number of segments** (2, 4, 6, 8...)
   - Example: `appData/my-app/publicData/_data/posts/doc123` (6 segments) ✅ CORRECT!

3. **How to use correctly:**

```typescript
// ✅ CORRECT - Collection paths have ODD segments
collection(db, `appData/${appId}/publicData/_data/posts`)  // 5 segments (odd)

// ✅ CORRECT - Document paths have EVEN segments
doc(db, `appData/${appId}/publicData/_data/posts/doc123`)  // 6 segments (even)

// 💡 TIP: Always use path helper functions to avoid counting!
// - getPublicDataPath(appId, 'posts')  // Returns correct collection path
// - getPublicDataDocPath(appId, 'posts', 'doc123')  // Returns correct document path
```

**Path Structure Examples:**

```typescript
// Public Data (everyone can read/write)
const postsRef = collection(db, `appData/${appId}/publicData/_data/posts`);
await addDoc(postsRef, { ...data });  // Firestore adds document ID

// User Private Data (owner only)
const notesRef = collection(db, `appData/${appId}/userData/${userId}/notes`);
await addDoc(notesRef, { ...data });

// Public Read-Only (everyone can read, admin can write)
const configRef = collection(db, `appData/${appId}/publicRead/_data/config`);
await getDocs(configRef);
```

**The pattern is always:**
- `appData` → `{app_id}` → `{permission_layer}` → `_data` (for publicRead/publicData) OR `{user_id}` (for userData) → `{collection}` → (auto-generated doc ID)
- publicRead/publicData Collection: 5 segments (odd) ✅
- userData Collection: 5 segments (odd) ✅ (userId replaces _data segment)
- Document paths: add 1 more segment for docId

### Required Fields

All Firestore documents MUST include these three fields:

```typescript
{
  _appId: string,        // Your application ID (for data isolation)
  _createdAt: timestamp, // Server timestamp (use serverTimestamp())
  _createdBy: string     // User ID who created the document
}
```

These fields are enforced by Firestore Security Rules and ensure proper data isolation.

**🎯 Good News for LLM**: When using `FirestoreHelper`, these fields are automatically injected - you don't need to remember them!

### Reserved Fields & Validation

**⚠️ IMPORTANT: Fields starting with `_` are reserved for system use!**

The SDK automatically validates your data to prevent common mistakes:

1. **Reserved Fields**: You cannot create custom fields starting with `_`
   ```typescript
   // ❌ WRONG - Will throw error
   await helper.addToPublicData('posts', {
     _custom: 'value',  // Reserved field!
     title: 'My Post'
   });

   // ✅ CORRECT
   await helper.addToPublicData('posts', {
     customField: 'value',  // No underscore prefix
     title: 'My Post'
   });
   ```

2. **Document Size**: Documents are limited to 256 KB
   ```typescript
   // SDK will automatically check size and throw error if too large
   ```

3. **Automatic Validation**: All `FirestoreHelper` methods validate data automatically
   ```typescript
   // Manual validation if needed
   import { validateFirestoreData, validateDataDetailed } from '@seaverse/data-service-sdk';

   try {
     validateFirestoreData(myData);
     // Data is valid
   } catch (error) {
     console.error('Validation failed:', error.message);
   }

   // Or get detailed errors without throwing
   const result = validateDataDetailed(myData);
   if (!result.valid) {
     console.error('Errors:', result.errors);
   }
   ```

### Soft Delete Support

**🗑️ Recommended Practice: Use soft delete instead of hard delete**

Soft delete marks documents as deleted without removing them from the database:

```typescript
// ✅ RECOMMENDED: Soft delete (mark as deleted)
await helper.softDeleteDoc(
  getPublicDataPath(appId, 'posts'),
  'post-123'
);

// Document is still in database but marked as _deleted = true
// By default, helper.getPublicData() won't return deleted documents

// ❌ Hard delete (only for admins, permanent)
await helper.deleteDoc(
  getPublicDataPath(appId, 'posts'),
  'post-123'
);
```

**Why soft delete?**
- ✅ Data recovery possible
- ✅ Audit trail preserved
- ✅ Safer for production
- ✅ Follows industry best practices

## Installation

```bash
npm install @seaverse/data-service-sdk
```

## Browser vs Node.js Usage

### For Build Tools (Webpack/Vite/Parcel)

The SDK automatically uses the correct version:

```javascript
import { DataServiceClient } from '@seaverse/data-service-sdk';
// Bundler will automatically use the browser version
```

### For Direct Browser Use (Without Build Tools)

**Option 1: ES Module (Recommended)**

```html
<script type="module">
  import { DataServiceClient } from 'https://unpkg.com/@seaverse/data-service-sdk/dist/browser.js';

  const client = new DataServiceClient();
  // Use the client...
</script>
```

**Option 2: UMD via Script Tag**

```html
<script src="https://unpkg.com/@seaverse/data-service-sdk/dist/browser.umd.js"></script>
<script>
  const client = new SeaVerseDataService.DataServiceClient();
  // Use the client...
</script>
```

### For Node.js

```javascript
const { DataServiceClient } = require('@seaverse/data-service-sdk');
// or
import { DataServiceClient } from '@seaverse/data-service-sdk';
```

## Path Helper Functions (🚨 Recommended for LLM)

To prevent `permission-denied` errors caused by incorrect paths, we provide helper functions that generate the correct Firestore paths automatically.

### Why Use Path Helpers?

**Problem**: LLM or developers might accidentally use wrong paths:
```typescript
// ❌ WRONG - Will cause permission-denied!
collection(db, `apps/${appId}/publicArticles`)  // Not matching security rules!
collection(db, `apps/${appId}/users/${userId}/articles`)  // Not matching security rules!
```

**Solution**: Use path helper functions:
```typescript
import { getPublicDataPath, getUserDataPath } from '@seaverse/data-service-sdk';

// ✅ CORRECT - Guaranteed to match security rules
collection(db, getPublicDataPath(appId, 'posts'))  // → appData/{appId}/publicData/_data/posts
collection(db, getUserDataPath(appId, userId, 'notes'))  // → appData/{appId}/userData/{userId}/_data/notes
```

### Available Path Helpers

```typescript
import {
  getPublicReadPath,      // Returns: appData/{appId}/publicRead/_data/{collection}
  getPublicDataPath,      // Returns: appData/{appId}/publicData/_data/{collection}
  getUserDataPath,        // Returns: appData/{appId}/userData/{userId}/_data/{collection}
  getPublicReadDocPath,   // For specific public document
  getPublicDataDocPath,   // For specific public data document
  getUserDataDocPath,     // For specific user document
  PathBuilder             // For advanced path building
} from '@seaverse/data-service-sdk';
```

### Basic Usage Examples

```typescript
// Public data that everyone can read/write
const postsPath = getPublicDataPath(appId, 'posts');
await addDoc(collection(db, postsPath), {
  _appId: appId,
  _createdAt: serverTimestamp(),
  _createdBy: userId,
  title: 'My Post'
});

// Private user data
const notesPath = getUserDataPath(appId, userId, 'notes');
await addDoc(collection(db, notesPath), {
  _appId: appId,
  _createdAt: serverTimestamp(),
  _createdBy: userId,
  content: 'Private note'
});

// Public read-only data (admin writes only)
const configPath = getPublicReadPath(appId, 'config');
const configs = await getDocs(collection(db, configPath));

// Access specific document
const docPath = getPublicDataDocPath(appId, 'posts', 'post-123');
const docSnap = await getDoc(doc(db, docPath));
```

### Advanced: PathBuilder

For complex path construction:

```typescript
import { PathBuilder } from '@seaverse/data-service-sdk';

const builder = new PathBuilder(appId);

// Build collection path
const path = builder.publicData('posts').build();
// Returns: 'appData/my-app/publicData/posts'

// Build document path
const docPath = builder.publicData('posts').doc('post-123').build();
// Returns: 'appData/my-app/publicData/posts/post-123'

// Build user data path
const userPath = builder.userData(userId, 'notes').build();
// Returns: 'appData/my-app/userData/user-123/_data/notes'
```

### Error Prevention

Path helpers validate inputs to prevent common mistakes:

```typescript
// ❌ These will throw errors:
getPublicDataPath('my-app', 'posts/comments');  // Error: cannot contain /
getPublicDataPath('my-app', '');  // Error: must be non-empty string
getUserDataPath('my-app', '', 'notes');  // Error: userId must be non-empty
```

## Quick Start

### 🚀 Easiest Way (🤖 LLM-Recommended)

```typescript
import { DataServiceClient, initializeWithToken } from '@seaverse/data-service-sdk';
import { AuthClient } from '@seaverse/auth-sdk';

// Step 1: Login user
const authClient = new AuthClient({ appId: 'my-app-123' });
const loginResponse = await authClient.loginWithEmail({
  email: 'user@example.com',
  password: 'password123'
});

// Step 2: Get Firestore token
const dataClient = new DataServiceClient();
const tokenResponse = await dataClient.generateFirestoreToken({
  token: loginResponse.token,
  app_id: 'my-app-123'
});

// Step 3: Initialize Firebase & get helper
const { helper } = await initializeWithToken(tokenResponse);

// Step 4: Use Firestore with helper (automatic required fields!)

// ✅ LLM-FRIENDLY: Write to publicData - required fields auto-injected!
await helper.addToPublicData('posts', {
  title: 'My First Post',
  content: 'Hello world!'
});

// ✅ LLM-FRIENDLY: Read from publicData
const posts = await helper.getPublicData('posts');
posts.forEach(doc => {
  console.log(doc.id, doc.data());
});

// ✅ LLM-FRIENDLY: Write to userData (private) - auto-isolated!
await helper.addToUserData('notes', {
  title: 'Private Note',
  content: 'Only I can see this'
});

// ✅ NEW in v0.11.0: Simple Pagination Support
// Get first 20 posts
const first20 = await helper.getPublicData('posts', false, { limit: 20 });
first20.forEach(doc => {
  console.log(doc.id, doc.data());
});

// Get next page after last document
const lastDoc = first20.docs[first20.docs.length - 1];
const next20 = await helper.getPublicData('posts', false, {
  limit: 20,
  startAfter: lastDoc
});

// Also works with getUserData
const first10Notes = await helper.getUserData('notes', false, { limit: 10 });
```

<details>
<summary>📖 Advanced: Manual Way (for fine-grained control)</summary>

```typescript
import {
  DataServiceClient,
  initializeWithToken,
  getPublicDataPath,
  getUserDataPath,
  collection,
  addDoc,
  getDocs,
  serverTimestamp
} from '@seaverse/data-service-sdk';
import { AuthClient } from '@seaverse/auth-sdk';

// ... login and initialize ...
const { db, appId, userId } = await initializeWithToken(tokenResponse);

// Manual way: Use path helpers + required fields
const postsPath = getPublicDataPath(appId, 'posts');
await addDoc(collection(db, postsPath), {
  _appId: appId,              // REQUIRED
  _createdAt: serverTimestamp(), // REQUIRED
  _createdBy: userId,         // REQUIRED
  title: 'My First Post',
  content: 'Hello world!'
});
```
</details>

### 👤 For Guest Users (🤖 Even Simpler!)

```typescript
import { DataServiceClient, initializeWithToken } from '@seaverse/data-service-sdk';

// Step 1: Get guest token (no authentication needed!)
const dataClient = new DataServiceClient();
const tokenResponse = await dataClient.generateGuestFirestoreToken({
  app_id: 'my-app-123'
});

// Step 2: Initialize & get helper
const { helper } = await initializeWithToken(tokenResponse);

// Step 3: Guest can write to publicData (automatic required fields!)
await helper.addToPublicData('comments', {
  comment: 'Great app!',
  rating: 5
});

// Note: Guests CANNOT access userData
```

### 🔧 Manual Way (If you need more control)

If you prefer to initialize Firebase manually:

```typescript
import { DataServiceClient, getFirebaseConfig } from '@seaverse/data-service-sdk';
import { initializeApp } from 'firebase/app';
import { getAuth, signInWithCustomToken } from 'firebase/auth';
import { getFirestore } from 'firebase/firestore';

const dataClient = new DataServiceClient();
const tokenResponse = await dataClient.generateGuestFirestoreToken({
  app_id: 'my-app-123'
});

// Option 1: Use getFirebaseConfig helper
const firebaseConfig = getFirebaseConfig(tokenResponse);
const app = initializeApp(firebaseConfig);

// Option 2: Manual config
const app = initializeApp({
  apiKey: tokenResponse.web_api_key,  // ✅ Provided automatically!
  projectId: tokenResponse.project_id
});

const auth = getAuth(app);
await signInWithCustomToken(auth, tokenResponse.custom_token);

// ⚠️ IMPORTANT: Must specify database_id!
const db = getFirestore(app, tokenResponse.database_id);
```

**🚨 CRITICAL: Always Specify database_id**

When initializing Firestore, you MUST pass the `database_id` from the token response:

```typescript
// ✅ CORRECT - Specify database_id
const db = getFirestore(app, tokenResponse.database_id);

// ❌ WRONG - Will try to use "(default)" database which may not exist
const db = getFirestore(app);
```

## API Reference

### DataServiceClient

The main client for generating Firestore tokens.

#### Constructor

```typescript
new DataServiceClient(options?: DataServiceClientOptions)
```

**Options:**

- `baseURL?: string` - Base URL for the API (default: `https://auth.seaverse.ai`)
- `timeout?: number` - Request timeout in milliseconds (default: `10000`)
- `headers?: Record<string, string>` - Custom request headers

**Example:**

```typescript
const client = new DataServiceClient({
  baseURL: 'https://auth.seaverse.ai',
  timeout: 15000,
  headers: {
    'X-Custom-Header': 'value'
  }
});
```

### Methods

#### generateFirestoreToken

Generate a Firestore token for an authenticated user.

```typescript
generateFirestoreToken(
  request: GenerateFirestoreTokenRequest,
  options?: AxiosRequestConfig
): Promise<FirestoreTokenResponse>
```

**Request:**

```typescript
interface GenerateFirestoreTokenRequest {
  token: string;    // User's JWT token from Auth SDK
  app_id: string;   // Application ID
}
```

**Response:**

```typescript
interface FirestoreTokenResponse {
  custom_token: string;     // Firebase Custom Token - use with signInWithCustomToken()
  web_api_key: string;      // Firebase Web API Key - use with initializeApp()
  project_id: string;       // Firebase Project ID for initializeApp()
  database_id: string;      // Firestore Database ID
  app_id?: string;          // Application ID (use in Firestore paths)
  user_id: string;          // User ID (use in Firestore paths)
  user_type: string;        // User type ('guest', 'user', 'admin', 'appadmin')
  expires_in: number;       // Token expiration in seconds (typically 3600)
}
```

**Example:**

```typescript
const firestoreToken = await client.generateFirestoreToken({
  token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
  app_id: 'my-app-123'
});

console.log('Custom Token:', firestoreToken.custom_token);
console.log('User ID:', firestoreToken.user_id);
console.log('App ID:', firestoreToken.app_id);
```

#### generateGuestFirestoreToken

Generate a Firestore token for a guest (unauthenticated) user.

```typescript
generateGuestFirestoreToken(
  request: GenerateGuestFirestoreTokenRequest,
  options?: AxiosRequestConfig
): Promise<FirestoreTokenResponse>
```

**Request:**

```typescript
interface GenerateGuestFirestoreTokenRequest {
  app_id: string;   // Application ID
}
```

**Response:**

Same as `FirestoreTokenResponse` above, with `role` set to `'guest'`.

**Example:**

```typescript
const guestToken = await client.generateGuestFirestoreToken({
  app_id: 'my-app-123'
});

console.log('Guest Custom Token:', guestToken.custom_token);
console.log('Guest User ID:', guestToken.user_id);
console.log('User Type:', guestToken.user_type); // 'guest'
```

### Helper Functions

#### getFirebaseConfig

Extract Firebase configuration from token response.

```typescript
getFirebaseConfig(tokenResponse: FirestoreTokenResponse): FirebaseConfig
```

**Example:**

```typescript
import { getFirebaseConfig } from '@seaverse/data-service-sdk';
import { initializeApp } from 'firebase/app';

const tokenResponse = await client.generateGuestFirestoreToken({ app_id: 'my-app' });
const firebaseConfig = getFirebaseConfig(tokenResponse);
// Returns: { apiKey: '...', projectId: '...' }

const app = initializeApp(firebaseConfig);
```

#### initializeWithToken

Automatically initialize Firebase and sign in with token (one-line setup).

```typescript
initializeWithToken(tokenResponse: FirestoreTokenResponse): Promise<{
  app: FirebaseApp;
  auth: Auth;
  db: Firestore;
  userId: string;
  appId: string;
  helper: FirestoreHelper;
}>
```

**⚠️ IMPORTANT: Safe for Multiple Calls**

This function has been updated to safely handle multiple calls:
- ✅ **Reuses existing Firebase app** if already initialized
- ✅ **Re-authenticates with new token** for token refresh scenarios
- ✅ **Handles errors gracefully** with automatic cleanup
- ✅ **Validates app_id** before initialization

**Common Use Cases:**
```typescript
// ✅ SAFE: First-time initialization
const { helper } = await initializeWithToken(tokenResponse);

// ✅ SAFE: Token refresh (reuses existing app)
const newTokenResponse = await client.generateFirestoreToken({ ... });
const { helper: newHelper } = await initializeWithToken(newTokenResponse);

// ✅ SAFE: User re-login (reuses existing app)
const { helper } = await initializeWithToken(newUserTokenResponse);
```

**Example:**

```typescript
import { initializeWithToken } from '@seaverse/data-service-sdk';

const tokenResponse = await client.generateGuestFirestoreToken({ app_id: 'my-app' });

// One line to get everything!
const { db, appId, userId, helper } = await initializeWithToken(tokenResponse);

// Ready to use Firestore
await addDoc(collection(db, `appData/${appId}/publicData/_data/posts`), { ... });
```

**Note:** This function requires Firebase SDK to be installed separately:
```bash
npm install firebase
```

**Error Handling:**

```typescript
try {
  const { helper } = await initializeWithToken(tokenResponse);
} catch (error) {
  if (error.message.includes('app_id is required')) {
    // Handle missing app_id in token response
  } else if (error.message.includes('Firebase SDK not found')) {
    // Handle missing Firebase installation
  } else if (error.message.includes('Failed to initialize Firebase')) {
    // Handle authentication or initialization errors
  }
}
```

## Common Use Cases

### Use Case 1: Public Forum with Comments

```typescript
// Anyone (including guests) can post comments
await addDoc(collection(db, `appData/${appId}/publicData/_data/comments`), {
  _appId: appId,
  _createdAt: serverTimestamp(),
  _createdBy: userId,
  postId: 'post-123',
  comment: 'Great post!',
  likes: 0
});

// Anyone can read comments
const comments = await getDocs(
  collection(db, `appData/${appId}/publicData/_data/comments`)
);
```

### Use Case 2: User Private Settings

```typescript
// Only the user can write to their own settings
await setDoc(doc(db, `appData/${appId}/userData/${userId}/settings/preferences`), {
  _appId: appId,
  _createdAt: serverTimestamp(),
  _createdBy: userId,
  theme: 'dark',
  notifications: true,
  language: 'en'
});

// Only the user can read their own settings
const settings = await getDoc(
  doc(db, `appData/${appId}/userData/${userId}/settings/preferences`)
);
```

### Use Case 3: System Announcements (Admin Only)

```typescript
// Only admins can write to publicRead
// Regular users and guests can only read
const announcements = await getDocs(
  collection(db, `appData/${appId}/publicRead/_data/announcements`)
);

announcements.forEach(doc => {
  console.log('Announcement:', doc.data().message);
});
```

### Use Case 4: Querying Public Data

```typescript
import { query, where, orderBy, limit } from 'firebase/firestore';

// Query posts created by a specific user
const userPosts = query(
  collection(db, `appData/${appId}/publicData/_data/posts`),
  where('_createdBy', '==', userId),
  orderBy('_createdAt', 'desc'),
  limit(10)
);

const snapshot = await getDocs(userPosts);
```

## Permission Examples

### What Users CAN Do

✅ **Authenticated Users:**
- Read `publicRead` data
- Read and write `publicData` (all users' data)
- Read and write their own `userData/{userId}/_data/` only
- Update/delete documents where `_createdBy == userId`

✅ **Guest Users:**
- Read `publicRead` data
- Read and write `publicData`
- **Cannot** access any `userData`

✅ **Admin Users:**
- Everything regular users can do
- Write to `publicRead` data

### What Users CANNOT Do

❌ **All Users:**
- Access data from a different `app_id`
- Create documents without required fields (`_appId`, `_createdAt`, `_createdBy`)
- Modify documents created by other users (except in special cases)
- Access another user's `userData`

❌ **Guest Users:**
- Access any `userData` paths

## Error Handling

```typescript
try {
  const token = await client.generateFirestoreToken({
    token: 'invalid-token',
    app_id: 'my-app'
  });
} catch (error) {
  if (error instanceof Error) {
    console.error('Error:', error.message);
    // Handle error (e.g., invalid token, network error)
  }
}
```

Common errors:
- `Invalid token` - JWT token is invalid or expired
- `permission-denied` - Missing required fields or insufficient permissions
- `Missing or insufficient permissions` - Trying to access unauthorized data

## TypeScript Support

This SDK is written in TypeScript and provides full type definitions:

```typescript
import type {
  DataServiceClient,
  DataServiceClientOptions,
  GenerateFirestoreTokenRequest,
  GenerateGuestFirestoreTokenRequest,
  FirestoreTokenResponse,
  ApiResponse,
  ApiError
} from '@seaverse/data-service-sdk';
```

## Production Usage Recommendations

### Token Expiration Management

Firestore tokens expire after 1 hour (3600 seconds). For production applications, you should implement token refresh logic:

```typescript
import { DataServiceClient, initializeWithToken } from '@seaverse/data-service-sdk';

class FirestoreManager {
  private tokenExpiryTime: number | null = null;
  private dataClient = new DataServiceClient();
  private userToken: string | null = null;
  private appId: string;

  constructor(appId: string) {
    this.appId = appId;
  }

  async initialize(userToken: string) {
    this.userToken = userToken;

    // Generate Firestore token
    const tokenResponse = await this.dataClient.generateFirestoreToken({
      token: userToken,
      app_id: this.appId
    });

    // Initialize Firebase (safe to call multiple times)
    const result = await initializeWithToken(tokenResponse);

    // Track token expiry (expires_in is in seconds)
    this.tokenExpiryTime = Date.now() + (tokenResponse.expires_in * 1000);

    return result;
  }

  isTokenExpiringSoon(): boolean {
    if (!this.tokenExpiryTime) return true;

    // Check if token expires within 5 minutes
    const fiveMinutes = 5 * 60 * 1000;
    return Date.now() + fiveMinutes >= this.tokenExpiryTime;
  }

  async refreshTokenIfNeeded() {
    if (this.isTokenExpiringSoon() && this.userToken) {
      console.log('Token expiring soon, refreshing...');
      return await this.initialize(this.userToken);
    }
  }
}

// Usage
const manager = new FirestoreManager('my-app-123');

// Initialize
const { helper } = await manager.initialize(userJwtToken);

// Before making Firestore operations, check token
await manager.refreshTokenIfNeeded();
await helper.addToPublicData('posts', { title: 'My Post' });
```

### Advanced: Singleton Pattern with React

For React applications, you can create a singleton manager with hooks:

```typescript
// firestore-manager.ts
class FirestoreManager {
  private static instance: FirestoreManager | null = null;
  private helper: FirestoreHelper | null = null;
  private db: Firestore | null = null;
  private appId: string | null = null;
  private userId: string | null = null;
  private tokenExpiryTime: number | null = null;

  static getInstance(): FirestoreManager {
    if (!FirestoreManager.instance) {
      FirestoreManager.instance = new FirestoreManager();
    }
    return FirestoreManager.instance;
  }

  async initialize(tokenResponse: FirestoreTokenResponse) {
    // Always call initializeWithToken - it's safe for multiple calls
    const result = await initializeWithToken(tokenResponse);

    this.db = result.db;
    this.appId = result.appId;
    this.userId = result.userId;
    this.helper = result.helper;
    this.tokenExpiryTime = Date.now() + (tokenResponse.expires_in * 1000);

    return result;
  }

  getHelper(): FirestoreHelper | null {
    return this.helper;
  }

  isTokenExpiringSoon(): boolean {
    if (!this.tokenExpiryTime) return true;
    const fiveMinutes = 5 * 60 * 1000;
    return Date.now() + fiveMinutes >= this.tokenExpiryTime;
  }

  reset() {
    this.db = null;
    this.appId = null;
    this.userId = null;
    this.helper = null;
    this.tokenExpiryTime = null;
  }
}

export default FirestoreManager;

// useFirestore.ts (React Hook)
import { useState, useEffect } from 'react';
import { DataServiceClient } from '@seaverse/data-service-sdk';
import FirestoreManager from './firestore-manager';

export function useFirestore(userToken: string, appId: string) {
  const [helper, setHelper] = useState<FirestoreHelper | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    let mounted = true;

    async function init() {
      try {
        const dataClient = new DataServiceClient();
        const tokenResponse = await dataClient.generateFirestoreToken({
          token: userToken,
          app_id: appId
        });

        const manager = FirestoreManager.getInstance();
        const { helper } = await manager.initialize(tokenResponse);

        if (mounted) {
          setHelper(helper);
          setLoading(false);
        }
      } catch (err) {
        if (mounted) {
          setError(err as Error);
          setLoading(false);
        }
      }
    }

    init();

    return () => {
      mounted = false;
    };
  }, [userToken, appId]);

  return { helper, loading, error };
}

// Usage in component
function MyComponent() {
  const { helper, loading, error } = useFirestore(userToken, 'my-app-123');

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  const handleAddPost = async () => {
    await helper.addToPublicData('posts', {
      title: 'My Post',
      content: 'Hello World'
    });
  };

  return <button onClick={handleAddPost}>Add Post</button>;
}
```

### Background Token Refresh

For long-running applications, set up automatic background token refresh:

```typescript
class FirestoreManager {
  private refreshTimer: NodeJS.Timeout | null = null;

  async initialize(userToken: string, appId: string) {
    const dataClient = new DataServiceClient();
    const tokenResponse = await dataClient.generateFirestoreToken({
      token: userToken,
      app_id: appId
    });

    const result = await initializeWithToken(tokenResponse);

    // Schedule token refresh 5 minutes before expiry
    const refreshIn = (tokenResponse.expires_in - 5 * 60) * 1000;
    this.scheduleRefresh(userToken, appId, refreshIn);

    return result;
  }

  private scheduleRefresh(userToken: string, appId: string, delay: number) {
    if (this.refreshTimer) {
      clearTimeout(this.refreshTimer);
    }

    this.refreshTimer = setTimeout(async () => {
      console.log('Auto-refreshing Firestore token...');
      try {
        await this.initialize(userToken, appId);
      } catch (error) {
        console.error('Failed to refresh token:', error);
      }
    }, delay);
  }

  cleanup() {
    if (this.refreshTimer) {
      clearTimeout(this.refreshTimer);
      this.refreshTimer = null;
    }
  }
}
```

### Error Recovery

Implement proper error recovery for network failures:

```typescript
async function initializeWithRetry(
  tokenResponse: FirestoreTokenResponse,
  maxRetries = 3
) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await initializeWithToken(tokenResponse);
    } catch (error) {
      console.error(`Initialization attempt ${i + 1} failed:`, error);

      if (i === maxRetries - 1) {
        throw error; // Last attempt failed
      }

      // Wait before retry (exponential backoff)
      const delay = Math.pow(2, i) * 1000;
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}
```

## Best Practices for LLM

When using this SDK with LLM-generated code:

1. **🎯 EASIEST: Use `FirestoreHelper` for everything!**
   ```typescript
   // ✅ BEST PRACTICE - Let helper handle everything
   const { helper } = await initializeWithToken(tokenResponse);
   await helper.addToPublicData('posts', { title: 'Post', content: 'Hello' });

   // No need to remember:
   // - Required fields (_appId, _createdAt, _createdBy)
   // - Path construction
   // - Data validation
   // - Soft delete logic
   ```

2. **🛡️ Use path helper functions to avoid permission-denied errors:**
   ```typescript
   import { getPublicDataPath, getUserDataPath } from '@seaverse/data-service-sdk';

   // ✅ CORRECT - Use helpers
   const path = getPublicDataPath(appId, 'posts');
   await addDoc(collection(db, path), { ... });

   // ❌ WRONG - Manual paths may be incorrect
   await addDoc(collection(db, `apps/${appId}/posts`), { ... });
   ```

3. **⚠️ Never use field names starting with `_`:**
   ```typescript
   // ❌ WRONG - Reserved field
   { _myField: 'value' }

   // ✅ CORRECT
   { myField: 'value' }
   ```

4. **🗑️ Use soft delete instead of hard delete:**
   ```typescript
   // ✅ RECOMMENDED
   await helper.softDeleteDoc(path, docId);

   // ❌ Only for admins
   await helper.deleteDoc(path, docId);
   ```

5. **✅ Validation is automatic, but you can validate manually if needed:**
   ```typescript
   import { validateFirestoreData } from '@seaverse/data-service-sdk';

   try {
     validateFirestoreData(myData);
   } catch (error) {
     console.error('Invalid data:', error.message);
   }
   ```

6. **Handle token expiration:**
   - Tokens expire after 1 hour (3600 seconds)
   - Check `expires_in` field and refresh when needed

7. **Use serverTimestamp() for timestamps:**
   ```typescript
   import { serverTimestamp } from 'firebase/firestore';

   {
     _createdAt: serverTimestamp() // Not new Date()!
   }
   ```

8. **Separate guest and authenticated flows:**
   - Use `generateGuestFirestoreToken()` for anonymous users
   - Use `generateFirestoreToken()` for logged-in users

## Complete Example

Here's a complete example combining authentication and data access:

```typescript
import { DataServiceClient } from '@seaverse/data-service-sdk';
import { AuthClient } from '@seaverse/auth-sdk';
import { initializeApp } from 'firebase/app';
import { getAuth, signInWithCustomToken } from 'firebase/auth';
import { getFirestore, collection, addDoc, getDocs, query, where, serverTimestamp } from 'firebase/firestore';

async function completeExample() {
  const appId = 'my-app-123';

  // 1. Authenticate user
  const authClient = new AuthClient({ appId });
  const loginResponse = await authClient.loginWithEmail({
    email: 'user@example.com',
    password: 'password123'
  });

  // 2. Get Firestore token
  const dataClient = new DataServiceClient();
  const firestoreToken = await dataClient.generateFirestoreToken({
    token: loginResponse.token,
    app_id: appId
  });

  // 3. Initialize Firebase
  const app = initializeApp({ projectId: firestoreToken.project_id });
  const auth = getAuth(app);
  await signInWithCustomToken(auth, firestoreToken.custom_token);
  const db = getFirestore(app);

  const userId = firestoreToken.user_id;

  // 4. Create a post (publicData)
  const postRef = await addDoc(
    collection(db, `appData/${appId}/publicData/_data/posts`),
    {
      _appId: appId,
      _createdAt: serverTimestamp(),
      _createdBy: userId,
      title: 'My First Post',
      content: 'Hello world!',
      tags: ['introduction', 'first-post']
    }
  );
  console.log('Created post:', postRef.id);

  // 5. Read all posts
  const postsSnapshot = await getDocs(
    collection(db, `appData/${appId}/publicData/_data/posts`)
  );
  postsSnapshot.forEach(doc => {
    console.log('Post:', doc.id, doc.data());
  });

  // 6. Query user's own posts
  const myPostsQuery = query(
    collection(db, `appData/${appId}/publicData/_data/posts`),
    where('_createdBy', '==', userId)
  );
  const myPosts = await getDocs(myPostsQuery);
  console.log('My posts count:', myPosts.size);

  // 7. Save user preferences (private)
  await addDoc(
    collection(db, `appData/${appId}/userData/${userId}/preferences`),
    {
      _appId: appId,
      _createdAt: serverTimestamp(),
      _createdBy: userId,
      theme: 'dark',
      language: 'en',
      notifications: true
    }
  );
  console.log('Saved user preferences');
}

completeExample();
```

## API Endpoints

The SDK connects to the following endpoints by default:

- **Base URL:** `https://auth.seaverse.ai`
- **Firestore Token:** `POST /api/v1/firestore/token`
- **Guest Token:** `POST /api/v1/firestore/guest-token`

## License

MIT

## Support

For issues and questions, please visit:
- GitHub: https://github.com/seaverseai/sv-sdk
- Email: support@seaverse.com

## Browser Example (Vanilla JavaScript)

Here's a complete example for using the SDK directly in the browser without any build tools:

```html
<!DOCTYPE html>
<html>
<head>
  <title>SeaVerse Data Service SDK - Browser Example</title>
</head>
<body>
  <h1>Firestore Token Demo</h1>
  <button id="getGuestToken">Get Guest Token</button>
  <pre id="output"></pre>

  <script type="module">
    // Import from CDN
    import { DataServiceClient } from 'https://unpkg.com/@seaverse/data-service-sdk/dist/browser.js';

    const output = document.getElementById('output');
    const client = new DataServiceClient();

    document.getElementById('getGuestToken').addEventListener('click', async () => {
      try {
        output.textContent = 'Getting guest token...';

        const guestToken = await client.generateGuestFirestoreToken({
          app_id: 'my-app-123'
        });

        output.textContent = JSON.stringify({
          custom_token: guestToken.custom_token.substring(0, 50) + '...',
          user_id: guestToken.user_id,
          role: guestToken.role,
          project_id: guestToken.project_id,
          expires_in: guestToken.expires_in
        }, null, 2);

        // Now you can use this token with Firebase SDK
        console.log('Guest token received:', guestToken);
      } catch (error) {
        output.textContent = 'Error: ' + error.message;
      }
    });
  </script>
</body>
</html>
```

## Related SDKs

- [@seaverse/auth-sdk](../auth-sdk) - User authentication and account management
- [@seaverse/assets-sdk](../assets-sdk) - Assets and payment processing integration