Skip to main content
The official Dart SDK for TrailBase provides a fully-typed client for accessing your TrailBase backend from Flutter and Dart applications.

Installation

Add trailbase to your pubspec.yaml:
pubspec.yaml
dependencies:
  trailbase: ^0.7.2
Then run:
dart pub get
Or for Flutter:
flutter pub get

Initialization

Basic Client

import 'package:trailbase/trailbase.dart';

final client = Client('https://your-server.trailbase.io');

Client with Tokens

Restore a previous session with saved tokens:
final tokens = Tokens(authToken, refreshToken, csrfToken);
final client = await Client.withTokens(
  'https://your-server.trailbase.io',
  tokens,
);

Auth State Listener

final client = Client(
  'https://your-server.trailbase.io',
  onAuthChange: (client, tokens) {
    if (tokens != null) {
      print('User logged in');
      // Persist tokens
      saveTokens(tokens);
    } else {
      print('User logged out');
      // Clear persisted tokens
    }
  },
);

Authentication

Login

try {
  final tokens = await client.login('[email protected]', 'password');
  print('Auth token: ${tokens.auth}');
  
  final user = client.user();
  print('Logged in as: ${user?.email}');
} catch (e) {
  print('Login failed: $e');
}

Login with OAuth (PKCE)

import 'package:trailbase/trailbase.dart';

// Generate PKCE code verifier and challenge
final pkce = PkceHelper.generate();

// Open OAuth authorization URL with the challenge
// After callback with authorization code:
final tokens = await client.loginWithAuthCode(
  authorizationCode,
  pkceCodeVerifier: pkce.codeVerifier,
);

Logout

await client.logout();

Current User

final user = client.user();
if (user != null) {
  print('User ID: ${user.id}');
  print('Email: ${user.email}');
}

Access Tokens

final tokens = client.tokens();
if (tokens != null) {
  // Persist tokens for later use
  await storage.save('tokens', tokens);
}

Refresh Token

await client.refreshAuthToken();

Record API

List Records

final posts = client.records('posts');

final response = await posts.list(
  pagination: Pagination(limit: 10, offset: 0),
  order: ['-created_at'],
  count: true,
);

print('Records: ${response.records}');
print('Total count: ${response.totalCount}');
print('Next cursor: ${response.cursor}');

Read a Record

// Using integer ID
final post = await posts.read(123.id());

// Using UUID string
final post = await posts.read('uuid-string'.id());

// With expanded relationships
final postWithAuthor = await posts.read(
  postId.id(),
  expand: ['author'],
);

print('Title: ${post['title']}');

Create a Record

final newPost = {
  'title': 'Hello World',
  'content': 'My first post from Dart',
};

final id = await posts.create(newPost);
print('Created post with ID: $id');

Create Multiple Records

final newPosts = [
  {'title': 'Post 1', 'content': 'Content 1'},
  {'title': 'Post 2', 'content': 'Content 2'},
];

final ids = await posts.createBulk(newPosts);
print('Created ${ids.length} posts');

Update a Record

await posts.update(
  postId.id(),
  {'title': 'Updated Title'},
);

Delete a Record

await posts.delete(postId.id());

Filtering

// Simple equality filter
final response = await posts.list(
  filters: [
    Filter(
      column: 'author_id',
      value: userId,
    ),
  ],
);

// With comparison operators
final recentPosts = await posts.list(
  filters: [
    Filter(
      column: 'created_at',
      op: CompareOp.greaterThan,
      value: DateTime.now().subtract(Duration(days: 7)).toString(),
    ),
  ],
);

// LIKE operator
final searchResults = await posts.list(
  filters: [
    Filter(
      column: 'title',
      op: CompareOp.like,
      value: '%search%',
    ),
  ],
);

// AND composite filter
final filtered = await posts.list(
  filters: [
    And([
      Filter(column: 'status', value: 'published'),
      Filter(column: 'author_id', value: userId),
    ]),
  ],
);

