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.

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',

  // 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 two 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',
  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',
  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();

API Reference

Initialization Options

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

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

  /** Optional bearer token for user authentication */
  token?: 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('StructureName', {
  field1: 'value1',
  field2: 123,
  nested: {
    subField: 'value'
  }
});

console.log('Created record ID:', record.id);

Get a Record

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

Update a Record

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

console.log('Updated record:', updated.data);

Delete a Record

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

Query Records

// Simple query with filters
const products = await centrali.queryRecords('Product', {
  filter: 'inStock = true',
  sort: '-createdAt',
  limit: 10,
  offset: 0
});

console.log(`Found ${products.data.length} products`);

// Query with complex filters
const orders = await centrali.queryRecords('Order', {
  filter: 'status = "pending" AND total > 100',
  sort: 'createdAt',
  limit: 20
});

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

Compute Functions

Execute serverless compute functions:

// Invoke a function with payload
const result = await centrali.invokeFunction('calculate-discount', {
  productId: 'prod-123',
  quantity: 2,
  couponCode: 'SAVE20'
});

console.log('Discount result:', result.data);

// Example: Send email via compute function
const emailResult = await centrali.invokeFunction('send-email', {
  to: 'customer@example.com',
  subject: 'Order Confirmation',
  template: 'order-confirmation',
  data: {
    orderNumber: '12345',
    items: [...]
  }
});

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];

const uploadResult = await centrali.uploadFile(
  file,
  'product-images',  // folder/location
  true               // make publicly accessible
);

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

// In Node.js (with form-data library)
const FormData = require('form-data');
const fs = require('fs');

const form = new FormData();
form.append('file', fs.createReadStream('image.jpg'));

// Note: File upload in Node.js requires additional setup

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',
      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
    await this.centrali.invokeFunction('update-inventory', {
      productId: product.id,
      action: 'initialize',
      quantity: productData.initialStock || 0
    });

    return product;
  }

  // Search products
  async searchProducts(query: string, category?: string) {
    const filter = category
      ? `(name CONTAINS "${query}" OR description CONTAINS "${query}") AND category = "${category}"`
      : `name CONTAINS "${query}" OR description CONTAINS "${query}"`;

    return await this.centrali.queryRecords('Product', {
      filter,
      sort: '-popularity',
      limit: 20
    });
  }

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

    // Get reviews
    const reviews = await this.centrali.queryRecords('Review', {
      filter: `productId = "${productId}"`,
      sort: '-createdAt',
      limit: 10
    });

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

  // Update inventory
  async updateInventory(productId: string, quantity: number, operation: 'add' | 'subtract') {
    const result = await this.centrali.invokeFunction('update-inventory', {
      productId,
      quantity,
      operation
    });

    if (!result.data.success) {
      throw new Error(`Inventory update failed: ${result.data.error}`);
    }

    return result.data;
  }
}

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

  // 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
    await this.centrali.invokeFunction('send-welcome-email', {
      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',
      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
  async pollChanges(since: Date, callback: (changes: any[]) => void) {
    const changes = await this.centrali.queryRecords('SyncedData', {
      filter: `updatedAt > "${since.toISOString()}"`,
      sort: 'updatedAt',
      limit: 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
} 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=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,
  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!,
      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',
  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 structure 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({
  structures: ['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 structure slugs (empty = all)
  structures?: 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;      // Structure's slug
  recordId: string;
  data: object;            // Record data
  timestamp: string;       // ISO timestamp
  createdBy?: string;
  updatedBy?: string;
}

Filtering Events

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

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

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

// Combine filters
centrali.realtime.subscribe({
  structures: ['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', { limit: 50 })
      .then(res => setOrders(res.data));

    // 2. Subscribe to updates
    const sub = centrali.realtime.subscribe({
      structures: ['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 - Realtime Troubleshooting

Support

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