gbaas_sdk

pub package

Official Dart SDK for BaaS (Backend as a Service). Pure Dart — works in Flutter, server-side Dart, and Dart CLI.

What's new in 2.0.0

  • Security fix: validateResetToken no longer always returns true. Upgrade if you ship a password-reset UI.
  • BaasClient.onAuthStateChange broadcast stream (signedIn / signedOut / sessionExpired).
  • Pluggable SessionStorage — drop in flutter_secure_storage (or anything) to persist the JWT.
  • 401 auto-handling: any authenticated request that comes back 401 clears the session, emits sessionExpired, and throws UnauthorizedException.
  • Query builder: isNull / isNotNull and the renamed inFilter (was inList) now emit correct PostgREST syntax.
  • Storage: upload actually respects bucket (sent as bucket_id) and path; getFile returns StorageFileWithUrl.
  • Realtime: parses concatenated JSON frames so coalesced events are no longer dropped; documents the role=admin backend limitation.

See CHANGELOG.md for the full breaking-changes list and KNOWN_ISSUES.md for what remains.

Installation

dart pub add gbaas_sdk

Migration from 1.x

// 1.x
await client.from('users').inList('status', ['active', 'pending']).get();
// 2.0
await client.from('users').inFilter('status', ['active', 'pending']).get();

// 1.x — getFile returned a (broken, empty) StorageFile
final file = await client.storage.getFile('id');
print(file.name); // ''

// 2.0 — getFile returns metadata + signed URL
final result = await client.storage.getFile('id');
print(result.file.name); // 'photo.jpg'
print(result.url);       // 'https://.../photo.jpg?X-Amz-...'

// 1.x — updateProfile (removed; was a 404)
await client.auth.updateProfile(email: '...');
// 2.0 — use admin users API or your own profile table.

// 1.x — restoreSession required the token
await client.auth.restoreSession(token);
// 2.0 — reads from SessionStorage by default
await client.auth.restoreSession();

Session persistence (Flutter)

The SDK keeps the token in memory by default. Implement SessionStorage once with your favorite secure store and pass it to BaasClient:

import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:gbaas_sdk/gbaas_sdk.dart';

class SecureSessionStorage implements SessionStorage {
  final _store = const FlutterSecureStorage();
  @override Future<String?> readToken() => _store.read(key: 'baas_token');
  @override Future<void> writeToken(String t) => _store.write(key: 'baas_token', value: t);
  @override Future<void> deleteToken() => _store.delete(key: 'baas_token');
}

final baas = BaasClient(
  baseUrl: '...',
  apiKey: '...',
  sessionStorage: SecureSessionStorage(),
);

// At boot: pick up the persisted token, validate it against /api/profile.
await baas.auth.restoreSession();

// React to lifecycle events.
baas.onAuthStateChange.listen((s) {
  if (s == AuthState.sessionExpired) {
    // Redirect to login screen.
  }
});

Known limitations

  • Realtime — role=admin only: the backend currently emits events only to clients authenticated with role=admin. Subscribing with a client/driver/anon token connects successfully but no events arrive. See KNOWN_ISSUES.md.
  • MFA endpoints: shape mismatches around setup/enable/finalize against the current backend; methods stay on the surface but expect to revisit them. See KNOWN_ISSUES.md.

Quick Start

import 'package:gbaas_sdk/gbaas_sdk.dart';

final baas = BaasClient(
  baseUrl: 'https://your-baas-instance.com',
  apiKey: 'baas_sk_...',
);

// Auth
await baas.auth.login(email: 'user@example.com', password: 'password');

// Database
final users = await baas.from('users').select().get();

// Storage
await baas.storage.upload(bucket: 'avatars', path: 'user.png', bytes: bytes, filename: 'avatar.png');

// Realtime
baas.realtime.subscribe('posts', (event) => print(event));

// Functions
final result = await baas.functions.invoke('my-function', body: {'key': 'value'});

Modules

Authentication

// Register
final session = await baas.auth.register(
  email: 'new@example.com',
  password: 'password123',
);

// Login
final session = await baas.auth.login(
  email: 'user@example.com',
  password: 'password',
);

// MFA
final mfaSetup = await baas.auth.setupMfa();
await baas.auth.enableMfa(code: '123456');

// OAuth
final providers = await baas.auth.getProviders();
final url = baas.auth.getOAuthUrl('google');

// Password reset
await baas.auth.forgotPassword(email: 'user@example.com');
await baas.auth.resetPassword(token: 'reset-token', newPassword: 'newpass');

// Email verification
await baas.auth.requestEmailVerification();
await baas.auth.verifyEmail(token: 'token');

// Profile
final user = await baas.auth.getProfile();
await baas.auth.updateProfile(email: 'new@email.com');

