Skip to content

Centrali JavaScript/TypeScript SDK

The Centrali SDK provides a simple, type-safe way to interact with Centrali's APIs from JavaScript or TypeScript applications. It handles authentication, request formatting, and response parsing automatically.

Try it now! Explore the SDK in our Live Playground on StackBlitz - no setup required.

Workspace slug terminology

The current SDK option is still named workspaceId, but the value you pass is always your workspace slug such as acme-corp, not an internal UUID. The examples on this page keep the current option name so they match the SDK surface.

Choose a Path

If you need to... Start here Why
Get a basic client working quickly Quick Start Fastest way to create a client and make requests
Pick the right auth model SDK Authentication Best overview of service accounts, publishable keys, and BYOT
Connect AI clients through MCP MCP Server Separate setup path from direct SDK usage
Subscribe to live record updates Realtime Events Realtime has its own connection model and event types
Find route families across services API Overview Useful when you need to compare SDK behavior to raw HTTP routes

This page is the broad SDK reference. If you only need auth or MCP setup, jump to those focused pages instead of reading the whole SDK reference front to back.

Installation

npm install @centrali-io/centrali-sdk
# or
yarn add @centrali-io/centrali-sdk

Quick Start

import { CentraliSDK } from '@centrali-io/centrali-sdk';

// Initialize the SDK
const centrali = new CentraliSDK({
  baseUrl: 'https://api.centrali.io',
  workspaceId: 'your-workspace-slug',  // current SDK option name; pass the workspace slug

  // Option 1: Use an existing token (from user login)
  token: 'your-bearer-token',

  // Option 2: Use service account credentials (auto-fetches token)
  clientId: 'your-client-id',
  clientSecret: 'your-client-secret'
});

// Create a record
const product = await centrali.createRecord('Product', {
  name: 'Wireless Headphones',
  price: 99.99,
  inStock: true
});

console.log('Created product:', product.data);

Authentication

The SDK supports three authentication methods:

1. Bearer Token (User Authentication)

For user-authenticated requests, provide a JWT token:

const centrali = new CentraliSDK({
  baseUrl: 'https://api.centrali.io',
  workspaceId: 'your-workspace-slug',  // current SDK option name; pass the workspace slug
  token: 'jwt-token-from-login'
});

// Update the token later if needed
centrali.setToken('new-jwt-token');

2. Client Credentials (Service Account)

For server-to-server communication, use service account credentials:

const centrali = new CentraliSDK({
  baseUrl: 'https://api.centrali.io',
  workspaceId: 'your-workspace-slug',  // current SDK option name; pass the workspace slug
  clientId: process.env.CENTRALI_CLIENT_ID,
  clientSecret: process.env.CENTRALI_CLIENT_SECRET
});

// The SDK automatically fetches and manages the token
// You can also manually fetch a token:
const token = await centrali.fetchServiceAccountToken();

3. Publishable Key or Dynamic Token Callback

Use publishableKey for browser-safe access, or getToken when your app already manages user tokens through Clerk, Auth0, or another provider.

const browserClient = new CentraliSDK({
  baseUrl: 'https://centrali.io',
  workspaceId: 'your-workspace-slug', // current SDK option name; pass the workspace slug
  publishableKey: 'pk_live_your_key_here'
});

const byotClient = new CentraliSDK({
  baseUrl: 'https://centrali.io',
  workspaceId: 'your-workspace-slug', // current SDK option name; pass the workspace slug
  getToken: async () => await clerk.session.getToken()
});

API Reference

Initialization Options

interface CentraliSDKOptions {
  /** Base URL of the Centrali API */
  baseUrl: string;

  /** Your workspace slug */
  workspaceId: string;

  /** Optional bearer token for user authentication */
  token?: string;

  /** Optional browser-safe publishable key */
  publishableKey?: string;

  /** Optional dynamic token callback for BYOT auth */
  getToken?: () => Promise<string>;

  /** Optional service account client ID */
  clientId?: string;

  /** Optional service account client secret */
  clientSecret?: string;

  /** Optional custom axios configuration */
  axiosConfig?: AxiosRequestConfig;
}

Records Management

Create a Record

const record = await centrali.createRecord('CollectionName', {
  field1: 'value1',
  field2: 123,
  nested: {
    subField: 'value'
  }
});

