@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:
- ✅
helperautomatically 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 - Complete HTML examples (todo list, message board)
- 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}/_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:
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
- Example:
Document paths = EVEN number of segments (2, 4, 6, 8...)
- Example:
appData/my-app/publicData/_data/posts/doc123(6 segments) ✅ CORRECT!
- Example:
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 pathPath 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:
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' });Document Size: Documents are limited to 256 KB
// SDK will automatically check size and throw error if too largeAutomatic Validation: All
FirestoreHelpermethods 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-sdkBrowser 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 versionFor 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/notesAvailable 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-emptyQuick 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): FirebaseConfigExample:
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 firebaseError 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
publicReaddata - 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
publicReaddata - Read and write
publicData - Cannot access any
userData
✅ Admin Users:
- Everything regular users can do
- Write to
publicReaddata
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
userDatapaths
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 expiredpermission-denied- Missing required fields or insufficient permissionsMissing 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:
🎯 EASIEST: Use
FirestoreHelperfor 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🛡️ 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`), { ... });⚠️ Never use field names starting with
_:// ❌ WRONG - Reserved field { _myField: 'value' } // ✅ CORRECT { myField: 'value' }🗑️ Use soft delete instead of hard delete:
// ✅ RECOMMENDED await helper.softDeleteDoc(path, docId); // ❌ Only for admins await helper.deleteDoc(path, docId);✅ 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); }Handle token expiration:
- Tokens expire after 1 hour (3600 seconds)
- Check
expires_infield and refresh when needed
Use serverTimestamp() for timestamps:
import { serverTimestamp } from 'firebase/firestore'; { _createdAt: serverTimestamp() // Not new Date()! }Separate guest and authenticated flows:
- Use
generateGuestFirestoreToken()for anonymous users - Use
generateFirestoreToken()for logged-in users
- Use
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:
- GitHub: https://github.com/seaverseai/sv-sdk
- Email: [email protected]
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
- @seaverse/auth-sdk - User authentication and account management
- @seaverse/payment-sdk - Payment processing integration
