koolbase_flutter

pub.dev License: MIT

Flutter SDK for Koolbase — Backend as a Service built for mobile developers.

Auth, database, storage, realtime, functions, feature flags, remote config, version enforcement, OTA updates, code push, server-driven UI, logic engine, analytics, and cloud messaging — one SDK, one initialize() call.


Get started in 2 minutes

  1. Create a free account at app.koolbase.com

  2. Create a project and copy your public key from Environments

  3. Add the SDK

dependencies:
  koolbase_flutter: ^3.3.0

4. Initialize before runApp():

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await Koolbase.initialize(KoolbaseConfig(
    publicKey: 'pk_live_xxxx',
    baseUrl: 'https://api.koolbase.com',
  ));

  runApp(MyApp());
}

That's it. Every feature below is now available via Koolbase.*.


Authentication

Email + password, Apple Sign-In, Google Sign-In, and phone + OTP — out of the box.

// Register
await Koolbase.auth.register(email: 'user@example.com', password: 'password');

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

// Current user
final user = Koolbase.auth.currentUser;

// Logout
await Koolbase.auth.logout();

// Password reset
await Koolbase.auth.forgotPassword(email: 'user@example.com');

// Listen to auth state changes
final subscription = Koolbase.auth.authStateChanges.listen((user) {
  print(user != null ? 'signed in' : 'signed out');
});

OAuth — Apple

Apple Sign-In uses the native authentication flow via the sign_in_with_apple package:

import 'package:sign_in_with_apple/sign_in_with_apple.dart';

final credential = await SignInWithApple.getAppleIDCredential(
  scopes: [
    AppleIDAuthorizationScopes.email,
    AppleIDAuthorizationScopes.fullName,
  ],
);

final user = await Koolbase.auth.signInWithApple(
  identityToken: credential.identityToken!,
  nonce: credential.nonce,
  fullName: credential.givenName != null
      ? AppleFullName(
          givenName: credential.givenName,
          familyName: credential.familyName,
        )
      : null,
);

Configure Apple Sign-In for your environment with your iOS app's Bundle ID. Full setup guide at docs.koolbase.com/auth/oauth.

OAuth — Google

Google Sign-In uses the native authentication flow via the google_sign_in package:

import 'package:google_sign_in/google_sign_in.dart';

final googleUser = await GoogleSignIn().signIn();
final googleAuth = await googleUser?.authentication;

final user = await Koolbase.auth.signInWithGoogle(
  idToken: googleAuth!.idToken!,
);

Configure Google Sign-In for your environment with the OAuth client IDs from Google Cloud Console (typically one each for iOS, Android, and web). Full setup guide at docs.koolbase.com/auth/oauth.

Phone + OTP

// Send a one-time code
await Koolbase.auth.sendOtp(phoneE164: '+233200000000');

// Verify and sign in
await Koolbase.auth.verifyOtp(
  phoneE164: '+233200000000',
  code: '123456',
);

// Or link a phone to an existing account
await Koolbase.auth.linkPhone(
  phoneE164: '+233200000000',
  code: '123456',
);