console.log('Created record ID:', record.id);
Create with TTL
// Expire after 1 hour
const session = await centrali.createRecord('Sessions', {
  userId: 'user-123',
  token: 'abc-xyz',
}, { ttlSeconds: 3600 });

console.log(session.data.expiresAt); // ISO timestamp ~1 hour from now

// Expire at a specific date
const promo = await centrali.createRecord('Promotions', {
  code: 'SUMMER2026',
  discount: 20,
}, { expiresAt: '2026-09-01T00:00:00Z' });

The third argument accepts RecordTtlOptions:

Option Type Description
ttlSeconds number Seconds until expiration
expiresAt string ISO 8601 expiration timestamp

If neither is set, the record inherits the collection's defaultTtlSeconds (if configured). Note: clearTtl is only valid when updating an existing record and has no effect on creation.

Get a Record

const record = await centrali.getRecord('CollectionName', 'record-id');
console.log('Record data:', record.data);

Update a Record

const updated = await centrali.updateRecord('CollectionName', 'record-id', {
  field1: 'new value',
  field2: 456
});

console.log('Updated record:', updated.data);
Update TTL
// Extend session by 2 hours
await centrali.updateRecord('Sessions', 'record-id', {}, { ttlSeconds: 7200 });

// Remove TTL (make permanent)
await centrali.updateRecord('Sessions', 'record-id', {}, { clearTtl: true });

The fourth argument accepts RecordTtlOptions with an additional clearTtl option.

Upsert a Record

Atomically create or update a record based on match fields. Returns the record and whether it was created or updated.

const result = await centrali.upsertRecord('Product', {
  match: { sku: 'WDG-001' },
  data: {
    sku: 'WDG-001',
    name: 'Premium Widget',
    price: 29.99,
    inStock: true
  }
});

console.log(result.operation); // 'created' or 'updated'
console.log(result.data);      // the record

The match object contains the business key fields used to find an existing record. The data object contains the full record payload for create, or the fields to update. Match field values are protected from being overwritten on update.

This is safe for concurrent calls — the backend uses advisory locking to serialize requests with the same match criteria.

Delete a Record

await centrali.deleteRecord('CollectionName', 'record-id');
console.log('Record deleted');

Query Records

Use the records namespace for new query code. It gives you one consistent query model across ad-hoc filters, text search, and validation:

  • centrali.records.query(resource, definition) — canonical POST /records/query
  • centrali.records.list(resource, urlOptions) — simple GET-style adapter
  • centrali.records.search(resource, text, options?) — text-search convenience
  • centrali.records.test(resource, definition) — authoring-time validation
  • centrali.queryRecords(resource, definition) — top-level convenience for canonical queries

Canonical queries use:

  • resource for the collection slug
  • where with bare operators like eq, gte, and contains
  • sort as an array of { field, direction }
  • page as { limit, offset? }
  • select.fields for projections
  • text for full-text search
const orders = await centrali.records.query('orders', {
  where: {
    and: [
      { 'data.status': { eq: 'paid' } },
      { 'data.amount': { gte: 100 } }
    ]
  },
  sort: [{ field: 'createdAt', direction: 'desc' }],
  page: { limit: 25 },
  select: { fields: ['id', 'data.amount', 'data.status'] }
});

console.log(orders.data);
console.log(orders.meta.hasMore);

Simple list-style queries still work well through records.list():

const recent = await centrali.records.list('orders', {
  'data.status': 'paid',
  sort: '-createdAt',
  pageSize: 25
});

Text search stays on the same model:

const matches = await centrali.records.search('products', 'wireless headphones', {
  where: {
    'data.inStock': { eq: true }
  },
  page: { limit: 10 }
});

Validate Queries Locally (client.query)

The SDK bundles the same query primitives that run server-side, exposed at client.query. Use them to catch malformed queries before sending them, to migrate older saved-query bodies forward, or to convert GET-style URL state into the canonical filter/sort/page/select shape.

Method Use when
client.query.validate(definition, options?) Validate a QueryDefinition locally before issuing a request
client.query.translateLegacy(legacyBody, options?) Convert a legacy { collection, $eq, ... } body into canonical
client.query.parseUrl(params, options) Parse GET-adapter params (page, pageSize, sort, fields, filters) into { query, searchTerm? }
client.query.errorsToHttp(errors) Convert a QueryError[] into the same { status, code, message, errors } envelope the server returns
Validate

