Flutter Backend: Postgres, Auth, Storage and Push for Apps
A Flutter backend has to cover more than a web backend does: sign-in that comes back through a deep link, data that keeps working on a train with no signal, push notifications on two platforms and store purchases you can trust. StitchDB gives a Flutter app a dedicated Postgres database plus Auth, Storage, Realtime, Edge Functions, push and in-app purchase checks, and the stitchdb_flutter package talks to all of them from Dart. This guide walks through the setup and the parts that are specific to mobile.
If you just want the answer: add stitchdb_flutter, call Stitch.initialize in main(), protect your tables with Row Level Security, and use the anon key in the app. Everything else builds on that.
#What you get on the Dart side
| Need | Flutter API | Docs |
|---|---|---|
| Queries, inserts, RPC | stitchdb.from('todos')..., stitchdb.rpc(...) | Dart and Flutter SDK |
| Live lists | stitchdb.from('todos').stream(primaryKey: ['id']) | same |
| Sign-in | stitchdb.auth.*, hosted sign-in page | Auth |
| Files | stitchdb.storage.from('avatars') | Storage |
| Push | stitchdb.push.register(...) | Push notifications |
| Purchases | stitchdb.purchases.sync(...) | In-app purchases |
| Offline copy | client.offline(...) | Offline sync |
#1. Add the package
The Dart packages are installed from GitHub for now, not from pub.dev. The package plus overrides go in pubspec.yaml:
# pubspec.yaml
dependencies:
stitchdb_flutter:
git: { url: https://github.com/stitchdb/stitchdb-flutter, path: packages/stitchdb_flutter }
dependency_overrides:
stitchdb: { git: { url: https://github.com/stitchdb/stitchdb-flutter, path: packages/stitchdb } }
stitchdb_auth: { git: { url: https://github.com/stitchdb/stitchdb-flutter, path: packages/stitchdb_auth } }
stitchdb_common: { git: { url: https://github.com/stitchdb/stitchdb-flutter, path: packages/stitchdb_common } }
stitchdb_functions: { git: { url: https://github.com/stitchdb/stitchdb-flutter, path: packages/stitchdb_functions } }
stitchdb_postgrest: { git: { url: https://github.com/stitchdb/stitchdb-flutter, path: packages/stitchdb_postgrest } }
stitchdb_realtime: { git: { url: https://github.com/stitchdb/stitchdb-flutter, path: packages/stitchdb_realtime } }
stitchdb_storage: { git: { url: https://github.com/stitchdb/stitchdb-flutter, path: packages/stitchdb_storage } }Run flutter pub get. You need Dart 3.9 or newer. The plain stitchdb package (without _flutter) is for Dart servers and command-line tools.
#2. Initialize once
import 'package:stitchdb_flutter/stitchdb_flutter.dart';
Future<void> main() async {
await Stitch.initialize(url: 'https://<project-ref>.p.stitchdb.com', anonKey: '<anon-key>');
runApp(const MyApp());
}
final stitchdb = Stitch.instance.client;The anon key is fine to ship in the app. It only identifies your project; what a user can do is decided by policies in the database. The service_role key must never be in a Flutter build, because anyone can unpack an APK or IPA and read strings from it.
#3. A schema that protects itself
create table public.todos (
id bigint generated always as identity primary key,
user_id uuid not null default auth.uid() references auth.users on delete cascade,
task text not null,
is_complete boolean not null default false,
created_at timestamptz not null default now()
);
alter table public.todos enable row level security;
create policy "Users manage their own todos" on public.todos
for all to authenticated
using (user_id = auth.uid())
with check (user_id = auth.uid());With this policy, your Dart code never filters by user. The database does it on every request.
#4. Sign-in that returns to the app
Password sign-in works as you would expect:
await stitchdb.auth.signUp(email: '[email protected]', password: 'a-strong-password');
final res = await stitchdb.auth.signInWithPassword(email: '[email protected]', password: 'a-strong-password');
final user = stitchdb.auth.currentUser;Magic links and OAuth leave your app and open a browser, so they need a deep link to come back:
await stitchdb.auth.signInWithOtp(email: '[email protected]', emailRedirectTo: 'io.myapp://login-callback');
await stitchdb.auth.signInWithOAuth(OAuthProvider.github, redirectTo: 'io.myapp://login-callback');Register io.myapp://login-callback in two places: the project's redirect URLs in Studio, and your Android and iOS app configuration (an intent filter on Android, a URL type on iOS). stitchdb_flutter handles the incoming link and stores the session on the device.
To skip building auth screens, open the hosted sign-in page. It covers sign-in, sign-up, e-mail codes, password reset and two-step codes:
await stitchdb.auth.signInWithHostedPage(
redirectTo: 'io.myapp://login-callback',
view: HostedPageView.signUp, // optional: signIn, signUp, magicLink or forgot
lang: 'de', // optional
);React to auth changes with a stream, which fits naturally into a StreamBuilder or a state management library:
stitchdb.auth.onAuthStateChange.listen((data) {
final event = data.event; // AuthChangeEvent.signedIn, signedOut, tokenRefreshed, ...
final session = data.session;
});Two small mobile touches come for free. When the project turns on its built-in sign-up check, signUp, signInAnonymously and signInWithOtp solve it off the main isolate, so there is no captcha widget to add. And sign-ups send the device locale as locale in the user metadata, so auth e-mails go out in the user's language.
#5. Queries and live lists
final todos = await stitchdb
.from('todos')
.select('id, task, is_complete')
.eq('is_complete', false)
.order('created_at', ascending: false)
.range(0, 9);
final created = await stitchdb.from('todos').insert({'task': 'Ship it'}).select().single();
await stitchdb.from('todos').update({'is_complete': true}).eq('id', created['id']);Rows come back as List<Map<String, dynamic>>. For typed models, generate Dart classes from your schema; you get a class per table with fromJson and toJson, plus an insert class:
stitchdb gen types dart --linked -o lib/database_types.dartFor a screen that should update by itself, use stream():
StreamBuilder<List<Map<String, dynamic>>>(
stream: stitchdb.from('todos').stream(primaryKey: ['id']),
builder: (context, snapshot) {
final rows = snapshot.data ?? [];
return ListView(children: [for (final r in rows) Text(r['task'])]);
},
);Add the table to the realtime publication first (alter publication stitchdb_realtime add table public.todos;). Each user only receives rows their policies let them read.
#6. Files
final bucket = stitchdb.storage.from('avatars');
await bucket.upload('user-1/avatar.png', File('avatar.png'),
fileOptions: const FileOptions(contentType: 'image/png', upsert: true));
final publicUrl = bucket.getPublicUrl('user-1/avatar.png'); // public buckets
final signedUrl = await bucket.createSignedUrl('user-1/avatar.png', 60); // privateOn Flutter web, use uploadBinary with a Uint8List. Pair uploads with a storage policy that limits each user to their own folder, and use image resizing URLs to avoid downloading full-size photos into a list view. See the file uploads guide.
#7. Push notifications
StitchDB sends to APNs, Firebase Cloud Messaging and Web Push from your project's server, and keeps an in-app inbox per user. Get the device token with your usual Flutter push plugin, then register it after sign-in and whenever it changes:
await stitchdb.push.register(PushDevice.android(fcmToken, topics: ['news']));
await stitchdb.push.subscribe('sale');
final inbox = await stitchdb.push.inbox(unread: true);
await stitchdb.push.markAllRead();Sending needs the service role, so it happens on a server, in an Edge Function or straight from SQL:
select push.notify('<user-id>', 'Order shipped', 'Your order #1042 is on its way', '{"order": 1042}');The Free plan includes 10,000 pushes a month and Pro 1,000,000.
#8. In-app purchases
Never unlock paid features based on what the device says. Send each purchase to be checked with Apple or Google:
final r = await stitchdb.purchases.sync(
PurchaseInput.google(purchaseToken: token, productId: 'pro_monthly'),
);
await stitchdb.purchases.sync(PurchaseInput.apple(signedTransaction: jws));
if (await stitchdb.purchases.hasEntitlement('pro')) unlockPro();Then use the same entitlement in policies, so premium rows are protected at the database. This feature is included from the Pro plan; the purchase validation guide covers store setup.
#9. Working offline
Offline sync keeps a local copy of chosen tables, queues writes made without a connection and sends them later, with a conflict rule per table. The Flutter SDK has the same client as the JavaScript one, as client.offline(...), with streams for its events. Use uuid primary keys the device can generate, because tables with generated always as identity keys reject rows created offline with an id.
#10. Crashes and analytics
final crashes = Stitch.instance.client.crashes;
await crashes.start(release: '1.4.0', environment: 'production');
// FlutterError.onError and PlatformDispatcher.instance.onError are reported for youReports are grouped into issues and show the API call that failed just before the crash. Events and crashes are stored in your project's database, not a third-party service. More in crash reporting and analytics.
#Trade-offs
- Installation from GitHub. The Dart packages are git dependencies today, so you pin by commit or branch rather than a pub.dev version range.
- EU hosting only. Projects run in Germany and Finland. That suits apps with European users and GDPR concerns; users far from Europe will see higher latency.
- A younger platform. If you need a long track record or a huge community of Flutter tutorials, Firebase has more of both. StitchDB's argument is plain Postgres with SQL, policies you can read, and an export you can take elsewhere.
#FAQ
#Can I use the Supabase Flutter package instead?
StitchDB speaks the same REST, Auth, Storage and Realtime protocols as the open-source Supabase stack, so client libraries for it work by changing the URL and keys. The StitchDB package adds the platform features (push, purchases, flags, offline sync).
#How do I keep users signed in between app starts?
stitchdb_flutter stores the session on the device and refreshes the access token for you. Nothing else is needed.
#Where do I put server-side Dart code?
Run it anywhere you like with the plain stitchdb package and the service_role key, or write the server logic as a TypeScript Edge Function that runs next to your database.
#Does it work on Flutter web and desktop?
The Dart client runs wherever Dart does. On the web, use uploadBinary for files and add your web origin to the redirect URLs for OAuth.
Try it with your next Flutter build: start a free project.