Configure your SMS provider (Twilio, Africa's Talking, or Hubtel) in the dashboard under Phone Auth.


Database

// Insert
await Koolbase.db.collection('posts').insert({
  'title': 'Hello world',
  'body': 'My first post',
});

// Query
final records = await Koolbase.db.collection('posts').get();

// Read fields off a record
final posts = await Koolbase.db.collection('posts').get();
for (final post in posts.records) {
  print(post['title']);   // field access (shorthand for post.data['title'])
  print(post.id);         // record id
}

// Filter
final filtered = await Koolbase.db
    .collection('posts')
    .where('status', 'published')
    .get();

// Relational data
final result = await Koolbase.db
    .collection('posts')
    .populate(['author_id:users'])
    .get();

// Update
await Koolbase.db.collection('posts').doc('record-id').update({'title': 'Updated'});

// Delete
await Koolbase.db.collection('posts').doc('record-id').delete();

Handling unique-constraint conflicts

A write that would violate a unique constraint throws KoolbaseConflictException:

try {
  await Koolbase.db.collection('users').insert({'email': email});
} on KoolbaseConflictException catch (e) {
  showError('That ${e.field ?? 'value'} is already registered.');
}

See Error handling for the full set of typed exceptions.

Upsert

Insert a record, or update the existing one matching a filter. The server decides: one match updates it, no match inserts (seeded with the match fields), more than one match errors.

final result = await Koolbase.db.upsert(
  collection: 'profiles',
  match: {'user_id': userId},
  data: {'weight_kg': 70},
);

print(result.created); // true if inserted, false if updated
print(result.record.id);

Online-only: an upsert needs the server's view to decide insert vs update, so unlike insert it isn't queued offline and throws on network failure.

Delete where

Bulk-delete every record matching a filter. Returns the number deleted.

final deleted = await Koolbase.db.deleteWhere(
  collection: 'sessions',
  filters: {'user_id': userId, 'status': 'expired'},
);

A non-empty filter is required — Koolbase won't delete an entire collection. The collection's delete rule applies; for owner/scoped rules the delete is scoped to your own records. Online-only.

Offline-first

The SDK caches all reads locally using Drift. Queries return instantly from cache and refresh in the background. Writes are queued and synced automatically when online.

final result = await Koolbase.db.collection('posts').get();
print(result.isFromCache); // true if served from local cache

await Koolbase.db.syncPendingWrites();

Storage

// Upload
await Koolbase.storage.upload(
  bucket: 'avatars',
  path: 'user-123.jpg',
  file: file,
  onProgress: (p) => print('${p.percentage}%'),
);

// Get download URL
final url = await Koolbase.storage.getDownloadUrl(
  bucket: 'avatars',
  path: 'user-123.jpg',
);

// Delete
await Koolbase.storage.delete(bucket: 'avatars', path: 'user-123.jpg');

Realtime

Subscribe to live changes on a collection. Records arrive in the flat shape — your fields are top-level; system metadata is under $-prefixed keys ($id, $createdAt, …).

// New records
final sub = Koolbase.realtime.onRecordCreated(
  projectId: 'your-project-id',
  collection: 'messages',
).listen((record) {
  print('New message: ${record['text']}');
});

// Updated records
Koolbase.realtime.onRecordUpdated(
  projectId: 'your-project-id',
  collection: 'messages',
).listen((record) => print('Updated: ${record['text']}'));

// Deleted records — the stream yields the deleted record's id
Koolbase.realtime.onRecordDeleted(
  projectId: 'your-project-id',
  collection: 'messages',
).listen((id) => print('Deleted: $id'));

// Stop listening
sub.cancel();

For full event objects (type, channel, timestamp), use the lower-level Koolbase.realtime.on(projectId: ..., collection: ...), which returns a Stream<RealtimeEvent>.


Functions

Invoke deployed serverless functions. When a user is signed in via Koolbase.auth, their access token is automatically forwarded — the function receives the caller's identity via ctx.auth. No token handling on the client side.

// Invoke a deployed function
final result = await Koolbase.functions.invoke(
  'send-welcome-email',
  body: {'userId': '123'},
);

if (result.success) print(result.data);

Inside the function, read the caller:

// In your deployed Dart function
Future<Map<String, dynamic>> handler(Map<String, dynamic> ctx) async {
  final userId = (ctx['auth'] as Map?)?['user_id'] as String?;
  if (userId == null) {
    return {'error': {'code': 'AUTH_REQUIRED'}, 'status': 401};
  }
  // Authenticated logic here
  return {'ok': true};
}

Token refresh is transparent — the SDK reads the current token fresh on every invoke. Full docs at docs.koolbase.com/functions/authentication.


Feature Flags & Remote Config

// Feature flag
if (Koolbase.isEnabled('new_checkout')) {
  // show new checkout
}

// Remote config
final timeout = Koolbase.configInt('api_timeout_ms', fallback: 3000);
final url = Koolbase.configString('api_url', fallback: 'https://api.example.com');
final dark = Koolbase.configBool('force_dark_mode', fallback: false);

Version Enforcement

final result = Koolbase.checkVersion();

switch (result.status) {
  case VersionStatus.forceUpdate:
    // Block the app — show update screen
    break;
  case VersionStatus.softUpdate:
    // Show a banner
    break;
  case VersionStatus.upToDate:
    break;
}

OTA Updates

final result = await Koolbase.ota.initialize(channel: 'production');

// Read a JSON file from the active bundle
final config = await Koolbase.ota.readJson('config.json');

// Get path to a bundled asset
final path = await Koolbase.ota.getFilePath('banner.png');

Code Push

Push config overrides, feature flag overrides, and UI updates to your app without a store release.

await Koolbase.initialize(KoolbaseConfig(
  publicKey: 'pk_live_xxxx',
  baseUrl: 'https://api.koolbase.com',
  codePushChannel: 'stable',
));

// Bundle values transparently override Remote Config + Feature Flags
final timeout = Koolbase.configInt('api_timeout_ms', fallback: 3000);
final enabled = Koolbase.isEnabled('new_checkout_flow');

// Directive handlers
Koolbase.codePush.onDirective('force_logout_all', (value) {
  if (value == true) Koolbase.auth.logout();
});

Server-Driven UI

Push new screen layouts OTA using Flutter's official rfw package. Change your app UI without shipping a new binary.

// Wrap your app
KoolbaseCodePushScope(
  client: Koolbase.codePush,
  child: MaterialApp(...),
)

// Drop a dynamic screen anywhere
KoolbaseDynamicScreen(
  screenId: 'onboarding',
  data: { 'username': user.name },
  onEvent: (name, args) {
    if (name == 'get_started') Navigator.pushNamed(context, '/home');
  },
  fallback: const OnboardingScreen(),
)

Logic Engine

Define conditional app behavior as data in your Runtime Bundle — no code changes required.

final result = Koolbase.executeFlow(
  flowId: 'on_checkout_tap',
  context: { 'plan': user.plan, 'usage': user.usage },
);

if (result.hasEvent) {
  switch (result.eventName) {
    case 'show_upgrade': Navigator.pushNamed(context, '/upgrade');
    case 'go_checkout': Navigator.pushNamed(context, '/checkout');
  }
}

v2 operators: eq, neq, gt, gte, lt, lte, contains, starts_with, ends_with, in_list, not_in_list, between, is_true, is_false, exists, not_exists, and, or

Full docs at docs.koolbase.com/sdk/logic-engine.


Analytics

Track screen views, custom events, and user behaviour. View DAU, WAU, MAU, funnels, and retention in the Koolbase dashboard.

// Add to MaterialApp for automatic screen tracking
MaterialApp(
  navigatorObservers: [
    KoolbaseNavigatorObserver(client: Koolbase.analytics),
  ],
)

// Custom events
Koolbase.analytics.track('purchase', properties: {
  'value': 1200,
  'currency': 'GHS',
});

// User identity
Koolbase.analytics.identify(user.id);
Koolbase.analytics.setUserProperty('plan', 'pro');

// On logout
Koolbase.analytics.reset();

Cloud Messaging

// Register FCM token
final fcmToken = await FirebaseMessaging.instance.getToken();
await Koolbase.messaging.registerToken(
  token: fcmToken!,
  platform: 'android', // or 'ios'
);

// Send to a specific device
await Koolbase.messaging.send(
  to: deviceToken,
  title: 'Your order is ready',
  body: 'Pick up at counter 3',
  data: {'order_id': '123'},
);

Error handling

Koolbase throws typed exceptions you can catch to branch on what went wrong. The SDK selects the exception from the server's stable error code, so your handling doesn't depend on message text.

Database errors

All data-layer failures extend KoolbaseDataException (which implements Exception), so you can catch them broadly or by specific type:

Exception When
KoolbaseConflictException A write violates a unique constraint (409). Exposes .field — the field that collided, when the server reports it.
KoolbaseNotFoundException The record or collection doesn't exist (404).
KoolbaseValidationException The request was rejected as invalid (400).
KoolbasePermissionException An access rule denied the operation (403).
KoolbaseRateLimitException The caller is being rate-limited (429).
try {
  await Koolbase.db.insert(
    collection: 'users',
    data: {'email': email},
  );
} on KoolbaseConflictException catch (e) {
  // e.field is 'email' when the server reports which field clashed
  showError('That ${e.field ?? 'value'} is already taken.');
} on KoolbasePermissionException {
  showError('You do not have permission to do that.');
} on KoolbaseDataException catch (e) {
  // Catch-all for any other data-layer error
  showError(e.message);
}

insert only queues offline on a genuine network failure. A server-side rejection (e.g. a unique conflict) surfaces immediately rather than being silently queued.

Auth errors

Auth methods throw KoolbaseAuthException subtypes — InvalidCredentialsException, AccountLockedException, EmailAlreadyInUseException, OtpExpiredException, and so on — also selected from the server's error code.


What's included

  • Authentication: email + password, Apple Sign-In, Google Sign-In, phone + OTP
  • Database with offline-first cache (Drift), realtime subscriptions, populate for related records
  • Storage with download URLs and progress callbacks
  • Realtime subscriptions over WebSocket
  • Authenticated Dart functions (ctx.auth exposes the caller automatically)
  • Feature flags and remote config
  • Version enforcement (force update, soft update)
  • OTA updates for assets, configs, and JSON
  • Code push (config + flag overrides + directives, no store release)
  • Server-driven UI via Flutter's rfw — push new screens OTA
  • Logic engine (conditional flows as data, updatable OTA)
  • Analytics (DAU/WAU/MAU, funnels, retention)
  • Cloud Messaging (FCM token registration, targeted send)
  • Flutter-first SDK with Dart-native APIs

Documentation

Full documentation at docs.koolbase.com

Dashboard

Manage your projects at app.koolbase.com

Support

License

MIT

Libraries

koolbase_flutter