validate returns a discriminated ValidationResult{ ok: true, value } on success, { ok: false, errors } on failure. The output is byte-for-byte identical to the server's validator, so a clean local pass guarantees the body will not be rejected for shape reasons.

const result = centrali.query.validate({
  resource: 'orders',
  where: { 'data.status': { eq: 'paid' } },
  page: { limit: 50 },
});

if (!result.ok) {
  for (const err of result.errors) {
    console.log(err.code, err.path, err.message);
  }
  return;
}

const orders = await centrali.records.query('orders', result.value);

result.value is a full QueryDefinition, including resource, so you can pass it directly to records.query() or queryRecords().

Translate a legacy query

Useful when carrying older saved-query bodies forward, or when integrating with code that still emits the legacy operator vocabulary ($eq, $gte, regex, etc.). Warnings flag any operator that was renamed, dropped, or auto-translated.

const t = centrali.query.translateLegacy({
  collection: 'orders',
  'data.status': { $eq: 'paid' },
  limit: 25,
});

if (t.ok) {
  console.log(t.value.query);     // canonical QueryDefinition
  console.log(t.value.warnings);  // [{ kind: 'operator_renamed', path: ..., from: ..., to: ... }, ...]
}
Parse a URL query

Mirrors the server-side GET adapter for filters, sorting, pagination, and projection. It returns { query, searchTerm? }: query is canonical, while search stays separate because the list surface still handles it outside the text clause.

const parsed = centrali.query.parseUrl(
  { 'data.status': 'paid', sort: '-createdAt', page: '2', pageSize: '25' },
  { resource: 'orders' }
);

if (parsed.ok) {
  const orders = await centrali.records.query('orders', parsed.value.query);
}

If the incoming params include search=..., read parsed.value.searchTerm separately.

Convert errors to the HTTP envelope

When you want a thrown error to match what the server would have sent, run validation errors through errorsToHttp:

const result = centrali.query.validate(definition);
if (!result.ok) {
  const http = centrali.query.errorsToHttp(result.errors);
  throw new Error(`${http.status} ${http.code}: ${http.message}`);
}

Bulk Operations

Get multiple records by their IDs:

const recordIds = ['rec-1', 'rec-2', 'rec-3'];
const records = await centrali.getRecordsByIds('Product', recordIds);

console.log(`Retrieved ${records.data.length} records`);

Collections Management

Manage collections (formerly known as structures) programmatically via client.collections. This enables infrastructure-as-code workflows for defining and versioning your data models.

List Collections

const result = await centrali.collections.list();

// With pagination
const page2 = await centrali.collections.list({ page: 2, limit: 10 });

Get a Collection

// By ID
const structure = await centrali.collections.get('collection-uuid');

// By slug
const structure = await centrali.collections.getBySlug('orders');
console.log('Properties:', structure.data.properties);

Create a Collection

const structure = await centrali.collections.create({
  name: 'Orders',
  slug: 'orders',
  description: 'Customer orders',
  properties: [
    { name: 'title', type: 'string', required: true, maxLength: 200 },
    { name: 'amount', type: 'number', minimum: 0 },
    { name: 'status', type: 'string', enum: ['pending', 'completed', 'cancelled'] },
    { name: 'tags', type: 'array', items: { type: 'string' } },
    {
      name: 'customer',
      type: 'reference',
      target: 'customers',
      relationship: 'many-to-one',
      onDelete: 'restrict'
    },
  ],
  enableVersioning: true,
  schemaDiscoveryMode: 'strict',
  tags: ['core'],
});

Update a Collection

const updated = await centrali.collections.update('collection-uuid', {
  name: 'Updated Orders',
  properties: [
    { name: 'title', type: 'string', required: true },
    { name: 'amount', type: 'number', minimum: 0 },
    { name: 'priority', type: 'number' },  // new field
  ],
});
Set Default TTL
// All new records in this collection expire after 24 hours
await centrali.collections.update('collection-uuid', {
  defaultTtlSeconds: 86400,
});

// Remove default TTL
await centrali.collections.update('collection-uuid', {
  defaultTtlSeconds: null,
});

Delete a Collection

await centrali.collections.delete('collection-uuid');

Validate a Collection

Check slug uniqueness and property validity before creating:

const result = await centrali.collections.validate({
  slug: 'orders',
  properties: [{ name: 'title', type: 'string' }],
});

Property Types

