Skip to main content

gbaas_sdk 3.1.0 copy "gbaas_sdk: ^3.1.0" to clipboard
gbaas_sdk: ^3.1.0 copied to clipboard

Dart SDK for BaaS (Backend as a Service) - auth, database, storage, realtime, functions, admin APIs.

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 (unreleased) #

  • Refresh tokens: login now returns a rotating refresh token; on a 401 the client silently refreshes once (deduped across concurrent requests) and retries. Optional RefreshTokenStorage interface persists it across app restarts; auth.refresh() for manual refresh.
  • Realtime server-side filters: subscribe(..., filter: {'status': 'eq.pending'}) narrows delivery on the server (PostgREST-style op.value).
  • Realtime Broadcast & Presence: ephemeral channels — subscribeBroadcast / broadcast / unsubscribeBroadcast and subscribePresence / leavePresence. No DB, no RLS.
  • Storage: image transforms on the download route (?width=&height=&quality=&format=) and per-bucket upload size caps (max_bytes).

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; events are delivered per-row under RLS to any authenticated user (not admin-only).

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 tokens in memory by default. Implement SessionStorage once with your favorite secure store and pass it to BaasClient. To also survive app restarts with silent token refresh, implement the optional RefreshTokenStorage interface on the same adapter — it is a separate interface, so existing SessionStorage adapters keep compiling; without it auto-refresh still works in-session, but a restart loses the refresh token. The default InMemorySessionStorage already implements both.

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

class SecureSessionStorage implements SessionStorage, RefreshTokenStorage {
  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');
  @override Future<String?> readRefreshToken() => _store.read(key: 'baas_refresh');
  @override Future<void> writeRefreshToken(String t) => _store.write(key: 'baas_refresh', value: t);
  @override Future<void> deleteRefreshToken() => _store.delete(key: 'baas_refresh');
}

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

// At boot: pick up the persisted tokens, validate against /api/profile.
// With a persisted refresh token, an expired access token refreshes
// silently instead of forcing a re-login.
await baas.auth.restoreSession();

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

Known limitations #

  • Realtime — per-row RLS: the backend delivers events to any authenticated subscriber, filtered per-row by the same RLS policies as REST/RPC (fail-closed). Subscribe to a table to receive its row events. 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.
  • Refresh tokens — email/password only: only login (and /api/auth/refresh) issues the rotating refresh pair. MFA finalize, OAuth, passwordless OTP/magic-link, and anonymous sign-in mint access-only tokens — those sessions cannot silently refresh and expire with the access token.
  • Broadcast/Presence — single instance: channels are in-memory per backend instance; messages do not fan out across a multi-instance deployment.

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 & token refresh
await baas.auth.restoreSession();  // reads SessionStorage (+ refresh token if persisted)
await baas.auth.refresh();         // manual refresh — automatic on 401, see below
await baas.auth.logout();          // revokes refresh tokens server-side, clears both locally
baas.http.setTokens(accessToken: '...', refreshToken: '...'); // inject external tokens

login stores the rotating refresh token next to the access token. When an authenticated request comes back 401, the client calls /api/auth/refresh once (concurrent 401s are deduped into a single refresh) and retries the request; sessionExpired fires only when the refresh itself fails or no refresh token is available. Only email/password login issues refresh tokens — see Known limitations.

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);

// Public download URL (backend route — supports image transforms)
final publicUrl = baas.storage.getPublicUrl(fileId: 'file-id');
final thumbUrl = '$publicUrl?width=200&height=200&format=jpeg&quality=80';

// Download bytes (streams via the backend, enforces public/RLS rules)
final bytes = await baas.storage.download('file-id');

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

Image transforms run on GET /api/storage/files/{id}/download via query params width, height, quality (JPEG, 1-100), format (jpeg|png) — after the public/RLS check. Source formats: jpeg/png/gif/tiff/bmp (gif re-encodes to a static png; webp/avif pass through untouched). Output dimensions are clamped to 4096; sources over 32 MiB or 25 megapixels are served untransformed, and any transform error falls back to serving the original file.

Uploads are size-capped: globally by the server's STORAGE_MAX_UPLOAD_BYTES (default 10 MiB) plus an optional per-bucket max_bytes (null = no bucket cap; not yet exposed on createBucket — set it through the bucket API or dashboard).

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!'));

// Server-side value filters (PostgREST "op.value"; a bare value means eq)
// Operators: eq, neq, gt, gte, lt, lte, in (comma list), like (% and _)
baas.realtime.subscribe('orders', (event) => print(event),
    filter: {'status': 'eq.pending', 'total': 'gt.100'});

// Broadcast — ephemeral pub/sub (no DB, no RLS): UI sync, typing, chat
baas.realtime.subscribeBroadcast('room-42', (payload) => print(payload));
baas.realtime.broadcast('room-42', {'typing': true});
baas.realtime.unsubscribeBroadcast('room-42');

// Presence — who is online, with arbitrary per-member state
baas.realtime.subscribePresence('lobby', {'status': 'online'}, (msg) {
  // msg: {type: 'presence', channel: 'lobby', action: 'sync'|'join'|'leave',
  //        members: [{user_id, role, state}]}
});
baas.realtime.leavePresence('lobby');

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

// Unsubscribe
sub.unsubscribe();

// Disconnect
baas.realtime.disconnect();

Filters narrow delivery only — RLS still authorizes every row. The filter is per-subscription: it is pushed to the server only while every subscriber on the table shares the identical filter; otherwise the table streams unfiltered server-side and each subscriber's filter is applied client-side (same matcher). Presence joiners receive the full roster (sync); peers get join/leave, and a disconnect auto-leaves. Broadcast/presence channels re-join automatically on reconnect, including for clients with no table subscriptions.

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

0
likes
140
points
47
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Dart SDK for BaaS (Backend as a Service) - auth, database, storage, realtime, functions, admin APIs.

Homepage
Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

http, meta, web_socket_channel

More

Packages that depend on gbaas_sdk