Client SDK
The official TypeScript client for ObjectStack — auto-discovery, typed metadata, CRUD, batch operations, and service-aware feature detection.
Client SDK
The @objectstack/client is the official TypeScript client for ObjectStack. It provides a typed, protocol-aware interface that automatically adapts to your server's available services.
Features
- Auto-Discovery: Connects to your ObjectStack server and discovers available services, routes, and capabilities
- Service-Aware: Checks
discovery.servicesto determine which features are available before calling them - Typed Metadata: Retrieve Object and View definitions with full type support
- Metadata Caching: ETag-based conditional requests for efficient metadata caching
- Unified Data Access: Simple CRUD operations for any object in your schema
- Batch Operations: Efficient bulk create/update/upsert/delete with transaction support
- Query Builder: Programmatic query construction with
createQuery()andcreateFilter() - Standardized Errors: Machine-readable error codes with retry guidance
- Protocol Compliant: Implements the core API namespaces defined in
@objectstack/spec, plus additional namespaces (approvals, feed, organizations, projects/environments)
Installation
pnpm add @objectstack/clientQuick Start
import { ObjectStackClient } from '@objectstack/client';
const client = new ObjectStackClient({
baseUrl: 'http://localhost:3004',
});
async function main() {
// 1. Connect — resolves with the discovery manifest
const discovery = await client.connect();
// 2. Check available services
console.log('Services:', discovery.services);
// → { metadata: { enabled: true, status: 'degraded' }, data: { enabled: true, status: 'available' }, auth: { enabled: false, ... } }
// 3. Query data
const tasks = await client.data.find('todo_task', {
fields: ['subject', 'priority'],
where: { priority: { $gte: 2 } },
orderBy: ['-priority'],
limit: 10
});
// 4. Create a record
const newTask = await client.data.create('todo_task', {
subject: 'New Task',
priority: 1
});
// 5. Batch operations
const result = await client.data.batch('todo_task', {
operation: 'update',
records: [
{ id: '1', data: { status: 'active' } },
{ id: '2', data: { status: 'active' } }
],
options: { atomic: true, returnRecords: true }
});
console.log(`Updated ${result.succeeded} records`);
}Auto-Discovery
When you call client.connect(), the client:
- Probes
/api/v1/discoveryfirst, then falls back to{origin}/.well-known/objectstack - Parses the discovery response including
routes,features, andservices - Configures all API route paths dynamically
// connect() resolves with the discovery manifest — capture the return value
const discovery = await client.connect();
console.log(discovery.version); // "1.0.0"
console.log(discovery.environment); // "development"
// Check if a service is available before using it
if (discovery.services?.auth?.enabled) {
// Auth plugin is installed — login is available
await client.auth.login({ email: 'admin@example.com', password: 'secret' });
} else {
console.log(discovery.services?.auth?.message);
// → "Install an auth plugin to enable"
}Plugin-driven services: Auth, workflow, automation, AI, and other services are only available when the corresponding plugin is installed. Always check discovery.services before calling plugin-provided API methods.
Protocol Coverage
The @objectstack/client SDK aims to implement the ObjectStack API protocol specification. The core namespaces are listed below; the client also exposes additional namespaces (approvals, feed, organizations, oauth, projects/environments) — see index.ts for the full surface:
| Namespace | Status | Methods | Purpose |
|---|---|---|---|
| discovery | ✅ | 1 | API version & capabilities detection |
| meta | ✅ | 7 | Metadata operations (objects, views, plugins) |
| data | ✅ | 10 | CRUD & query operations |
| auth | ✅ | 5 | Authentication & user management |
| permissions | ✅ | 3 | Access control checks |
| packages | ✅ | 6 | Plugin/package lifecycle management |
| views | ✅ | 5 | UI view definitions |
| workflow | ✅ | 3 | Workflow state transitions |
| analytics | ✅ | 3 | Analytics queries |
| automation | ✅ | 1 | Automation triggers |
| storage | ✅ | 2 | File upload & download |
| i18n | ✅ | 3 | Internationalization |
| notifications | 🟡 | 7 | Legacy notification helpers; receipt/inbox cut-over pending |
| realtime | ✅ | 6 | Connection/subscription/presence calls (server transport is plugin-provided) |
| ai | ✅ | 3 | AI services (NLQ, suggest, insights) |
Protocol compliance & verification: See CLIENT_SPEC_COMPLIANCE.md for detailed method-by-method verification and CLIENT_SERVER_INTEGRATION_TESTS.md for comprehensive integration test specifications.
API Namespaces
client.meta — Metadata
// Get object schema (getItem('object', name) returns the object definition)
const accountSchema = await client.meta.getItem('object', 'account');
console.log(accountSchema.fields);
// List all metadata types
const types = await client.meta.getTypes();
// → { types: ['object', 'view', 'plugin', ...] }
// Get with ETag caching
const cached = await client.meta.getCached('account', {
ifNoneMatch: '"686897696a7c876b7e"'
});
if (cached.notModified) {
console.log('Using cached metadata');
}
// Get auto-generated view
const listView = await client.meta.getView('account', 'list');client.data — CRUD & Batch
// Find with query options
const accounts = await client.data.find('account', {
fields: ['name', 'industry', 'revenue'],
where: { industry: 'Technology' },
orderBy: ['-revenue'],
limit: 20,
offset: 0,
});
// Get by ID
const account = await client.data.get('account', '123');
// Create
const created = await client.data.create('account', {
name: 'Acme Corp',
industry: 'Technology',
});
// Update (partial)
const updated = await client.data.update('account', '123', {
industry: 'Healthcare',
});
// Delete
await client.data.delete('account', '123');
// Batch operation (recommended for bulk)
const batchResult = await client.data.batch('account', {
operation: 'upsert',
records: [
{ id: '1', data: { name: 'Acme', status: 'active' } },
{ id: '2', data: { name: 'Beta', status: 'active' } },
],
options: {
atomic: true,
returnRecords: true,
continueOnError: false,
validateOnly: false,
},
});
// Convenience bulk methods
await client.data.createMany('account', [{ name: 'A' }, { name: 'B' }]);
await client.data.updateMany('account', [
{ id: '1', data: { status: 'closed' } },
{ id: '2', data: { status: 'closed' } },
]);
await client.data.deleteMany('account', ['1', '2', '3']);client.analytics — BI Queries
// Semantic analytics query (cube-style)
const result = await client.analytics.query({
cube: 'account',
measures: ['revenue.sum', 'count'],
dimensions: ['industry'],
where: { status: 'active' },
limit: 100,
});
// Get cube metadata for a specific cube
const meta = await client.analytics.meta('account');
// Explain a query plan
const explained = await client.analytics.explain({
cube: 'account',
measures: ['revenue.sum'],
});client.packages — Package Management
const packages = await client.packages.list();
await client.packages.install({ id: 'com.objectstack.plugin-auth', version: '1.0.0' });
await client.packages.enable('com.objectstack.plugin-auth');
await client.packages.disable('com.objectstack.plugin-auth');
await client.packages.uninstall('com.objectstack.plugin-auth');Additional Namespaces
The client also provides full implementations for:
// Auth — User authentication and session management
await client.auth.login({ email: 'user@example.com', password: 'pass' });
await client.auth.register({ email: 'new@example.com', password: 'pass', name: 'New User' });
await client.auth.me();
await client.auth.logout();
await client.auth.refreshToken('refresh-token-string');
// Permissions — Access control checks
await client.permissions.check({ object: 'account', action: 'create' });
await client.permissions.getObjectPermissions('account');
await client.permissions.getEffectivePermissions();
// Workflow — State machine management
await client.workflow.getConfig('approval');
await client.workflow.getState('approval', recordId);
await client.workflow.transition({ object: 'approval', recordId, transition: 'submit' });
// Approvals — request-based decision API (ADR-0019)
// Approval is a flow node, not a workflow step: decisions are keyed by request id.
await client.approvals.listRequests({ status: 'pending' }); // "my approvals" inbox
await client.approvals.getRequest(requestId);
await client.approvals.approve(requestId, { comment: 'Looks good' });
await client.approvals.reject(requestId, { comment: 'Incomplete' });
await client.approvals.listActions(requestId); // audit trail
// Realtime — WebSocket subscriptions
await client.realtime.connect({ transport: 'websocket' });
await client.realtime.subscribe({ channel: 'account', events: ['record.updated'] });
await client.realtime.unsubscribe('subscription-id');
await client.realtime.setPresence('account', { userId: 'user-123', status: 'online', lastSeen: new Date().toISOString() });
await client.realtime.getPresence('account');
await client.realtime.disconnect();
// Notifications — legacy helper surface
await client.notifications.registerDevice({ token: 'device-token', platform: 'ios' });
await client.notifications.unregisterDevice('device-id');
await client.notifications.getPreferences();
await client.notifications.updatePreferences({ email: true, push: false });
await client.notifications.list({ read: false });
await client.notifications.markRead(['notif-1', 'notif-2']);
await client.notifications.markAllRead();
// ADR-0030 note: the framework pipeline now materializes in-app messages
// through sys_inbox_message and tracks read-state in sys_notification_receipt.
// These helpers will be repointed during the objectui bell cut-over.
// AI — AI-powered features
await client.ai.nlq({ query: 'Show me all active accounts' });
await client.ai.suggest({ object: 'account', field: 'industry' });
await client.ai.insights({ object: 'sales', recordId: dealId });
// Note: conversational chat is no longer on the client — use the Vercel AI SDK
// (`useChat()` from `@ai-sdk/react`) directly against your chat endpoint.
// i18n — Internationalization
await client.i18n.getLocales();
await client.i18n.getTranslations('zh-CN');
await client.i18n.getFieldLabels('account', 'zh-CN');
// Automation — Trigger workflows and automations
await client.automation.trigger('send_welcome_email', { userId });
// Storage — File upload and management
await client.storage.upload(fileData, 'user');
await client.storage.getDownloadUrl('file-123');
// Views — UI view management
await client.views.list('account');
await client.views.get('account', viewId);
await client.views.create('account', { name: 'my_view', ... });
await client.views.update('account', viewId, { ... });
await client.views.delete('account', viewId);Service availability: Optional services (workflow, ai, etc.) are only available when the corresponding plugin is installed on the server. Always check the services map on the discovery result returned by client.connect() (cache it yourself — the client has no discovery getter) to verify service availability before calling these methods.
Query Builder
Build complex queries programmatically:
import { createQuery, createFilter } from '@objectstack/client';
const query = createQuery('account')
.select('name', 'industry', 'revenue')
.where((f) => {
f.equals('industry', 'Technology');
f.greaterThan('revenue', 10000);
})
.orderBy('revenue', 'desc')
.limit(50)
.build();
const results = await client.data.query('account', query);Filter Builder Methods
The FilterBuilder provides a rich set of filter methods:
import { createFilter } from '@objectstack/client';
const filter = createFilter()
.equals('status', 'active') // field = value
.notEquals('type', 'archived') // field != value
.greaterThan('revenue', 10000) // field > value
.lessThanOrEqual('age', 100) // field <= value
.in('category', ['A', 'B', 'C']) // field IN (...)
.like('name', '%Corp%') // field LIKE pattern
.contains('name', 'Corp') // LIKE %Corp%
.startsWith('name', 'Acme') // LIKE Acme%
.isNull('deleted_at') // field IS NULL
.between('created_at', '2024-01', '2024-12')
.build();Query Options
The find method accepts an options object with canonical (recommended) field names:
| Property | Type | Description | Example |
|---|---|---|---|
where | Object or FilterCondition | Filter conditions (WHERE clause) | { status: 'active' } |
fields | string[] | Fields to retrieve (SELECT) | ['name', 'email'] |
orderBy | string or string[] or SortNode[] | Sort order (ORDER BY) | ['-created_at'] |
limit | number | Max records to return (LIMIT) | 20 |
offset | number | Records to skip (OFFSET) | 0 |
expand | Record<string, any> or string[] | Relation loading (JOIN) | { owner: {} } |
Canonical names recommended: find() accepts the canonical names above; the legacy aliases (select, filter/filters, sort, top, skip) still work for backward compatibility, but new code should use the canonical names. (@objectstack/client-react's useQuery/useInfiniteQuery hooks are stricter — the legacy aliases were removed there in v11; see docs/upgrading-to-11.md.)
Batch Options
| Property | Type | Default | Description |
|---|---|---|---|
atomic | boolean | true | Rollback entire batch on any failure |
returnRecords | boolean | false | Include full records in response |
continueOnError | boolean | false | Continue after errors (when atomic=false) |
validateOnly | boolean | false | Dry-run mode — validate without persisting |
Error Handling
All errors follow a standardized format:
try {
await client.data.create('todo_task', { subject: '' });
} catch (error) {
console.error(error.code); // 'validation_error'
console.error(error.category); // 'validation'
console.error(error.httpStatus); // 400
console.error(error.retryable); // false
console.error(error.details); // { ... }
}Error Codes
| Code | HTTP | Category | Retryable | Description |
|---|---|---|---|---|
validation_error | 400 | validation | No | Input validation failed |
invalid_query | 400 | validation | No | Malformed query expression |
unauthenticated | 401 | authentication | No | Authentication required |
permission_denied | 403 | authorization | No | Insufficient permissions |
resource_not_found | 404 | not_found | No | Resource does not exist |
rate_limit_exceeded | 429 | rate_limit | Yes | Too many requests |
internal_error | 500 | server | Yes | Unexpected server error |
service_unavailable | 503 | server | Yes | Service temporarily unavailable |
not_implemented | 501 | server | No | Service not installed (plugin missing) |
Configuration
interface ClientConfig {
/** ObjectStack server URL */
baseUrl: string;
/** Bearer token for authentication (if auth plugin installed) */
token?: string;
/** Custom fetch implementation (e.g. for SSR or testing) */
fetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
/** Logger instance for debugging */
logger?: Logger;
/** Enable debug logging */
debug?: boolean;
/** Active project/environment id — injects an `X-Environment-Id` header for multi-tenant routing */
environmentId?: string;
/** Active UI locale (BCP-47, e.g. `'zh-CN'`) — injects an `Accept-Language` header; sync via `setLocale()` */
locale?: string;
}React Hooks
For React applications, use @objectstack/client-react:
pnpm add @objectstack/client-reactimport { ObjectStackProvider, useClient, useQuery } from '@objectstack/client-react';
import { ObjectStackClient } from '@objectstack/client';
// 1. Wrap your app with the provider
const client = new ObjectStackClient({ baseUrl: 'http://localhost:3004' });
function App() {
return (
<ObjectStackProvider client={client}>
<AccountList />
</ObjectStackProvider>
);
}
// 2. Use hooks in child components
function AccountList() {
const { data, isLoading, error, refetch } = useQuery('account', {
where: { status: 'active' },
orderBy: ['-created_at'],
limit: 20,
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return data?.records.map(a => <div key={a.id}>{a.name}</div>);
}Available Hooks
| Hook | Purpose |
|---|---|
useClient() | Access the ObjectStackClient instance from context |
useQuery(object, options) | Query data with auto caching and refetching |
useMutation(object) | Create/update/delete mutations |
usePagination(object, options) | Paginated data queries |
useInfiniteQuery(object, options) | Infinite scroll queries |
useObject(name) | Get object metadata |
useView(object, type) | Get view definition |
useFields(object) | Get field definitions |
useMetadata(type, name) | Get arbitrary metadata |
Testing
The client SDK includes comprehensive unit and integration tests to ensure reliability and protocol compliance.
Unit Tests
cd packages/client
pnpm testUnit tests use mocks to verify client behavior without requiring a server.
Integration Tests
Note: Integration tests require a running ObjectStack server. The server is provided by a separate repository and must be set up independently.
# Prerequisite: Start an ObjectStack server with test data
# For example, using the reference server repository
# Follow the server repository's documentation for local setup
# From this repository, run the integration test script
cd packages/client
pnpm test:integrationIntegration tests verify end-to-end communication with a live ObjectStack server across the client's API namespaces.
Test coverage: Integration test specifications cover discovery/connection, authentication, metadata operations, CRUD operations (basic, batch, advanced queries), permissions, workflow, realtime, notifications, AI services, i18n, analytics, packages, views, storage, and automation.
Protocol Compliance Documentation
For detailed information about the client's protocol implementation:
- Protocol Compliance Matrix — Method-by-method verification of all API methods across 13 namespaces
- Integration Test Specifications — Comprehensive test cases for client-server communication
- Package README — Developer navigation and API reference