orbitnest_studio_flutter 1.6.3
orbitnest_studio_flutter: ^1.6.3 copied to clipboard
Official Flutter SDK for OrbitNest Studio — auth (MFA, passkeys, SMS), Postgres queries, realtime, storage, and vector search for your backend.
OrbitNest Studio Flutter #
The official Flutter client for OrbitNest Studio — a Postgres-backed application platform. One SDK for authentication, database queries, realtime, storage, edge functions, background jobs, and migrations, with a Supabase-compatible API for easy migration.
The client is built on the BLoC pattern internally, but you don't need to know BLoC to use it: every feature is exposed through plain async/await methods. Reactive state streams are available when you want them.
Contents #
- Installation
- Configuration
- Quick start
- Authentication
- Database
- Storage
- Realtime
- Edge functions
- Background jobs
- Migrations
- Reactive state with BLoC
- Error handling
- Migrating from Supabase
- API surface
Installation #
dependencies:
orbitnest_studio_flutter: ^1.5.0
flutter pub get
Configuration #
A client app authenticates with your project's anon key — a public, RLS-protected key. The project slug and API URL are encoded inside the anon key JWT and resolved automatically; you don't set them separately.
Copy .env.example to .env and set your key:
# From OrbitNest Studio → Settings → API Keys
ORBITNEST_ANON_KEY=your_anon_key_here
# Verbose SDK logging — leave false/unset in production
ORBITNEST_DEBUG=false
# Optional. HTTP timeout in ms (default 180000). Local dev base URL override.
# ORBITNEST_API_TIMEOUT=180000
# ORBITNEST_API_URL=http://localhost:3002
Register .env as an asset in your app's pubspec.yaml:
flutter:
assets:
- .env
Security. This is a client SDK. Ship only the anon key. A service-role / admin key is server-side and would be extractable from the installed app binary, so it must never be bundled. Backend management — creating tables, policies, functions, or jobs — is done from the OrbitNest Studio dashboard or admin tooling, not from this SDK.
Quick start #
import 'package:orbitnest_studio_flutter/orbitnest_studio_flutter.dart';
Future<void> main() async {
await EnvConfig.initialize(); // load .env
final orbitnest = OrbitNestClient.create();
// Or pass the key explicitly:
// final orbitnest = OrbitNestClient.create(anonKey: 'your_anon_key');
final users = await orbitnest
.from('users')
.select('id, name, email')
.eq('status', 'active')
.limit(20)
.execute();
print(users.data);
orbitnest.dispose();
}
The client exposes each capability as a sub-API:
orbitnest.auth // authentication
orbitnest.database // database CRUD + query builder
orbitnest.storage // file storage
orbitnest.realtime // live subscriptions, broadcast, presence
orbitnest.functions // edge function invocation
orbitnest.jobs // background jobs
orbitnest.migrations // server-side migration runs
Authentication #
OrbitNest supports password, email OTP, SMS OTP, TOTP-based multi-factor auth, and passkeys (WebAuthn). All methods return plain maps/objects and throw AuthException on failure.
Password #
// Sign up sends a verification OTP to the email.
await orbitnest.auth.signUp(email: 'user@example.com', password: 'secret123');
final result = await orbitnest.auth.signInWithPassword(
email: 'user@example.com',
password: 'secret123',
);
final user = result['user'] as User;
await orbitnest.auth.resetPasswordForEmail('user@example.com');
await orbitnest.auth.resetPassword(
email: 'user@example.com',
token: '123456',
newPassword: 'newSecret123',
);
Email OTP #
await orbitnest.auth.signInWithEmail('user@example.com'); // sends a code
await orbitnest.auth.verifySignIn(email: 'user@example.com', otp: '123456');
// Sign-up variants: signUpWithEmail(...) then verifySignUp(...).
SMS OTP #
Requires SMS to be configured for your project. Phone numbers are E.164 (e.g. +15555550123).
await orbitnest.auth.signInWithSms('+15555550123');
await orbitnest.auth.verifySmsOtp(phone: '+15555550123', code: '123456');
Multi-factor authentication (TOTP) #
// Enroll an authenticator app.
final enroll = await orbitnest.auth.enrollMfaTotp(friendlyName: 'My phone');
// enroll['qr_code'] / enroll['otpauth_url'] — show to the user to scan.
final verified = await orbitnest.auth.verifyMfaEnrollment(
factorId: enroll['factor_id'],
code: '123456',
);
final recoveryCodes = verified['recovery_codes']; // shown once — store safely
// When a password sign-in is MFA-gated, complete it with the challenge token:
await orbitnest.auth.verifyMfa(challengeToken: '...', code: '123456');
await orbitnest.auth.listMfaFactors();
await orbitnest.auth.regenerateMfaRecoveryCodes();
await orbitnest.auth.unenrollMfa(factorId: '...');
Passkeys (WebAuthn) #
if (await orbitnest.auth.isPasskeySupported()) {
// New user: create an account and register a passkey in one ceremony.
await orbitnest.auth.signUpWithPasskey(email: 'user@example.com');
// Returning user:
await orbitnest.auth.signInWithPasskey(identifier: 'user@example.com');
// For an already-authenticated user:
await orbitnest.auth.registerPasskey(deviceName: 'iPhone');
await orbitnest.auth.listPasskeys();
await orbitnest.auth.renamePasskey(deviceId: '...', deviceName: 'Work phone');
await orbitnest.auth.revokePasskey(deviceId: '...');
}
Session #
orbitnest.auth.isAuthenticated; // bool
orbitnest.auth.currentUser; // User?
orbitnest.auth.currentSession; // Session?
await orbitnest.auth.refreshSession();
await orbitnest.auth.signOut();
// React to changes (optional).
orbitnest.auth.onAuthStateChange.listen((state) { /* ... */ });
// Fires when the server rejects the session and it can't be recovered.
orbitnest.onSessionExpired.listen((_) => goToLogin());
Sessions are stored in encrypted device storage. Tokens refresh automatically ahead of expiry.
Database #
A Supabase-compatible query builder plus direct CRUD helpers. Queries run against your existing tables and respect Row Level Security.
Query builder #
final posts = await orbitnest
.from('posts')
.select('*, author:users(name)')
.eq('published', true)
.gte('created_at', '2024-01-01')
.order('created_at', ascending: false)
.range(0, 49) // pagination
.execute();
await orbitnest.from('users').insert({'name': 'Jane', 'email': 'jane@x.io'});
await orbitnest.from('users').upsert({'id': 1, 'name': 'Jane'}, onConflict: 'id');
await orbitnest.from('users').update({'status': 'inactive'}).execute(); // filter, then update
await orbitnest.from('users').delete().execute();
Filters: eq, neq, gt, gte, lt, lte, like, ilike, isFilter, isNull, isNotNull, inFilter, contains, containedBy, overlaps, the range* family, textSearch, match, not, or, and, filter.
Modifiers: order, limit, range, single, maybeSingle.
Direct CRUD #
final res = await orbitnest.database.select('users',
columns: 'id, name', filters: {'status': 'active'}, limit: 10);
print(res.data);
await orbitnest.database.insert('users', {'name': 'Sam'});
await orbitnest.database.update('users', {'age': 31}, filters: {'id': 1});
await orbitnest.database.delete('users', filters: {'id': 1});
await orbitnest.database.insertMany('users', [ {...}, {...} ]);
await orbitnest.database.updateMany('users', {'tier': 'pro'}, filters: {'plan': 'paid'});
await orbitnest.database.deleteMany('users', filters: {'status': 'deleted'});
Vector search (pgvector) #
final matches = await orbitnest.database.vectorSearch(
'documents',
'embedding',
[0.12, 0.04, /* ... */],
metric: 'cosine', // 'l2' (default), 'cosine', or 'ip'
limit: 5,
);
Raw SQL #
final result = await orbitnest.database.sql(
'SELECT name, COUNT(*) FROM posts GROUP BY name ORDER BY count DESC LIMIT ?',
parameters: [10],
);
Storage #
Bucket-scoped file operations. getPublicUrl builds a URL locally (no network) and can request on-the-fly image transforms.
final bucket = orbitnest.storage.from('avatars');
await bucket.upload(path: 'u/1.png', bytes: pngBytes, contentType: 'image/png', upsert: true);
final bytes = await bucket.download('u/1.png');
final files = await bucket.list(prefix: 'u/');
await bucket.remove(['u/1.png']);
final url = bucket.getPublicUrl('u/1.png', transform: StorageTransform(
width: 200, height: 200, format: 'webp', quality: 80, fit: 'cover',
));
Realtime #
Subscribe to Postgres changes, send broadcast messages, and track presence over a single WebSocket.
final channel = orbitnest.realtime.channel('room:42');
channel
.onPostgresChanges(
event: PgEvent.insert,
table: 'messages',
filter: RealtimeFilter(column: 'room_id', op: 'eq', value: '42'),
callback: (payload) => print('new message: ${payload.newRecord}'),
)
.onBroadcast(event: 'typing', callback: (p) => print(p.payload))
.onPresenceSync((state) => print('online: ${state.keys}'));
await channel.subscribe();
await channel.track({'user': 'jane'}); // presence
await channel.send(event: 'typing', payload: {'user': 'jane'});
await channel.unsubscribe();
await orbitnest.realtime.dispose();
Edge functions #
The SDK invokes edge functions. (Creating and managing functions is done in the OrbitNest Studio dashboard, not from the client.)
final res = await orbitnest.functions.invoke('send-email', body: {
'to': 'user@example.com',
'subject': 'Welcome',
});
print(res.data);
// HTTP-verb helpers:
await orbitnest.functions.get('health');
await orbitnest.functions.post('charge', body: {'amount': 100});
await orbitnest.functions.put('profile', body: {'name': 'Jane'});
await orbitnest.functions.delete('session');
// Shorthand on the client:
await orbitnest.function('send-email', params: {'to': 'user@example.com'});
Background jobs #
Trigger and inspect scheduled server-side jobs. (Job authoring is an admin operation.)
await orbitnest.jobs.trigger('nightly-report');
final runs = await orbitnest.jobs.getRuns('nightly-report', limit: 20);
final all = await orbitnest.jobs.list();
Migrations #
The SDK never runs migrations on-device — it triggers server-side runs and reads their status, which is useful for in-app admin or diagnostics screens.
final result = await orbitnest.migrations.run(); // apply all pending
final status = await orbitnest.migrations.status(); // applied / pending / failed
// MigrationLogController drives a live log view:
final controller = MigrationLogController(orbitnest.migrations);
await controller.refreshStatus();
await controller.runMigrations();
print(controller.logs);
Reactive state with BLoC #
Every sub-API also exposes a state stream, and the underlying BLoCs are available for flutter_bloc integration. Use these only when you want UI that reacts to state automatically — the async/await API above covers most apps.
orbitnest.auth.onAuthStateChange.listen((state) { /* ... */ });
orbitnest.database.onStateChange.listen((state) { /* ... */ });
// Direct BLoC access for BlocProvider / BlocBuilder:
orbitnest.authBloc;
orbitnest.databaseBloc;
orbitnest.functionsBloc;
Error handling #
All operations throw a subclass of OrbitNestException on failure:
try {
await orbitnest.auth.signInWithPassword(email: e, password: p);
} on AuthException catch (e) {
print('${e.code}: ${e.message}');
} on DatabaseException catch (e) {
print(e.message);
} on FunctionException catch (e) {
print(e.message);
}
Transient network failures (timeouts, connection errors, and 408/429/5xx responses) are retried automatically with exponential backoff. Logs redact tokens, keys, and other sensitive fields in both debug and release builds.
Migrating from Supabase #
The query builder and client shape mirror supabase_flutter, so most data code is unchanged.
// Before
await Supabase.initialize(url: url, anonKey: key);
final db = Supabase.instance.client;
// After
await EnvConfig.initialize();
final db = OrbitNestClient.create();
// Identical query syntax:
final rows = await db.from('users').select('*').eq('status', 'active').execute();
API surface #
| Area | Entry point | Notes |
|---|---|---|
| Auth | orbitnest.auth |
password, email/SMS OTP, MFA (TOTP), passkeys, sessions |
| Database | orbitnest.from(table), orbitnest.database |
query builder, CRUD, bulk, pgvector, raw SQL |
| Storage | orbitnest.storage.from(bucket) |
upload, download, list, remove, public URLs + transforms |
| Realtime | orbitnest.realtime.channel(name) |
Postgres changes, broadcast, presence |
| Functions | orbitnest.functions |
invoke only (invoke, call, get/post/put/delete) |
| Jobs | orbitnest.jobs |
trigger, getRuns, list, get |
| Migrations | orbitnest.migrations |
run, status (server-side) |
| Config | EnvConfig |
.env loading, runtime settings |
Call orbitnest.dispose() when the client is no longer needed to release the HTTP client and any open realtime connection.
License #
MIT — see LICENSE.