Plexivo Postgres for Flutter and Dart
plexivo_client is the client for Plexivo Postgres: your database, its Data API, authentication, realtime over WebSockets, GraphQL, functions and file storage.
Use it Supabase-style (from('notes').select().eq('pinned', true)) or GORM-style: models, relations, preload, whereHas, first, create, save, hooks, scopes, soft deletes, transactions, association mode, and migrations and raw SQL on your server. Row-level security decides what every request can read and write.
dependencies:
plexivo_client: ^0.2.0
import 'package:plexivo_client/plexivo_client.dart';
final db = PlexivoClient(dataUrl, publishableKey);
await db.signInWithPassword(email: email, password: password);
final posts = await db.from('posts')
.where('likes', '>', 10)
.preload('author')
.order('created_at', 'desc')
.limit(20);
print(posts.first['title']);
Your data URL and publishable key are on the database's Connect tab. The publishable key belongs in your app. The secret key is for servers only: it bypasses row-level security and runs raw SQL and migrations. Never ship it in a Flutter app.
Queries
A query is immutable and chainable; await runs it. Results are a PlexivoResult: a list of rows that also carries count.
db.from('posts')
.where('likes', '>', 10)
.where({'published': true, 'deleted_at': null}) // null → IS NULL
.orWhere('title', 'ilike', '%postgres%') // AND binds tighter than OR
.where((PlexivoConditions q) => q.where('a', 1).orWhere('b', 'in', [2, 3]))
.whereNot('author_id', [1, 2])
.whereBetween('score', 1, 5)
.whereJson('meta', 'lang.code', 'hi')
.whereContains('tags', ['postgres'])
.whereSearch('body', 'fast cars', 'english')
.whereHas('comments', (PlexivoEmbed c) => c.where('approved', true))
.select('id, title as heading')
.order('likes', 'desc', 'last')
.page(2, 20);
// Supabase-style
final res = await db.from('notes')
.select('id, body, author:authors(name)', 'exact')
.eq('pinned', true)
.or('priority.gte.3,body.ilike.*urgent*')
.range(0, 19);
print(res.count);
final one = await db.from('notes').select().eq('id', 1).single(); // one.data
is and in are Dart keywords, so their PostgREST filters are isFilter and inFilter.
Reading: first(id?), last(), take(), firstOrNull(), sole(), find([ids]), pluck('col'), count(), exists(), sum/avg/min/max, aggregate(...), paginate(page:, perPage:), findInBatches(500, (rows, i) {...}), await for (final row in q.each()). first() throws RecordNotFoundException when nothing matches.
Writes
final post = await db.from('posts').create({'title': 'Hello'});
await db.from('posts').createMany([{'title': 'a'}, {'title': 'b'}]);
await db.from('products').upsert({'sku': 'A1', 'price': 11}, onConflict: 'sku', update: ['price']);
await db.from('posts').save({'id': 7, 'title': 'New title'});
await db.from('posts').where('id', 7).update({'title': 'T'});
await db.from('posts').update({'title': 'T'}).eq('id', 7); // Supabase order works too
await db.from('products').where('sku', 'A1').decrement('stock', by: 2);
await db.from('posts').where('id', 7).delete();
await db.from('products').firstOrCreate({'sku': 'D4'}, {'name': 'Desk'});
An update or delete must say which rows it touches (missing_where otherwise); allowGlobalUpdate() is the opt-in.
Models, relations, transactions
final post = db.model('posts',
softDelete: 'deleted_at',
relations: {
'author': const PlexivoRelation.belongsTo('authors', foreignKey: 'author_id'),
'comments': const PlexivoRelation.hasMany('comments', foreignKey: 'post_id'),
'tags': const PlexivoRelation.manyToMany('tags', through: 'post_tags', foreignKey: 'post_id', otherKey: 'tag_id'),
},
hooks: PlexivoHooks(beforeCreate: (row) => {...row, 'slug': slugify(row['title'])}),
scopes: {'popular': (q) => q.where('likes', '>=', 10)},
);
final rows = await post.query()
.scopes(['popular'])
.preload('author', (PlexivoEmbed a) => a.select('name'))
.preload('tags')
.withCount('comments')
.joins('comments', (PlexivoEmbed c) => c.where('approved', true));
await post.association(row, 'tags').append([tagId]);
// All or nothing, in one request; .ref() passes a result to the next step.
await db.transaction((tx) {
final author = tx.from('authors').insert({'name': 'Asha'});
tx.from('posts').insert({'title': 'Hi', 'author_id': author.ref('id')});
tx.from('products').where('sku', 'B2').lockForUpdate().find();
});
Realtime over WebSockets
final feed = db.channel('changes:orders', 'status=eq.open')..subscribe();
feed.changes.listen((c) => print('${c.type} ${c.record}')); // insert | update | delete
final room = db.channel('room:lobby', null, {'name': 'Asha'})..subscribe();
room.on('cursor').listen((p) => print(p));
await room.ready();
room.sendBroadcast('cursor', {'x': 10});
room.presence.listen((p) => print(p.state));
final stop = await db.graphqlSubscribe('subscription { notes { body } }', (r) => print(r['data']));
Each subscriber receives only the rows their row-level security lets them read. Channels reconnect on their own.
Auth
signUp, signInWithPassword, signInWithOtp/verifyOtp, signInAnonymously + linkEmail, getOAuthSignInUrl(provider: 'google' | 'github' | 'microsoft') + exchangeCodeForSession(code), password recovery, mfa.enroll/verify, listSessions/revokeSession, signOut, authStateChanges, and orgs (create, invite, acceptInvitation, members, setRole). Keep sessions in encrypted device storage: pass initialSession, and write updated sessions from authStateChanges.
Server: raw SQL and migrations (secret key)
final server = PlexivoClient(dataUrl, Platform.environment['PLEXIVO_SECRET_KEY']!);
await server.raw(r'select * from orders where total > $1', [100]);
await server.migrator.autoMigrate([postModel, commentModel]); // creates what is missing, never drops
await server.migrate([PlexivoMigration.sql('001_slug', 'alter table posts add column slug text', 'alter table posts drop column slug')]);
Also
db.rpc('fn', {...}), db.graphql(query, persisted: true), db.functions.invoke('name', body: {...}), db.storage.upload/download, db.getApi(). Errors are PlexivoException with code, message, hint, sqlstate, traceId and, in a transaction, operation.
Development
dart test
dart analyze
API_URL=… PUBLISHABLE_KEY=… SECRET_KEY=… dart run tool/e2e_frontend.dart # against a live project
Libraries
- plexivo_client
- The client for Plexivo Postgres: queries (Supabase-style and GORM-style), models and relations, transactions, auth, storage, realtime, GraphQL, functions, and — with the secret key, on a server — raw SQL and migrations.