The official TypeScript/JavaScript SDK for TrailBase provides a type-safe client for accessing your TrailBase backend from web browsers, Node.js, Deno, and other JavaScript runtimes.
Installation
Initialization
Basic Client
Initialize a client with your TrailBase server URL:
import { initClient } from 'trailbase';
const client = initClient('https://your-server.trailbase.io');
Client with Existing Tokens
If you have persisted tokens from a previous session:
import { initClient, type Tokens } from 'trailbase';
const tokens: Tokens = {
auth_token: 'your-auth-token',
refresh_token: 'your-refresh-token',
csrf_token: 'your-csrf-token',
};
const client = initClient('https://your-server.trailbase.io', { tokens });
Client from Cookies
For server-side rendering or when working with cookies:
import { initClientFromCookies } from 'trailbase';
const client = await initClientFromCookies('https://your-server.trailbase.io');
Auth State Changes
Listen to authentication state changes:
import { initClient, type Client, type User } from 'trailbase';
const client = initClient('https://your-server.trailbase.io', {
onAuthChange: (client: Client, user?: User) => {
if (user) {
console.log('User logged in:', user.email);
} else {
console.log('User logged out');
}
},
});
Authentication
Login
try {
await client.login('[email protected]', 'password');
const user = client.user();
console.log('Logged in as:', user?.email);
} catch (error) {
console.error('Login failed:', error);
}
Logout
Check Current User
const user = client.user();
if (user) {
console.log('User ID:', user.id);
console.log('Email:', user.email);
console.log('Is Admin:', user.admin);
}
Access Tokens
const tokens = client.tokens();
if (tokens) {
// Persist tokens for future sessions
localStorage.setItem('trailbase_tokens', JSON.stringify(tokens));
}
Refresh Token
await client.refreshAuthToken();
Record API
List Records
interface Post {
id: string;
title: string;
content: string;
author_id: string;
created_at: number;
}
const posts = client.records<Post>('posts');
const response = await posts.list({
pagination: { limit: 10, offset: 0 },
order: ['-created_at'],
count: true,
});
console.log('Posts:', response.records);
console.log('Total count:', response.total_count);
console.log('Next cursor:', response.cursor);
Read a Single Record
const post = await posts.read('post-id');
console.log('Post:', post);
// With expanded relationships
const postWithAuthor = await posts.read('post-id', {
expand: ['author'],
});
Create a Record
const newPostId = await posts.create({
title: 'Hello World',
content: 'My first post',
});
console.log('Created post with ID:', newPostId);
Create Multiple Records
const ids = await posts.createBulk([
{ title: 'Post 1', content: 'Content 1' },
{ title: 'Post 2', content: 'Content 2' },
]);
Update a Record
await posts.update('post-id', {
title: 'Updated Title',
});
Delete a Record
await posts.delete('post-id');
Filtering
TrailBase supports powerful filtering with comparison operators:
import type { Filter, CompareOp } from 'trailbase';
// Simple equality filter
const response = await posts.list({
filters: [
{
column: 'author_id',
value: 'user-123',
},
],
});
// With comparison operators
const recentPosts = await posts.list({
filters: [
{
column: 'created_at',
op: 'greaterThan' as CompareOp,
value: String(Date.now() - 7 * 24 * 60 * 60 * 1000),
},
],
});
// LIKE operator for text search
const searchResults = await posts.list({
filters: [
{
column: 'title',
op: 'like' as CompareOp,
value: '%search%',
},
],
});
// AND/OR composite filters
const filtered = await posts.list({
filters: [
{
and: [
{ column: 'status', value: 'published' },
{ column: 'author_id', value: 'user-123' },
],
},
],
});
Available Comparison Operators
equal - Equality (default)
notEqual - Not equal
lessThan - Less than
lessThanEqual - Less than or equal
greaterThan - Greater than
greaterThanEqual - Greater than or equal
like - SQL LIKE pattern matching
regexp - Regular expression matching
@within - Spatial within (for geospatial data)
@intersects - Spatial intersects
@contains - Spatial contains
Real-time Subscriptions
Subscribe to a Single Record
const stream = await posts.subscribe('post-id');
for await (const event of stream) {
if ('Insert' in event) {
console.log('Record inserted:', event.Insert);
} else if ('Update' in event) {
console.log('Record updated:', event.Update);
} else if ('Delete' in event) {
console.log('Record deleted:', event.Delete);
} else if ('Error' in event) {
console.error('Subscription error:', event.Error);
}
}
Subscribe to All Records
const stream = await posts.subscribeAll({
filters: [
{ column: 'author_id', value: 'user-123' },
],
});
for await (const event of stream) {
console.log('Change event:', event);
}
WebSocket Subscriptions
For improved performance, you can use WebSocket-based subscriptions:
const recordApi = client.records('posts') as any;
const stream = await recordApi.subscribeWs('post-id');
for await (const event of stream) {
console.log('WebSocket event:', event);
}
Batch Operations
Execute multiple operations in a single transaction:
const ops = [
posts.createOp({ title: 'Post 1', content: 'Content 1' }),
posts.updateOp('post-id', { title: 'Updated' }),
posts.deleteOp('old-post-id'),
];
const ids = await client.execute(ops, true);
console.log('Operation results:', ids);
File Handling
Access File URLs
import { filePath, filesPath } from 'trailbase';
// Single file column
const avatarUrl = filePath('users', userId, 'avatar');
// Files array column
const attachmentUrl = filesPath('messages', messageId, 'attachments', 'document.pdf');
Avatar URL
const avatarUrl = client.avatarUrl(userId);
// Or current user's avatar
const myAvatarUrl = client.avatarUrl();
Error Handling
import { FetchError } from 'trailbase';
try {
await posts.create({ title: 'Test' });
} catch (error) {
if (error instanceof FetchError) {
console.error('Status:', error.status);
console.error('Message:', error.message);
console.error('URL:', error.url);
if (error.isClient()) {
console.log('Client error (4xx)');
} else if (error.isServer()) {
console.log('Server error (5xx)');
}
}
}
Advanced Usage
Custom Fetch Options
const response = await client.fetch('/api/custom/endpoint', {
method: 'POST',
body: JSON.stringify({ data: 'value' }),
throwOnError: false, // Don't throw on error status
});
Deferred Operations
Build operations without executing them immediately:
const listOp = posts.listOp({ pagination: { limit: 5 } });
const readOp = posts.readOp('post-id');
// Execute later
const listResult = await listOp.query();
const readResult = await readOp.query();
Type Definitions
User
type User = {
id: string;
email: string;
admin?: boolean;
};
Tokens
type Tokens = {
auth_token: string;
refresh_token: string | null;
csrf_token: string | null;
};
ListResponse
type ListResponse<T> = {
cursor?: string;
records: T[];
total_count?: number;
};
type Pagination = {
cursor?: string;
limit?: number;
offset?: number;
};
Best Practices
Always handle authentication errors gracefully and redirect users to login when tokens expire.
Store tokens securely. For web applications, consider using secure, HTTP-only cookies instead of localStorage.
The client automatically refreshes auth tokens before they expire. Manual refresh is rarely needed.