Type Key Fields
string minLength, maxLength, pattern, renderAs, isSecret
number minimum, maximum, multipleOf, expression, autoIncrement
boolean
datetime earliestDate, latestDate, expression
array items, minItems, maxItems, uniqueItems, itemSchema
object properties, requiredProperties, strictProperties
reference target, relationship, onDelete, displayField

Compute Functions

Invoke an On-Demand Trigger

Execute compute logic through an on-demand trigger:

const job = await centrali.triggers.invoke('trigger-id', {
  payload: {
    productId: 'prod-123',
    quantity: 2,
    couponCode: 'SAVE20'
  }
});

console.log('Queued job:', job.data);

Manage Compute Functions

Manage compute function definitions programmatically via client.functions. This enables CI/CD deployment of function code.

// List all functions
const fns = await centrali.functions.list();
const searched = await centrali.functions.list({ search: 'order', limit: 10 });

// Get a function by ID
const fn = await centrali.functions.get('function-uuid');
console.log('Code:', fn.data.code);

// Create a function
const fn = await centrali.functions.create({
  name: 'Process Order',
  slug: 'process-order',
  code: `async function run() {
    const order = await api.fetchRecord('orders', triggerParams.orderId);
    return { processed: true, total: order.data.amount };
  }`,
  description: 'Processes incoming orders',
  timeout: 60000,
});

// Update a function
const updated = await centrali.functions.update('function-uuid', {
  code: 'async function run() { return { v2: true }; }',
  timeout: 120000,
});

// Delete a function
await centrali.functions.delete('function-uuid');

Test Execute (Without Saving)

Validate function code before deploying:

const result = await centrali.functions.testExecute({
  code: `async function run() {
    return { sum: executionParams.a + executionParams.b };
  }`,
  params: { a: 1, b: 2 },
});

console.log('Output:', result.data.output);        // { sum: 3 }
console.log('Duration:', result.data.duration_ms);  // e.g., 45
console.log('Logs:', result.data.logs);             // console.log output
console.log('Success:', result.data.success);       // true

Triggers

Manage and invoke compute function triggers via client.triggers.

List Triggers

// List on-demand triggers only (default filter)
const onDemand = await centrali.triggers.list();

// List ALL triggers (any execution type)
const all = await centrali.triggers.listAll();

// Filter by execution type
const scheduled = await centrali.triggers.listAll({ executionType: 'scheduled' });

// Include health metrics
const withHealth = await centrali.triggers.listAll({ includeHealth: true });

Get a Trigger

// Get on-demand trigger (validates type)
const trigger = await centrali.triggers.get('trigger-id');

// Get any trigger type (no type restriction)
const trigger = await centrali.triggers.getDetails('trigger-id');

// With health metrics
const trigger = await centrali.triggers.getDetails('trigger-id', { includeHealth: true });

Invoke a Trigger

const result = await centrali.triggers.invoke('trigger-id', {
  payload: { orderId: 'order-456', action: 'process' },
});
// result.data → queued job ID

Create a Trigger

// Event-driven trigger
const trigger = await centrali.triggers.create({
  name: 'On Order Created',
  functionId: 'function-uuid',
  executionType: 'event-driven',
  triggerMetadata: { event: 'record.created', recordSlug: 'orders' },
});

// Scheduled trigger (cron)
const scheduled = await centrali.triggers.create({
  name: 'Daily Report',
  functionId: 'function-uuid',
  executionType: 'scheduled',
  triggerMetadata: {
    scheduleType: 'cron',
    cronExpression: '0 9 * * *',
    timezone: 'America/New_York',
  },
});

// On-demand trigger
const manual = await centrali.triggers.create({
  name: 'Manual Process',
  functionId: 'function-uuid',
  executionType: 'on-demand',
});

Update a Trigger

const updated = await centrali.triggers.update('trigger-uuid', {
  name: 'Updated Trigger',
  enabled: false,
  triggerMetadata: { cronExpression: '0 10 * * *' },
});

Delete a Trigger

await centrali.triggers.delete('trigger-uuid');

Pause / Resume a Trigger

await centrali.triggers.pauseTrigger('trigger-id');
await centrali.triggers.resumeTrigger('trigger-id');

Saved Queries

Saved queries are reusable named query definitions stored in Centrali. Use them when the same query should be executed by multiple clients or with runtime variables.

List Saved Queries

const all = await centrali.savedQueries.listAll();
const orderQueries = await centrali.savedQueries.list('orders');

