npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@seaverse/data-service-sdk

v0.10.2

Published

SDK for SeaVerse Data Service - Firestore token management with three-tier permission model

Readme

@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 - 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:


📖 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}/_data/{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:

// ✅ 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:

// 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}/_data/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} → ({user_id} for userData) → _data{collection} → (auto-generated doc ID)
  • publicRead/publicData Collection: 5 segments (odd) ✅
  • userData Collection: 6 segments (even) ✅ (includes userId)
  • Document paths: add 1 more segment for docId

Required Fields

All Firestore documents MUST include these three fields:

{
  _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 _

    // ❌ 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

    // SDK will automatically check size and throw error if too large
  3. Automatic Validation: All FirestoreHelper methods validate data automatically

    // 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:

// ✅ 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

npm install @seaverse/data-service-sdk

Browser vs Node.js Usage

For Build Tools (Webpack/Vite/Parcel)

The SDK automatically uses the correct version:

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)

<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

<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

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:

// ❌ 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:

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

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

// 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:

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:

// ❌ 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)

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: '[email protected]',
  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'
});
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!'
});

👤 For Guest Users (🤖 Even Simpler!)

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:

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:

// ✅ 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

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:

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.

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

Request:

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

Response:

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:

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.

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

Request:

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

Response:

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

Example:

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.

getFirebaseConfig(tokenResponse: FirestoreTokenResponse): FirebaseConfig

Example:

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).

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:

// ✅ 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:

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:

npm install firebase

Error Handling:

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

// 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

// Only the user can write to their own settings
await setDoc(doc(db, `appData/${appId}/userData/${userId}/_data/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}/_data/settings/preferences`)
);

Use Case 3: System Announcements (Admin Only)

// 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

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

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:

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:

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:

// 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:

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:

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!

    // ✅ 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:

    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 _:

    // ❌ WRONG - Reserved field
    { _myField: 'value' }
    
    // ✅ CORRECT
    { myField: 'value' }
  4. 🗑️ Use soft delete instead of hard delete:

    // ✅ 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:

    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:

    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:

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: '[email protected]',
    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}/_data/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:

Browser Example (Vanilla JavaScript)

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

<!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