// Session
await baas.auth.restoreSession('saved-token');
await baas.auth.logout();

Database

// Query with filters
final posts = await baas.from('posts')
    .select()
    .eq('status', 'published')
    .gte('created_at', '2024-01-01')
    .orderDesc('created_at')
    .limit(10)
    .get();

// Get single record
final post = await baas.from('posts').getById('post-id');

// Insert
final newPost = await baas.from('posts').insert({
  'title': 'New Post',
  'content': 'Content here',
});

// Update
await baas.from('posts').update('post-id', {
  'title': 'Updated Title',
});

// Delete
await baas.from('posts').delete('post-id');

// Raw SQL (admin only)
final results = await baas.database.executeRawSql('SELECT * FROM users LIMIT 10');

Filter Operators

.eq('column', value)           // =
.neq('column', value)          // !=
.gt('column', value)           // >
.gte('column', value)          // >=
.lt('column', value)           // <
.lte('column', value)          // <=
.like('column', '%pattern%')
.ilike('column', '%pattern%')  // case insensitive
.isNull('column')
.isNotNull('column')
.inList('column', [1, 2, 3])

Storage

// Create bucket
final bucket = await baas.storage.createBucket(name: 'avatars', isPublic: true);

// Upload file
final result = await baas.storage.upload(
  bucket: 'avatars',
  path: 'user123/avatar.png',
  bytes: fileBytes,
  filename: 'avatar.png',
);

// List files
final files = await baas.storage.listFiles(bucketId: 'avatars');

// Get download URL
final url = await baas.storage.getPresignedUrl(fileId: 'file-id', expiresIn: 3600);

// Delete file
await baas.storage.deleteFile('file-id');

Realtime

// Subscribe to all changes
final sub = baas.realtime.subscribe('posts', (event) {
  print('Event: ${event.type} on ${event.table}');
  print('Data: ${event.newData}');
});

// Subscribe to specific events
baas.realtime.onInsert('posts', (event) => print('New post!'));
baas.realtime.onUpdate('posts', (event) => print('Post updated!'));
baas.realtime.onDelete('posts', (event) => print('Post deleted!'));

// Connection state
baas.realtime.onConnectionStateChange = (state) {
  print('Connection: $state');
};

// Unsubscribe
sub.unsubscribe();

// Disconnect
baas.realtime.disconnect();

Edge Functions

// Execute by ID
final result = await baas.functions.invoke('function-id', body: {'key': 'value'});
print(result.isSuccess ? result.data : result.error);

// Execute by name
final result = await baas.functions.invokeByName('my-function', body: {'key': 'value'});

// Execute code directly
final result = await baas.functions.executeCode(code: 'return { hello: "world" };');

// CRUD
final fns = await baas.functions.list();
await baas.functions.create(name: 'my-fn', code: 'return {};', language: 'javascript');
await baas.functions.update('id', name: 'new-name');
await baas.functions.delete('id');

// Hooks (attach functions to events)
await baas.functions.createHook(eventType: 'INSERT', functionId: 'fn-id', tableName: 'posts');
final hooks = await baas.functions.listHooks();
await baas.functions.deleteHook('hook-id');

GraphQL

final result = await baas.graphql.query(
  query: '{ users { id email } }',
  variables: {'limit': 10},
);

final schema = await baas.graphql.getSchema();
final playgroundUrl = baas.graphql.getPlaygroundUrl();

Admin Modules

Webhooks

final webhooks = await baas.webhooks.list();
final wh = await baas.webhooks.create(url: 'https://example.com/hook', events: ['INSERT']);
await baas.webhooks.update('id', url: 'https://new-url.com');
await baas.webhooks.test('id');
await baas.webhooks.delete('id');

Policies (RLS)

final policies = await baas.policies.list();
await baas.policies.create(name: 'read_own', table: 'posts', operation: 'SELECT', expression: 'user_id = auth.uid()');
await baas.policies.delete('id');

API Keys

final keys = await baas.apiKeys.list();
await baas.apiKeys.create(name: 'mobile-app');
await baas.apiKeys.revoke('id');
await baas.apiKeys.delete('id');

// Instance token
final token = await baas.apiKeys.getInstanceToken();
await baas.apiKeys.regenerateInstanceToken();

Backups

final backups = await baas.backups.list();
await baas.backups.create(description: 'Before migration');
final backup = await baas.backups.get('id');
await baas.backups.restore('id');
final url = await baas.backups.getDownloadUrl('id');
await baas.backups.delete('id');

// Import
final tables = await baas.backups.listTables();
await baas.backups.importFile(table: 'users', bytes: csvBytes, filename: 'users.csv', format: 'csv');

Migrations