Get a Saved Query

const query = await centrali.savedQueries.get('orders', 'query-uuid');
const byName = await centrali.savedQueries.getByName('orders', 'High Value Paid Orders');

Execute a Saved Query

const result = await centrali.savedQueries.execute('orders', 'query-uuid', {
  variables: {
    minimumAmount: 250,
  },
});

console.log(result.data);

For canonical create, update, and test flows, use the console or the HTTP routes documented in Saved Queries.

File Uploads

Upload files to Centrali's storage service:

// In a browser environment
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const file = fileInput.files[0];

// Upload to default location (/root/shared)
const uploadResult = await centrali.uploadFile(file);

// Upload to a specific folder (folder must exist)
const uploadResult = await centrali.uploadFile(
  file,
  '/root/shared/product-images',  // Full path to target folder
  true                             // Make publicly accessible
);

console.log('File URL:', uploadResult.data);

Upload Parameters

Parameter Required Default Description
file Yes - File object to upload
location No /root/shared Target folder path (must be under /root/)
isPublic No false If true, file is publicly accessible

Getting File URLs

After uploading, use the render ID to get URLs for displaying or downloading the file:

// Upload a file
const result = await centrali.uploadFile(file, '/root/shared/images');
const renderId = result.data;  // e.g., "kvHJ4ipZ3Q6EAoguKrWmU7KYyDHcU03C"

// Get URL for displaying inline (e.g., in an <img> tag)
const renderUrl = centrali.getFileRenderUrl(renderId);

// Get URL with image transformations (resize, compress, convert)
const thumbnailUrl = centrali.getFileRenderUrl(renderId, { width: 200 });
const optimizedUrl = centrali.getFileRenderUrl(renderId, {
  width: 800,
  quality: 60,
  format: 'webp'
});

// Get URL for downloading as attachment
const downloadUrl = centrali.getFileDownloadUrl(renderId);

Important Notes

  • Paths must be absolute: Use full paths like /root/shared/images, not just images
  • Folders must exist: Create folders via the console or API before uploading to them
  • Only use /root/: The /system/ and /support/ folders are reserved for system use

Complete Examples

E-commerce Product Catalog

import { CentraliSDK } from '@centrali-io/centrali-sdk';

class ProductCatalog {
  private centrali: CentraliSDK;

  constructor() {
    this.centrali = new CentraliSDK({
      baseUrl: 'https://api.centrali.io',
      workspaceId: 'my-store', // current SDK option name; pass the workspace slug
      clientId: process.env.CENTRALI_CLIENT_ID,
      clientSecret: process.env.CENTRALI_CLIENT_SECRET
    });
  }

  // Create a new product
  async createProduct(productData: any) {
    const product = await this.centrali.createRecord('Product', {
      ...productData,
      createdAt: new Date().toISOString()
    });

    // Trigger inventory update via an on-demand trigger
    await this.centrali.triggers.invoke('update-inventory-trigger-id', {
      payload: {
        productId: product.id,
        action: 'initialize',
        quantity: productData.initialStock || 0
      }
    });

    return product;
  }

  // Search products by category
  async searchProducts(category?: string) {
    const filter: Record<string, any> = {};
    if (category) {
      filter.category = category;
    }

    // Filters passed at top level with 'data.' prefix
    return await this.centrali.queryRecords('Product', {
      ...filter,  // spread filter params at top level
      sort: '-popularity',
      pageSize: 20
    });
  }

  // Get product with reviews
  async getProductWithReviews(productId: string) {
    // Get product
    const product = await this.centrali.getRecord('Product', productId);

    // Get reviews (filter at top level)
    const reviews = await this.centrali.queryRecords('Review', {
      'data.productId': productId,
      sort: '-createdAt',
      pageSize: 10
    });

    return {
      ...product.data,
      reviews: reviews.data
    };
  }

  // Update inventory
  async updateInventory(productId: string, quantity: number, operation: 'add' | 'subtract') {
    return await this.centrali.triggers.invoke('update-inventory-trigger-id', {
      payload: {
        productId,
        quantity,
        operation
      }
    });
  }
}

// Usage
const catalog = new ProductCatalog();

// Create a product
const product = await catalog.createProduct({
  name: 'Wireless Mouse',
  description: 'Ergonomic wireless mouse with 6 buttons',
  price: 29.99,
  category: 'Electronics',
  initialStock: 100
});

