AI-Friendly Universal Data Storage SDK for TypeScript/JavaScript
npm install @seaverse/dataserviceA TypeScript/JavaScript SDK for universal data storage with PostgREST backend. Store and query JSON data with automatic user isolation and type safety.
- Type-safe: Full TypeScript support with generic types
- Secure: Built on PostgreSQL Row-Level Security (RLS)
- Flexible: Store any JSON data structure
- Query builder: Fluent API for complex queries
- Auto-extraction: Automatically extracts app ID from URL
- UUID support: Client-side or server-side ID generation
``bash`
npm install @seaverse/dataservice
This package uses a factory pattern for client creation. Here's what it exports:
Functions:
- createClient(config) - Factory function to create a client instance (async)debugSetToken(token)
- - Set debug token for testing (call before createClient)setAppId(appId)
- - [Deprecated] Set application ID manually (not recommended)
Types (TypeScript):
- DataServiceClient - Type of the client instance (not a constructor)DataRecord
- - Type of stored recordsCollection
- - Type of collection instancesQueryBuilder
- - Type of query builder instancesDataTable
- - Type of data table instancesClientConfig
- - Configuration options for createClientDataServiceError
- - Error class for API errors
Constants:
- VERSION - SDK version string
Important: Always use createClient() to create clients. DataServiceClient is a TypeScript type, not a class constructor.
`typescript
// ✓ CORRECT
import { createClient } from '@seaverse/dataservice';
const client = await createClient({});
// ✗ WRONG - DataServiceClient is not a constructor
import { DataServiceClient } from '@seaverse/dataservice';
const client = new DataServiceClient(); // This will fail!
`
The SDK uses a factory function pattern. Always use createClient():
Important: The SDK automatically obtains the service host from the parent page via PostMessage (500ms timeout). If the fetch fails, it defaults to https://dataservice-api.seaverse.ai. No configuration is needed.
`typescript
import { createClient, debugSetToken } from '@seaverse/dataservice';
// Production: Auto-fetch token and serviceHost from parent page (when running in iframe)
// Falls back to https://dataservice-api.seaverse.ai if fetch fails
const client = await createClient({});
// Development/Testing: Use debug token
debugSetToken('your-test-token');
const client = await createClient({});
`
`typescript
// Store user preferences (single record)
const userPrefs = client.userData.collection('user_preferences');
await userPrefs.insert({
theme: 'dark',
language: 'en',
notifications: true,
});
// Update preferences
await userPrefs.patch(userPrefs.id, { theme: 'light' });
// Store multiple items (e.g., orders) - each needs unique collection name
const order1 = await client.userData.collection('order_001').insert({
order_number: 'ORD-001',
status: 'pending',
total: 99.99,
});
const order2 = await client.userData.collection('order_002').insert({
order_number: 'ORD-002',
status: 'shipped',
total: 149.99,
});
// Or use batch insert for multiple records
const orders = await client.userData.batchInsert('order', [
{ order_number: 'ORD-003', status: 'pending', total: 79.99 },
{ order_number: 'ORD-004', status: 'pending', total: 199.99 },
]);
// Delete a specific record
await client.userData.collection('order_001').delete(order1.id);
`
``
DataServiceClient
└── DataTable (e.g., userData)
└── Collection (e.g., "orders")
└── DataRecord (JSONB data + metadata)
The SDK automatically extracts app_id from the current environment:
Browser: Extracted from URL hostname
``
https://app_8e5e867e-user_f4ed2364.app.seaverse.ai
→ appId: "app_8e5e867e-user_f4ed2364"
Node.js: Set via environment variable
`bash`
export SEAVERSE_APP_ID=my-app-id
Access the extracted ID:
`typescript`
console.log(client.appId); // "app_8e5e867e-user_f4ed2364"
Manual Override (Not Recommended):
For special cases where auto-extraction doesn't work, you can manually set the appId using setAppId():
`typescript
import { setAppId, createClient } from '@seaverse/dataservice';
// Set manual appId before creating client (highest priority)
setAppId('my-custom-app-id');
// Client will use manual appId instead of auto-extraction
const client = await createClient({});
console.log(client.appId); // "my-custom-app-id"
`
⚠️ Note: setAppId() is deprecated and not recommended for normal use. The SDK automatically extracts appId from the URL, which is the recommended approach. Only use this for special cases where auto-extraction doesn't work.
AppId Priority (from highest to lowest):
1. setAppId() - Manual override (not recommended)
2. URL extraction (browser) / Environment variable (Node.js) - Automatic and recommended
The SDK provides access to different data tables with different permission scopes:
- userData: User-specific data (isolated by user_id)
- More tables coming soon (public data, shared data, etc.)
Critical Understanding: A collection name identifies a single record, not a container for multiple records.
The (user_id, app_id, collection_name) combination is a unique constraint - meaning each collection name can only store ONE record per user and app.
To store multiple records, you MUST use unique collection names:
`typescript
// ✓ CORRECT: Each record gets a unique collection name
await client.userData.collection('order_001').insert(order1);
await client.userData.collection('order_002').insert(order2);
await client.userData.collection('order_003').insert(order3);
// ✓ RECOMMENDED: Use batch insert (auto-generates unique names)
const insertedOrders = await client.userData.batchInsert('order', [order1, order2, order3]);
// Creates collections: order_
// ✗ WRONG: Reusing the same collection name
const orders = client.userData.collection('orders');
await orders.insert(order1); // ✓ Success
await orders.insert(order2); // ✗ ERROR: 409 Conflict (collection 'orders' already exists)
`
Why this design?
- Each collection name acts as a unique key for a single record
- Think of it like a key-value store: collection_name → single recordorder_${orderId}
- For multiple records, use patterns like: , msg_${conversationId}_${msgId}
`typescript
// ❌ WRONG: This looks like it should work, but it doesn't
const orders = client.userData.collection('orders');
await orders.insert({ order_number: 'ORD-001', total: 99.99 }); // ✓ Works
await orders.insert({ order_number: 'ORD-002', total: 149.99 }); // ✗ ERROR: 409 Conflict!
// ✓ CORRECT: Each record needs a unique collection name
await client.userData.collection('order_001').insert({ order_number: 'ORD-001', total: 99.99 });
await client.userData.collection('order_002').insert({ order_number: 'ORD-002', total: 149.99 });
`
Why? The (user_id, app_id, collection_name) combination is a unique constraint. Once you insert into 'orders', that collection name is "taken" for that user and app.
`typescript
// ❌ MISLEADING: This looks like it queries multiple orders
const orders = client.userData.collection('orders');
const pending = await orders.select().eq('data->>status', 'pending').execute();
// Returns: [] or [single order] - NOT multiple orders!
// ✓ CORRECT: To work with multiple records, track them separately
const orderIds = ['order_001', 'order_002', 'order_003'];
const allOrders = await Promise.all(
orderIds.map(id => client.userData.collection(id).get(recordId))
);
`
Why? A collection name identifies a single record, so queries on that collection can only return 0 or 1 record.
`typescript
// ❌ MISLEADING: Plural name suggests multiple items
const orders = client.userData.collection('orders');
const users = client.userData.collection('users');
// ✓ BETTER: Use singular or ID-based names
const order = client.userData.collection('order_12345');
const userProfile = client.userData.collection('user_profile');
const preference = client.userData.collection('user_preferences');
`
Why? Plural names create false expectations. Use singular names or include IDs to make it clear each collection is one record.
Use this for user preferences, settings, profiles - things where you only need one record:
`typescript
// User preferences (one per user)
const prefs = await client.userData.collection('preferences').insert({
theme: 'dark',
language: 'en',
});
// User profile (one per user)
const profile = await client.userData.collection('profile').insert({
name: 'John Doe',
avatar: 'https://...',
});
`
Use this for multiple items of the same type:
`typescriptorder_${orderId1}
// Multiple orders - each with unique collection name
const order1 = await client.userData.collection().insert(orderData1);order_${orderId2}
const order2 = await client.userData.collection().insert(orderData2);
// Multiple conversations
const conv1 = await client.userData.collection(conv_${convId1}).insert(convData1);conv_${convId2}
const conv2 = await client.userData.collection().insert(convData2);`
Use this when creating multiple records at once:
`typescript
// Create multiple orders in one call
const orders = await client.userData.batchInsert('order', [
{ order_number: 'ORD-001', total: 99.99 },
{ order_number: 'ORD-002', total: 149.99 },
{ order_number: 'ORD-003', total: 199.99 },
]);
// Returns array of DataRecord with auto-generated collection names
// order_
`
Use this for nested data structures:
`typescriptconv_${convId}_msg_1
// Messages within a conversation
await client.userData.collection().insert(message1);conv_${convId}_msg_2
await client.userData.collection().insert(message2);
// Tasks within a project
await client.userData.collection(project_${projId}_task_1).insert(task1);project_${projId}_task_2
await client.userData.collection().insert(task2);`
Each record contains:
`typescript`
interface DataRecord
id: string; // UUID primary key
user_id: string; // Owner user ID
app_id: string; // Application ID
collection_name: string; // Collection identifier
data: T; // Your JSON data
created_at: string; // ISO timestamp
updated_at: string; // ISO timestamp
deleted_at: string | null; // Soft delete timestamp
}
`typescript
import { createClient } from '@seaverse/dataservice';
const client = await createClient({
options?: {
timeout?: number; // Request timeout in ms (default: 30000)
tokenFetchTimeout?: number; // Token fetch timeout in ms (default: 5000)
headers?: Record
};
});
`
ServiceHost Auto-Detection:
The SDK automatically fetches the service host from the parent page via PostMessage:
- Timeout: 500ms (hardcoded, not configurable)
- Protocol: Sends { type: 'seaverse:get_service_host', payload: { serviceName: 'dataservice' } }https://dataservice-api.seaverse.ai
- Fallback: If fetch fails, defaults to
- No user configuration: ServiceHost is managed internally to prevent request failures
Token Authentication Priority:
The SDK uses the following priority order for obtaining authentication tokens:
1. Debug Token (Highest Priority) - Set via debugSetToken()
2. Parent Page Token - Auto-fetched via PostMessage when in iframe
3. No Token - Client created without authentication (API calls will fail with 401)
AppId Configuration Priority:
The SDK uses the following priority order for determining the application ID:
1. Manual AppId (Highest Priority) - Set via setAppId() (not recommended, deprecated)
2. Auto-extraction - Extracted from URL (browser) or environment variable (Node.js)
Production Usage (Auto-fetch from parent):
The SDK automatically fetches the authentication token and service host from the parent page via PostMessage when running in an iframe:
`typescript
// No configuration needed - auto-fetches token and serviceHost from parent
const client = await createClient({});
// Parent page should respond to PostMessage:
// Token request:
// Send: { type: 'seaverse:get_token' }
// Receive: { type: 'seaverse:token', payload: { accessToken: string, expiresIn: number } }
// Error: { type: 'seaverse:error', error: string }
// ServiceHost request:
// Send: { type: 'seaverse:get_service_host', payload: { serviceName: 'dataservice' } }
// Receive: { type: 'seaverse:service_host', payload: { serviceHost: string } }
// Error: { type: 'seaverse:error', error: string }
`
Development/Testing (Debug Token):
For development and testing, use debugSetToken to bypass the parent page token fetch:
`typescript
import { debugSetToken, createClient } from '@seaverse/dataservice';
// Set debug token BEFORE creating client (highest priority)
debugSetToken('your-test-token');
// Client will use debug token directly, skipping parent page fetch
const client = await createClient({});
`
Manual AppId Override (Not Recommended):
For special cases, you can force a specific appId using setAppId(), which will override auto-extraction:
`typescript
import { setAppId, createClient } from '@seaverse/dataservice';
// Current URL: https://app_auto_extracted.app.seaverse.ai
// Without setAppId, client.appId would be "app_auto_extracted"
// Force specific appId (overrides auto-extraction)
setAppId('my-custom-app-id');
const client = await createClient({});
console.log(client.appId); // "my-custom-app-id" (not "app_auto_extracted")
// All operations will use the manual appId
await client.userData.collection('test').insert({ foo: 'bar' });
// ↑ This record will be stored with app_id = "my-custom-app-id"
`
⚠️ Warning: setAppId() is deprecated. Only use it when:
- Testing in environments where URL-based extraction doesn't work
- Debugging issues with specific appIds
- Working around temporary limitations
In production, always rely on automatic appId extraction from the URL.
Graceful Degradation:
If token or serviceHost fetching fails, the SDK gracefully handles the failure:
- Token fetch failure: Creates client without authentication (API calls will return 401)
- ServiceHost fetch failure: Falls back to default https://dataservice-api.seaverse.ai
`typescript
// Client creation never throws, even if fetch fails
const client = await createClient({});
try {
// API calls may fail with authentication errors if no token
const data = await client.userData.collection('test').insert({ foo: 'bar' });
} catch (error) {
if (error instanceof DataServiceError && error.statusCode === 401) {
console.log('Authentication required - no token available');
// Handle authentication error (e.g., show login prompt)
}
}
`
`typescript
// Get a collection
client.userData.collection
// Batch insert multiple records
client.userData.batchInsert
baseName: string,
records: T[]
): Promise
`
#### Create
`typescript
// Insert with auto-generated UUID
collection.insert(data: T): Promise
// Insert with custom UUID
collection.insert(data: T, id: string): Promise
`
#### Read
`typescript
// Get by ID
collection.get(id: string): Promise
collection.selectById(id: string): Promise
// Query builder
collection.select(): QueryBuilder
// Search by criteria
collection.search(criteria: Partial
// Count records
collection.count(): Promise
`
#### Update
`typescript
// Full update (replaces entire data object)
collection.update(id: string, data: T): Promise
// Partial update (merges with existing data)
collection.patch(id: string, partial: Partial
`
#### Delete
`typescript`
// Delete a record by ID (permanent deletion)
collection.delete(id: string): Promise
Build complex queries with a fluent API:
`typescript
collection.select()
// Comparison operators
.eq(field: string, value: any) // Equal
.neq(field: string, value: any) // Not equal
.gt(field: string, value: any) // Greater than
.gte(field: string, value: any) // Greater than or equal
.lt(field: string, value: any) // Less than
.lte(field: string, value: any) // Less than or equal
// Pattern matching
.like(field: string, pattern: string) // Case-sensitive
.ilike(field: string, pattern: string) // Case-insensitive
// Array operations
.in(field: string, values: any[]) // Value in array
// JSONB operations
.contains(value: any) // JSONB contains
// Sorting and pagination
.order(field: string, options?: { descending?: boolean })
.limit(count: number)
.offset(count: number)
// Execute
.execute(): Promise
.count(): Promise
`
Field syntax for JSONB queries:
- Use data->>field for text comparison: .eq('data->>status', 'pending')data->field
- Use for numeric comparison: .gt('data->total', '100')
`typescript
// Get user data statistics
client.getStats(): Promise<{
total_records: number;
total_collections: number;
storage_bytes: number;
}>
// Health check
client.health(): Promise<{
status: string;
user_id: string;
}>
`
`typescript
import { createClient } from '@seaverse/dataservice';
type Order = {
order_number: string;
customer_email: string;
items: Array<{
product_id: string;
quantity: number;
price: number;
}>;
status: 'pending' | 'processing' | 'shipped' | 'delivered';
total: number;
notes?: string;
};
// For development/testing with Node.js
import { debugSetToken, createClient } from '@seaverse/dataservice';
debugSetToken(process.env.JWT_TOKEN!);
const client = await createClient({});
// Create multiple orders - each with unique collection name
const orderNumber1 = ORD-${Date.now()};order_${orderNumber1}
const order1 = await client.userData.collection).insert({
order_number: orderNumber1,
customer_email: 'customer@example.com',
items: [
{ product_id: 'PROD-001', quantity: 2, price: 29.99 },
{ product_id: 'PROD-002', quantity: 1, price: 49.99 },
],
status: 'pending',
total: 109.97,
});
console.log('Order created:', order1.id, 'Collection:', order_${orderNumber1});
// Create another order
const orderNumber2 = ORD-${Date.now() + 1};order_${orderNumber2}
const order2 = await client.userData.collection).insert({
order_number: orderNumber2,
customer_email: 'customer@example.com',
items: [
{ product_id: 'PROD-003', quantity: 1, price: 199.99 },
],
status: 'pending',
total: 199.99,
});
// Update order status (access by collection name)
await client.userData.collection(order_${orderNumber1}).patch(order1.id, {
status: 'shipped'
});
// Get specific order
const retrieved = await client.userData.collection(order_${orderNumber1}).get(order1.id);
console.log('Order status:', retrieved?.data.status);
// Delete a specific order
await client.userData.collection(order_${orderNumber1}).delete(order1.id);
// Note: To query across multiple orders, you would need to:
// 1. Store order metadata in a separate tracking collection
// 2. Or use a naming convention and iterate through known order IDs
// 3. Or use the batchInsert pattern and track the generated collection names
`
`typescript
type Message = {
role: 'user' | 'assistant' | 'system';
content: string;
timestamp: string;
};
type Conversation = {
title: string;
model: string;
messages: Message[];
metadata?: {
tokens_used?: number;
cost?: number;
};
};
// Create a new conversation with unique ID
const conversationId = conv_${Date.now()};
const conv = await client.userData.collection
title: 'Project Planning Discussion',
model: 'claude-3-opus',
messages: [
{
role: 'user',
content: 'Help me plan a new feature for my app',
timestamp: new Date().toISOString(),
},
],
});
console.log('Conversation created:', conversationId);
// Add assistant response to existing conversation
const current = await client.userData.collection
if (current) {
await client.userData.collection
messages: [
...current.data.messages,
{
role: 'assistant',
content: 'I can help you with that. What feature are you thinking about?',
timestamp: new Date().toISOString(),
},
],
});
}
// Store individual messages separately (alternative pattern)
// Each message gets its own collection
const msgId1 = await client.userData.collection(${conversationId}_msg_1).insert({
role: 'user',
content: 'Help me plan a new feature',
timestamp: new Date().toISOString(),
});
const msgId2 = await client.userData.collection(${conversationId}_msg_2).insert({
role: 'assistant',
content: 'I can help with that!',
timestamp: new Date().toISOString(),
});
// Get a specific conversation
const retrieved = await client.userData.collection(conversationId).get(conv.id);
console.log('Conversation title:', retrieved?.data.title);
`
`typescript
type UserPreferences = {
theme: 'light' | 'dark' | 'auto';
language: string;
notifications: {
email: boolean;
push: boolean;
sms: boolean;
};
privacy: {
profile_visible: boolean;
show_activity: boolean;
};
};
const prefs = client.userData.collection
// Initialize preferences
await prefs.insert({
theme: 'auto',
language: 'en',
notifications: {
email: true,
push: true,
sms: false,
},
privacy: {
profile_visible: true,
show_activity: false,
},
});
// Update theme only
await prefs.patch(prefId, { theme: 'dark' });
`
All records use UUID strings as primary keys.
`typescript
const order = await orders.insert({
order_number: 'ORD-001',
status: 'pending',
});
console.log(order.id); // "550e8400-e29b-41d4-a716-446655440000"
`
Useful for offline-first applications or client-side ID generation:
`typescript
// Node.js
import { randomUUID } from 'crypto';
const id = randomUUID();
// Browser
const id = crypto.randomUUID();
// With uuid library
import { v4 as uuidv4 } from 'uuid';
const id = uuidv4();
// Use custom ID
const order = await orders.insert(
{ order_number: 'ORD-002', status: 'pending' },
id
);
`
Full type safety with generic types:
`typescript
type Product = {
name: string;
price: number;
in_stock: boolean;
};
const products = client.userData.collection
const product = await products.insert({
name: 'Widget',
price: 29.99,
in_stock: true,
});
// TypeScript knows the shape
console.log(product.data.name); // ✓ string
console.log(product.data.price); // ✓ number
console.log(product.data.invalid); // ✗ TypeScript error
`
`typescript
import { DataServiceError } from '@seaverse/dataservice';
try {
await collection.insert(data);
} catch (error) {
if (error instanceof DataServiceError) {
console.error('Error code:', error.code);
console.error('Message:', error.message);
console.error('HTTP status:', error.statusCode);
// Handle specific errors
switch (error.code) {
case '23505':
console.log('Duplicate collection name - use a different name');
break;
case 'PGRST301':
console.log('Authentication failed - check your token');
break;
default:
console.log('Unexpected error:', error.message);
}
} else {
console.error('Unknown error:', error);
}
}
`
Common error codes:
- 23505: Unique constraint violation (duplicate collection name)PGRST301
- : JWT authentication failedPGRST204
- : No rows returnedPGRST116
- : Invalid query syntax
The SDK is built on PostgreSQL Row-Level Security (RLS):
- JWT Authentication: User identity from JWT token payload
- Automatic Isolation: Each user can only access their own data
- No Admin Bypass: Even database admins respect RLS policies
- Secure by Default: No configuration needed
Your JWT token must include a user_id claim:
`json`
{
"user_id": "user_f4ed2364ffcf24d6c6707d5ca5e4fe6d",
"exp": 1735689600
}
`bashClone repository
git clone https://github.com/seaverse/dataservice
cd dataservice
$3
`bash
Copy environment template
cp .env.example .envConfigure your credentials
POSTGREST_URL=https://your-api.example.com
JWT_TOKEN=Bearer your-token-here
Run tests
npm run test:examples
`$3
-
npm run build - Build the SDK
- npm run dev - Build in watch mode
- npm run test:examples - Run integration tests
- npm run lint - Lint code with ESLint
- npm run format` - Format code with PrettierMIT