final migrations = await baas.migrations.list();
await baas.migrations.create(name: 'add_posts_table', sql: 'CREATE TABLE posts (...)');
await baas.migrations.apply('id');
await baas.migrations.rollback('id');
await baas.migrations.delete('id');

// Auto-generate migration from current schema diff
final diff = await baas.migrations.generate();

Email

// Send email
await baas.email.send(to: 'user@example.com', subject: 'Welcome', body: '<h1>Hello!</h1>');

// Email config
final config = await baas.email.getConfig();
await baas.email.saveConfig(config: {'provider': 'smtp', 'host': 'smtp.gmail.com'});

// Templates
final templates = await baas.email.listTemplates();
await baas.email.createTemplate(name: 'welcome', subject: 'Welcome!', body: '<h1>Hi {{name}}</h1>');
await baas.email.updateTemplate('id', subject: 'Updated subject');
await baas.email.deleteTemplate('id');

// Logs
final logs = await baas.email.getLogs();

Cron Jobs

final jobs = await baas.jobs.list();
await baas.jobs.create(name: 'cleanup', schedule: '0 0 * * *', functionId: 'fn-id');
await baas.jobs.update('id', schedule: '0 */6 * * *');
await baas.jobs.toggle('id');
await baas.jobs.runNow('id');
final execs = await baas.jobs.getExecutions('id');
await baas.jobs.delete('id');

Environment Variables

final vars = await baas.envVars.list();
await baas.envVars.create(key: 'STRIPE_KEY', value: 'sk_live_...');
await baas.envVars.update('id', value: 'new-value');
await baas.envVars.delete('id');

Database Branches

final branches = await baas.branches.list();
await baas.branches.create(name: 'feature-x', sourceBranch: 'main');
await baas.branches.merge('branch-id');
await baas.branches.reset('branch-id');
await baas.branches.delete('branch-id');

Log Drains

final drains = await baas.logDrains.list();
await baas.logDrains.create(name: 'datadog', type: 'datadog', config: {'api_key': '...'});
await baas.logDrains.update('id', config: {'api_key': 'new-key'});
await baas.logDrains.toggle('id');
await baas.logDrains.test('id');
await baas.logDrains.delete('id');

Audit Logs

final logs = await baas.audit.list(action: 'LOGIN', limit: 50);
final actions = await baas.audit.getActions();

Metrics

final stats = await baas.metrics.getDashboardStats();
final requestLogs = await baas.metrics.getRequestLogs(limit: 100);
final appLogs = await baas.metrics.getApplicationLogs(level: 'error');
final timeseries = await baas.metrics.getTimeseries(metric: 'requests', period: '24h');

IP Whitelist

final ips = await baas.ipWhitelist.list();
await baas.ipWhitelist.create(ip: '192.168.1.0/24', description: 'Office');
await baas.ipWhitelist.delete('id');

CORS Origins

final origins = await baas.corsOrigins.list();
await baas.corsOrigins.create(origin: 'https://myapp.com');
await baas.corsOrigins.delete('id');

Environments

final status = await baas.environments.status();
await baas.environments.init(name: 'staging');
await baas.environments.promote(from: 'staging', to: 'production');
await baas.environments.revert(environment: 'production');
final results = await baas.search.search(query: 'hello', table: 'posts');
await baas.search.createIndex(table: 'posts', columns: ['title', 'content']);

Users (Admin)

final users = await baas.users.list(page: 1, limit: 50);
await baas.users.update('user-id', role: 'admin');
await baas.users.delete('user-id');

Flutter Integration

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

final baas = BaasClient(
  baseUrl: 'http://10.0.2.2:8086', // Android emulator localhost
  apiKey: 'baas_sk_...',
);

Persist Session

import 'package:shared_preferences/shared_preferences.dart';

// Save token after login
final session = await baas.auth.login(email: email, password: password);
final prefs = await SharedPreferences.getInstance();
await prefs.setString('access_token', session.accessToken);

// Restore on app start
final token = prefs.getString('access_token');
if (token != null) {
  await baas.auth.restoreSession(token);
}

Error Handling

try {
  await baas.auth.login(email: email, password: password);
} on AuthException catch (e) {
  if (e.data?['mfa_required'] == true) {
    // Handle MFA
    final tempToken = e.data!['temp_token'];
    await baas.auth.verifyMfa(tempToken: tempToken, code: userCode);
  } else {
    print('Auth error: ${e.message}');
  }
} on ValidationException catch (e) {
  print('Validation error: ${e.errors}');
} on BaasException catch (e) {
  print('Error: ${e.message} (${e.statusCode})');
}

Cleanup

baas.dispose(); // Closes WebSocket and HTTP connections

License

MIT

Libraries

gbaas_sdk
BaaS SDK for Dart/Flutter