// Search products
const searchResults = await catalog.searchProducts('wireless', 'Electronics');

User Authentication Flow

import { CentraliSDK } from '@centrali-io/centrali-sdk';

class AuthService {
  private centrali: CentraliSDK;

  constructor() {
    this.centrali = new CentraliSDK({
      baseUrl: 'https://api.centrali.io',
      workspaceId: 'my-app' // current SDK option name; pass the workspace slug
    });
  }

  // Register a new user
  async register(email: string, password: string, profile: any) {
    // Create user record
    const user = await this.centrali.createRecord('User', {
      email,
      profile,
      createdAt: new Date().toISOString()
    });

    // Trigger welcome email via an on-demand trigger
    await this.centrali.triggers.invoke('send-welcome-email-trigger-id', {
      payload: {
        userId: user.id,
        email
      }
    });

    return user;
  }

  // Login (assuming you have a login endpoint that returns a JWT)
  async login(email: string, password: string): Promise<string> {
    // This would typically call your auth endpoint
    const response = await fetch('https://auth.centrali.io/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password })
    });

    const { token } = await response.json();

    // Set the token for future SDK calls
    this.centrali.setToken(token);

    return token;
  }

  // Get user profile
  async getUserProfile(userId: string) {
    return await this.centrali.getRecord('User', userId);
  }

  // Update user preferences
  async updatePreferences(userId: string, preferences: any) {
    return await this.centrali.updateRecord('User', userId, {
      preferences,
      updatedAt: new Date().toISOString()
    });
  }
}

Real-time Data Sync

import { CentraliSDK } from '@centrali-io/centrali-sdk';

class DataSyncService {
  private centrali: CentraliSDK;
  private syncInterval: NodeJS.Timer | null = null;

  constructor() {
    this.centrali = new CentraliSDK({
      baseUrl: 'https://api.centrali.io',
      workspaceId: 'my-app', // current SDK option name; pass the workspace slug
      clientId: process.env.CENTRALI_CLIENT_ID,
      clientSecret: process.env.CENTRALI_CLIENT_SECRET
    });
  }

  // Sync local data with Centrali
  async syncData(localData: any[]) {
    const results = {
      created: 0,
      updated: 0,
      errors: []
    };

    for (const item of localData) {
      try {
        if (item.centraliId) {
          // Update existing record
          await this.centrali.updateRecord('SyncedData', item.centraliId, item);
          results.updated++;
        } else {
          // Create new record
          const record = await this.centrali.createRecord('SyncedData', item);
          item.centraliId = record.id; // Store ID for future syncs
          results.created++;
        }
      } catch (error) {
        results.errors.push({ item, error: error.message });
      }
    }

    return results;
  }

  // Poll for changes (filter at top level with bracket notation)
  async pollChanges(since: Date, callback: (changes: any[]) => void) {
    const changes = await this.centrali.queryRecords('SyncedData', {
      'updatedAt[gt]': since.toISOString(),
      sort: 'updatedAt',
      pageSize: 100
    });

    if (changes.data.length > 0) {
      callback(changes.data);
    }

    return changes.data;
  }

  // Start continuous sync
  startSync(interval: number = 30000) {
    let lastSync = new Date();

    this.syncInterval = setInterval(async () => {
      try {
        await this.pollChanges(lastSync, (changes) => {
          console.log(`Received ${changes.length} changes`);
          // Process changes
        });
        lastSync = new Date();
      } catch (error) {
        console.error('Sync error:', error);
      }
    }, interval);
  }

  // Stop sync
  stopSync() {
    if (this.syncInterval) {
      clearInterval(this.syncInterval);
      this.syncInterval = null;
    }
  }
}

Error Handling

The SDK throws errors for failed requests. Always wrap API calls in try-catch blocks:

try {
  const record = await centrali.createRecord('Product', productData);
  console.log('Success:', record);
} catch (error) {
  if (error.response) {
    // API returned an error response
    console.error('API Error:', error.response.data);
    console.error('Status:', error.response.status);

    if (error.response.status === 400) {
      // Validation error
      console.error('Validation failed:', error.response.data.error);
    } else if (error.response.status === 401) {
      // Authentication failed
      console.error('Authentication required');
    }
  } else if (error.request) {
    // Request was made but no response received
    console.error('Network error:', error.message);
  } else {
    // Something else happened
    console.error('Error:', error.message);
  }
}

