Skip to main content
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

npm install trailbase

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

await client.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;
};

Pagination

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.

Build docs developers (and LLMs) love