// OR composite filter
final filtered = await posts.list(
  filters: [
    Or([
      Filter(column: 'category', value: 'tech'),
      Filter(column: 'category', value: 'science'),
    ]),
  ],
);

Available Comparison Operators

enum CompareOp {
  equal,
  notEqual,
  lessThan,
  lessThanEqual,
  greaterThan,
  greaterThanEqual,
  like,
  regexp,
  stWithin,      // Geospatial: within
  stIntersects,  // Geospatial: intersects
  stContains,    // Geospatial: contains
}

Real-time Subscriptions

Subscribe to a Single Record

final stream = await posts.subscribe(postId.id());

await for (final event in stream) {
  if (event is Insert) {
    print('Record inserted: ${event.record}');
  } else if (event is Update) {
    print('Record updated: ${event.record}');
  } else if (event is Delete) {
    print('Record deleted: ${event.record}');
  } else if (event is Error) {
    print('Error: ${event.message}');
  }
}

Subscribe to All Records

final stream = await posts.subscribeAll(
  filters: [
    Filter(column: 'author_id', value: userId),
  ],
);

await for (final event in stream) {
  print('Change event: $event');
}

File Handling

Image URIs

final posts = client.records('posts');

// Single file
final coverImageUri = posts.imageUri(
  postId.id(),
  'cover_image',
);

// File from files array
final attachmentUri = posts.imageUri(
  postId.id(),
  'attachments',
  filename: 'document.pdf',
);

// Use in Flutter
Image.network(coverImageUri.toString())

Record ID Types

TrailBase supports both integer and UUID record IDs:
// Integer ID
final intId = 123.id();
final intId2 = RecordId.integer(123);

// UUID string ID
final uuidId = 'uuid-string'.id();
final uuidId2 = RecordId.uuid('uuid-string');

// Use in operations
await posts.read(intId);
await posts.update(uuidId, data);

Error Handling

try {
  final post = await posts.read(postId.id());
} on HttpException catch (e) {
  print('HTTP ${e.statusCode}: ${e.body}');
} catch (e) {
  print('Error: $e');
}

Flutter Integration Example

import 'package:flutter/material.dart';
import 'package:trailbase/trailbase.dart';

class PostsList extends StatefulWidget {
  @override
  _PostsListState createState() => _PostsListState();
}

class _PostsListState extends State<PostsList> {
  late Client client;
  List<Map<String, dynamic>> posts = [];
  bool loading = true;

  @override
  void initState() {
    super.initState();
    client = Client('https://your-server.trailbase.io');
    loadPosts();
  }

  Future<void> loadPosts() async {
    try {
      final response = await client.records('posts').list(
        order: ['-created_at'],
        pagination: Pagination(limit: 20),
      );
      setState(() {
        posts = response.records;
        loading = false;
      });
    } catch (e) {
      print('Error loading posts: $e');
      setState(() => loading = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    if (loading) {
      return Center(child: CircularProgressIndicator());
    }

    return ListView.builder(
      itemCount: posts.length,
      itemBuilder: (context, index) {
        final post = posts[index];
        return ListTile(
          title: Text(post['title']),
          subtitle: Text(post['content']),
        );
      },
    );
  }
}

Type Definitions

User

class User {
  final String id;
  final String email;
}

Tokens

class Tokens {
  final String auth;
  final String? refresh;
  final String? csrf;
}

ListResponse

class ListResponse {
  final String? cursor;
  final List<Map<String, dynamic>> records;
  final int? totalCount;
}

Pagination

class Pagination {
  final String? cursor;
  final int? limit;
  final int? offset;
}

Best Practices

Use the onAuthChange callback to persist tokens and update your app’s authentication state.
Store tokens securely using packages like flutter_secure_storage instead of shared preferences.
The client automatically refreshes auth tokens before they expire.

Build docs developers (and LLMs) love