TypeScript Support

The SDK is written in TypeScript and provides full type definitions:

import {
  CentraliSDK,
  CentraliSDKOptions,
  ApiResponse,
  // Structure types
  Structure,
  PropertyDefinition,
  CreateStructureInput,
  // Compute function types
  ComputeFunction,
  CreateComputeFunctionInput,
  TestComputeFunctionResult,
  // Trigger types
  FunctionTrigger,
  CreateTriggerInput,
  TriggerWithHealth,
  // Smart query types
  SmartQuery,
  CreateSmartQueryInput,
} from '@centrali-io/centrali-sdk';

// Define your data types
interface Product {
  id?: string;
  name: string;
  price: number;
  description: string;
  category: string;
  inStock: boolean;
}

// Use generic types for type-safe responses
const product = await centrali.createRecord<Product>('Product', {
  name: 'Laptop',
  price: 999.99,
  description: 'High-performance laptop',
  category: 'Electronics',
  inStock: true
});

// TypeScript knows product.data is of type Product
console.log(product.data.price); // Type-safe access

Best Practices

1. Environment Variables

Store credentials securely:

// .env file
CENTRALI_BASE_URL=https://centrali.io
CENTRALI_WORKSPACE_SLUG=my-workspace
CENTRALI_CLIENT_ID=your-client-id
CENTRALI_CLIENT_SECRET=your-client-secret

// Usage
const centrali = new CentraliSDK({
  baseUrl: process.env.CENTRALI_BASE_URL,
  workspaceId: process.env.CENTRALI_WORKSPACE_SLUG,
  clientId: process.env.CENTRALI_CLIENT_ID,
  clientSecret: process.env.CENTRALI_CLIENT_SECRET
});

2. Singleton Pattern

Create a single SDK instance for your application:

// centrali.ts
import { CentraliSDK } from '@centrali-io/centrali-sdk';

let instance: CentraliSDK | null = null;

export function getCentraliClient(): CentraliSDK {
  if (!instance) {
    instance = new CentraliSDK({
      baseUrl: process.env.CENTRALI_BASE_URL!,
      workspaceId: process.env.CENTRALI_WORKSPACE_SLUG!,
      clientId: process.env.CENTRALI_CLIENT_ID,
      clientSecret: process.env.CENTRALI_CLIENT_SECRET
    });
  }
  return instance;
}

// Usage in other files
import { getCentraliClient } from './centrali';

const centrali = getCentraliClient();

3. Retry Logic

Implement retry logic for transient failures:

async function retryOperation<T>(
  operation: () => Promise<T>,
  maxRetries: number = 3,
  delay: number = 1000
): Promise<T> {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await operation();
    } catch (error) {
      if (i === maxRetries - 1) throw error;

      // Only retry on network errors or 5xx status codes
      if (error.response && error.response.status < 500) {
        throw error;
      }

      await new Promise(resolve => setTimeout(resolve, delay * (i + 1)));
    }
  }
  throw new Error('Max retries exceeded');
}

// Usage
const record = await retryOperation(() =>
  centrali.createRecord('Product', productData)
);

4. Batch Operations

Process large datasets efficiently:

async function batchCreate(records: any[], batchSize: number = 10) {
  const results = [];

  for (let i = 0; i < records.length; i += batchSize) {
    const batch = records.slice(i, i + batchSize);
    const promises = batch.map(record =>
      centrali.createRecord('Product', record)
        .catch(error => ({ error, record }))
    );

    const batchResults = await Promise.all(promises);
    results.push(...batchResults);
  }

  return results;
}

Troubleshooting

Authentication Issues

// Check if token is valid
const token = centrali.getToken();
console.log('Current token:', token);

// Manually refresh service account token
if (centrali.options.clientId) {
  const newToken = await centrali.fetchServiceAccountToken();
  centrali.setToken(newToken);
}

Debug Logging

Enable axios debug logging:

const centrali = new CentraliSDK({
  baseUrl: 'https://api.centrali.io',
  workspaceId: 'my-workspace', // current SDK option name; pass the workspace slug
  token: 'your-token',
  axiosConfig: {
    // Add request/response interceptors for logging
    validateStatus: (status) => {
      console.log('Response status:', status);
      return status < 500;
    }
  }
});

Common Errors

