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

v1.8.0

Published

AI-Friendly Universal Data Storage SDK for TypeScript/JavaScript

Readme

@seaverse/dataservice

A TypeScript/JavaScript SDK for universal data storage with PostgREST backend. Store and query JSON data with automatic user isolation and type safety.

Features

  • 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

Installation

npm install @seaverse/dataservice

Package Exports

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<T> - Type of stored records
  • Collection<T> - Type of collection instances
  • QueryBuilder<T> - Type of query builder instances
  • DataTable - Type of data table instances
  • ClientConfig - Configuration options for createClient
  • DataServiceError - 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.

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

Quick Start

Creating a Client

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.

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({});

Basic Operations

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

Core Concepts

Data Hierarchy

DataServiceClient
  └── DataTable (e.g., userData)
      └── Collection (e.g., "orders")
          └── DataRecord (JSONB data + metadata)

Application ID

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

export SEAVERSE_APP_ID=my-app-id

Access the extracted ID:

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():

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

Data Tables

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

Collections

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:

// ✓ 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_<timestamp>_0, order_<timestamp>_1, order_<timestamp>_2

// ✗ 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 record
  • For multiple records, use patterns like: order_${orderId}, msg_${conversationId}_${msgId}

⚠️ Common Pitfalls

Pitfall #1: Treating Collections as Containers

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

Pitfall #2: Expecting Query Methods to Return Multiple Records

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

Pitfall #3: Using Plural Names for Collections

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

💡 Best Practices

Pattern 1: Single Record per Concept

Use this for user preferences, settings, profiles - things where you only need one record:

// 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://...',
});

Pattern 2: ID-Based Collection Names

Use this for multiple items of the same type:

// Multiple orders - each with unique collection name
const order1 = await client.userData.collection(`order_${orderId1}`).insert(orderData1);
const order2 = await client.userData.collection(`order_${orderId2}`).insert(orderData2);

// Multiple conversations
const conv1 = await client.userData.collection(`conv_${convId1}`).insert(convData1);
const conv2 = await client.userData.collection(`conv_${convId2}`).insert(convData2);

Pattern 3: Batch Insert for Multiple Records

Use this when creating multiple records at once:

// 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_<timestamp>_0, order_<timestamp>_1, order_<timestamp>_2

Pattern 4: Hierarchical Collection Names

Use this for nested data structures:

// Messages within a conversation
await client.userData.collection(`conv_${convId}_msg_1`).insert(message1);
await client.userData.collection(`conv_${convId}_msg_2`).insert(message2);

// Tasks within a project
await client.userData.collection(`project_${projId}_task_1`).insert(task1);
await client.userData.collection(`project_${projId}_task_2`).insert(task2);

Data Records

Each record contains:

interface DataRecord<T> {
  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
}

API Reference

Client Creation

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<string, string>; // Additional headers
  };
});

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' } }
  • Fallback: If fetch fails, defaults to https://dataservice-api.seaverse.ai
  • 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:

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

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:

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

DataTable Methods

// Get a collection
client.userData.collection<T>(name: string): Collection<T>

// Batch insert multiple records
client.userData.batchInsert<T>(
  baseName: string,
  records: T[]
): Promise<DataRecord<T>[]>

Collection Methods

Create

// Insert with auto-generated UUID
collection.insert(data: T): Promise<DataRecord<T>>

// Insert with custom UUID
collection.insert(data: T, id: string): Promise<DataRecord<T>>

Read

// Get by ID
collection.get(id: string): Promise<DataRecord<T> | null>
collection.selectById(id: string): Promise<DataRecord<T> | null>

// Query builder
collection.select(): QueryBuilder<T>

// Search by criteria
collection.search(criteria: Partial<T>): Promise<DataRecord<T>[]>

// Count records
collection.count(): Promise<number>

Update

// Full update (replaces entire data object)
collection.update(id: string, data: T): Promise<DataRecord<T>>

// Partial update (merges with existing data)
collection.patch(id: string, partial: Partial<T>): Promise<DataRecord<T>>

Delete

// Delete a record by ID (permanent deletion)
collection.delete(id: string): Promise<void>

Query Builder

Build complex queries with a fluent API:

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<DataRecord<T>[]>
  .count(): Promise<number>

Field syntax for JSONB queries:

  • Use data->>field for text comparison: .eq('data->>status', 'pending')
  • Use data->field for numeric comparison: .gt('data->total', '100')

Client Utility Methods

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

Examples

E-Commerce Orders

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()}`;
const order1 = await client.userData.collection<Order>(`order_${orderNumber1}`).insert({
  order_number: orderNumber1,
  customer_email: '[email protected]',
  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}`;
const order2 = await client.userData.collection<Order>(`order_${orderNumber2}`).insert({
  order_number: orderNumber2,
  customer_email: '[email protected]',
  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

Chat Conversations

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<Conversation>(conversationId).insert({
  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<Conversation>(conversationId).get(conv.id);
if (current) {
  await client.userData.collection<Conversation>(conversationId).patch(conv.id, {
    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);

User Preferences

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<UserPreferences>('preferences');

// 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' });

UUID Primary Keys

All records use UUID strings as primary keys.

Auto-generated UUIDs

const order = await orders.insert({
  order_number: 'ORD-001',
  status: 'pending',
});

console.log(order.id); // "550e8400-e29b-41d4-a716-446655440000"

Custom UUIDs

Useful for offline-first applications or client-side ID generation:

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

TypeScript Support

Full type safety with generic types:

type Product = {
  name: string;
  price: number;
  in_stock: boolean;
};

const products = client.userData.collection<Product>('products');

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

Error Handling

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 failed
  • PGRST204: No rows returned
  • PGRST116: Invalid query syntax

Security

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:

{
  "user_id": "user_f4ed2364ffcf24d6c6707d5ca5e4fe6d",
  "exp": 1735689600
}

Development

Setup

# Clone repository
git clone https://github.com/seaverse/dataservice
cd dataservice

# Install dependencies
npm install

# Build
npm run build

Testing

# Copy environment template
cp .env.example .env

# Configure your credentials
# POSTGREST_URL=https://your-api.example.com
# JWT_TOKEN=Bearer your-token-here

# Run tests
npm run test:examples

Scripts

  • 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 Prettier

License

MIT

Links