effidevFlutter · Cloudflare edge · Cloud cost optimization
English

Flutter Supabase Offline-First: Drift & PowerSync

Flutter Supabase Offline-First Architecture with Drift and PowerSync

When mobile apps freeze on “Network Error” spinners in subways, elevators, or dead zones, users uninstall them immediately. The conventional pattern of ‘Network Request ➔ Read Cache on Failure’ makes frequent loading spinners and stale data exposure inevitable.

The global standard for mobile app development in 2026 is the Offline-First (Local-First) architecture. In an offline-first app, the local SQLite database inside the browser or device serves as the Single Source of Truth, allowing the UI to react instantly without needing to know the network state.

This article details how to build an enterprise offline-first architecture in a Flutter and Supabase (PostgreSQL) environment by combining Drift SQLite, the PowerSync sync engine, and Riverpod 3.0 to guarantee 100% offline responsiveness and real-time server synchronization.

Key Takeaways

  • Single Source of Truth: The UI never calls the Supabase API directly—it subscribes only to the local Drift SQLite database. This guarantees 0ms responsiveness on data changes.
  • Role of PowerSync Engine: Fully automates Partial Sync, Conflict Resolution, and Asynchronous Outbox processing between local SQLite and Supabase Postgres.
  • Riverpod 3.0 Integration: Subscribes to local SQLite binding streams via StreamProvider, updating the UI instantly even when offline.
  • Network Transparency: Users can continue creating and editing data seamlessly during Wi-Fi drops in subways, with changes automatically synced to the backend in the background upon reconnection.
  • Moving Beyond Legacy Architectures (e.g., Brick): Replacing simple HTTP caching or bloated wrapper libraries with the 2026 standard Drift + PowerSync stack prevents rendering jank and data loss 100%.

1. Offline-First Paradigm: Network Caching vs Local-First

Here is the decisive difference between legacy caching methods and the 2026 offline-first architecture.

[Legacy Network Caching] ❌
UI ──► Network Fetch (300ms spinner) ──(On Failure)──► Read Local Cache (Stale Data)

[2026 Offline-First Architecture] ⭕️
UI ──(0ms Instant Read/Write)──► [Drift Local SQLite (Single Source of Truth)]

                                           │ (Background Sync 0ms ~ When Connected)

                                   [PowerSync Engine]

                                           │ (Real-time Sync)

                                  [Supabase PostgreSQL]

In an offline-first structure, network drops impose zero limitations on app behavior. All data reads and writes occur instantly on the device’s internal Drift SQLite database with 0ms UI response, while the PowerSync engine records changes in a background outbox queue and syncs them to Supabase as soon as connectivity is restored.

2. Drift SQLite + PowerSync + Riverpod 3.0 Architecture Layers

Layer Role & Components Key Features
UI & State Layer Flutter UI + Riverpod 3.0 (NotifierProvider) Subscribes only to local Drift DB Stream regardless of network status
Local Persistence Drift (SQLite ORM) Single Source of Truth. Type-safe Dart SQLite database
Sync Engine PowerSync Client Real-time delta sync & conflict resolution between local SQLite ↔ Supabase Postgres
Remote Database Supabase (PostgreSQL) RLS (Row Level Security)-based multi-tenant data store

3. Step-by-Step Offline-First Implementation

Step 1: Defining the Drift SQLite Table (lib/database/app_database.dart)

A type-safe local DB definition compliant with the Drift Official Documentation.

import 'package:drift/drift.dart';
import 'package:drift/native.dart';

part 'app_database.g.dart';

class Todos extends Table {
  TextColumn get id => text()();
  TextColumn get title => text()();
  BoolColumn get isCompleted => boolean().withDefault(const Constant(false))();
  DateTimeColumn get updatedAt => dateTime()();

  @override
  Set<Column> get primaryKey => {id};
}

@DriftDatabase(tables: [Todos])
class AppDatabase extends _$AppDatabase {
  AppDatabase() : super(NativeDatabase.memory());

  @override
  int get schemaVersion => 1;

  // Expose local DB changes via a 0ms stream
  Stream<List<Todo>> watchAllTodos() {
    return (select(todos)..orderBy([(t) => OrderingTerm.desc(t.updatedAt)])).watch();
  }
}

Step 2: Configuring PowerSync Offline Schema & Conflict Resolution (lib/sync/powersync.dart)

Setting up the Supabase offline background sync connector via the PowerSync Official Documentation.