Error Cause Solution
401 Unauthorized Invalid or expired token Refresh token or check credentials
403 Forbidden Insufficient permissions Check workspace access rights
404 Not Found Invalid record ID or collection name Verify the resource exists
400 Bad Request Invalid data format Check field types and required fields
429 Too Many Requests Rate limit exceeded Implement backoff and retry

Migration from REST API

If you're currently using direct REST API calls, here's how to migrate:

Before (REST API)

// Direct API call
const response = await fetch('https://api.centrali.io/data/workspace/my-workspace/api/v1/records/slug/Product', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer token',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    name: 'Product',
    price: 99.99
  })
});
const product = await response.json();

After (SDK)

// Using SDK
const product = await centrali.createRecord('Product', {
  name: 'Product',
  price: 99.99
});

Realtime Events

Subscribe to live record events using Server-Sent Events (SSE). The SDK handles connection management, authentication, and automatic reconnection.

Basic Usage

const subscription = centrali.realtime.subscribe({
  collections: ['order'],
  events: ['record_created', 'record_updated'],
  onEvent: (event) => {
    console.log('Event:', event.event, event.recordId);
    console.log('Data:', event.data);
  },
  onError: (error) => {
    console.error('Error:', error.code, error.message);
  }
});

// Later: stop receiving events
subscription.unsubscribe();

Subscription Options

interface RealtimeSubscribeOptions {
  // Filter by collection slugs (empty = all)
  collections?: string[];

  // Filter by event types (empty = all)
  events?: ('record_created' | 'record_updated' | 'record_deleted')[];

  // CFL filter expression for data filtering
  filter?: string;

  // Required: handle incoming events
  onEvent: (event: RealtimeRecordEvent) => void;

  // Optional callbacks
  onError?: (error: RealtimeError) => void;
  onConnected?: () => void;
  onDisconnected?: (reason?: string) => void;
}

Event Payload

interface RealtimeRecordEvent {
  event: 'record_created' | 'record_updated' | 'record_deleted';
  workspaceSlug: string;
  recordSlug: string;      // Collection's slug
  recordId: string;
  data: object;            // Record data
  timestamp: string;       // ISO timestamp
  createdBy?: string;
  updatedBy?: string;
}

Filtering Events

// By collection
centrali.realtime.subscribe({
  collections: ['order', 'invoice'],
  onEvent: handleEvent
});

// By event type
centrali.realtime.subscribe({
  events: ['record_created'],
  onEvent: handleEvent
});

// By data values (CFL filter)
centrali.realtime.subscribe({
  collections: ['order'],
  filter: 'data.status:shipped',
  onEvent: handleEvent
});

// Combine filters
centrali.realtime.subscribe({
  collections: ['order'],
  events: ['record_updated'],
  filter: 'data.total:gt:1000',
  onEvent: handleEvent
});

React Integration

function OrderList() {
  const [orders, setOrders] = useState<Order[]>([]);

  useEffect(() => {
    const centrali = new CentraliSDK({...});

    // 1. Fetch initial data
    centrali.queryRecords('order', { pageSize: 50 })
      .then(res => setOrders(res.data));

    // 2. Subscribe to updates
    const sub = centrali.realtime.subscribe({
      collections: ['order'],
      onEvent: (event) => {
        if (event.event === 'record_created') {
          setOrders(prev => [event.data as Order, ...prev]);
        } else if (event.event === 'record_updated') {
          setOrders(prev => prev.map(o =>
            o.id === event.recordId ? event.data.after : o
          ));
        }
      }
    });

    // 3. Cleanup
    return () => sub.unsubscribe();
  }, []);

  return <ul>{orders.map(o => <li key={o.id}>{o.name}</li>)}</ul>;
}

Error Handling

centrali.realtime.subscribe({
  onEvent: handleEvent,
  onError: (error) => {
    switch (error.code) {
      case 'TOKEN_EXPIRED':
        // Refresh token - SDK will reconnect
        refreshToken().then(t => centrali.setToken(t));
        break;
      case 'RATE_LIMIT_EXCEEDED':
        // Too many connections
        showUpgradePrompt();
        break;
      case 'FORBIDDEN':
        // No permission
        showAccessDenied();
        break;
    }
  }
});

For complete realtime documentation, see: - Realtime Quickstart - Realtime Authentication - Realtime Filtering

Support

For issues or questions: - Check the API Overview for endpoint details - Review error codes above - Contact support at support@centrali.io