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.
Installation¶
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¶
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];
// 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 justimages - 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',
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
Related Documentation¶
- API Overview - Complete API reference
- Records API - Detailed records operations
- Realtime API - Realtime HTTP API reference
- Compute Functions - Writing serverless functions
- Authentication Guide - Authentication details
- Query Language - Advanced querying syntax
Support¶
For issues or questions: - Check the API Overview for endpoint details - Review error codes above - Contact support at support@centrali.io