import 'package:powersync/powersync.dart';

final schema = Schema([
  Table('todos', [
    Column.text('title'),
    Column.integer('is_completed'),
    Column.text('updated_at'),
  ])
]);

late final PowerSyncDatabase db;

Future<void> initPowerSync(String supabaseUrl, String anonKey) async {
  db = PowerSyncDatabase(schema: schema, path: 'app_powersync.db');
  await db.initialize();

  // Connect Supabase connector (performs background sync when connected)
  final connector = SupabaseConnector(url: supabaseUrl, anonKey: anonKey);
  await db.connect(connector: connector);
}

Step 3: Subscribing to Reactive UI with Riverpod 3.0 (lib/providers/todo_provider.dart)

Combining the AsyncNotifier and Stream reactive patterns covered in our Riverpod 3.0 Guide, this setup ensures instant UI responsiveness regardless of network connection status.

import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../database/app_database.dart';

final databaseProvider = Provider<AppDatabase>((ref) => AppDatabase());

// Riverpod 3.0 StreamProvider subscribing to local Drift DB Stream in real time
final todoListProvider = StreamProvider.autoDispose<List<Todo>>((ref) {
  final db = ref.watch(databaseProvider);
  return db.watchAllTodos();
});

// When adding a Todo from UI: Instant insert into local DB without network request
class TodoNotifier extends AutoDisposeAsyncNotifier<void> {
  @override
  Future<void> build() async {}

  Future<void> addTodo(String title) async {
    final db = ref.read(databaseProvider);
    await db.into(db.todos).insert(
      TodosCompanion.insert(
        id: DateTime.now().millisecondsSinceEpoch.toString(),
        title: title,
        updatedAt: DateTime.now(),
      ),
    );
  }
}

4. Testing Network Disconnection and Reconnection

To validate an offline-first app, you must run it through a Network Manipulation Test.

[Test Scenario]
1. Enable Airplane Mode (offline) ➔ Write 5 new notes in app ➔ Confirm 0ms instant UI update ⭕️
2. Relaunch app after closing (offline state) ➔ Confirm 100% local data persistence ⭕️
3. Reconnect Wi-Fi ➔ PowerSync automatically syncs to Supabase Postgres in background ⭕️
4. Supabase Dashboard ➔ Confirm RLS policy application & clean data transfer without conflicts ⭕️

Applying this architecture eliminates UI thread rendering jank (as highlighted in our Flutter DevTools Memory Leak and CPU Profiling Guide), maintaining a seamless 60 to 120 fps frame rate.

5. 2026 Offline-First Library Selection Guide

Library Offline-First Fit Evaluation & Recommended Reason
PowerSync + Drift ★★★★★ (Recommended) 2026 Best Practice. Perfect harmony of partial sync, conflict resolution, and Drift type safety
Drift + Custom Outbox ★★★★☆ (Custom) Excellent when building a custom sync queue and REST/GraphQL connector without third-party services
Supabase Realtime Cache ★★☆☆☆ (Not Recommended) Offline writes impossible. Tailored only for real-time subscriptions when connected
Brick ORM ★☆☆☆☆ (Not Recommended) Maintenance avoided in 2026 due to unsynced server deletes and complex boilerplate

Frequently Asked Questions

What happens if multiple devices modify the same data while offline?

The PowerSync engine automatically resolves conflicts in the background using LWW (Last-Write-Wins) timestamp ordering or CRDT (Conflict-free Replicated Data Type) algorithms. If custom business logic is required, you can easily control it by defining Supabase Postgres trigger functions or custom PowerSync conflict handlers.

What if the local SQLite database grows too large?

By utilizing PowerSync’s Partial Sync Rules, you can selectively download only the data you currently need (e.g., data from the last 30 days or user-owned data) to the local device, minimizing memory and storage usage.

Is offline data security safe with Supabase?

Applying the sqlcipher encryption engine to the local Drift SQLite database file prevents decryption of the database file even in jailbroken or rooted environments. In addition, Supabase server-side RLS (Row Level Security) policies perform secondary validation upon reconnection, maintaining strict security.

Does offline-first work identically on the Flutter Web platform?

Yes, it does. In a Flutter Web environment, Drift automatically switches to a Wasm + IndexedDB backend, creating an offline SQLite database inside the browser. Web Wasm rendering performance is covered in detail in our Flutter Web Wasm + Skwasm Guide.