Building Offline-First Apps with Self-Hosted Supabase

Learn how to add offline-first capabilities to your self-hosted Supabase apps using PowerSync, ElectricSQL, and other sync solutions.

Cover Image for Building Offline-First Apps with Self-Hosted Supabase

Offline support has been one of the most requested features for Supabase—it's the most upvoted and most commented discussion in the entire Supabase GitHub organization. For developers building mobile apps or field applications with self-hosted Supabase, the question always comes up: how do you make it work offline?

The short answer is that Supabase doesn't have built-in offline support. But that doesn't mean you're stuck. Several excellent sync layers have emerged that integrate seamlessly with both Supabase Cloud and self-hosted instances, giving you true offline-first capabilities without changing your backend.

Why Offline-First Matters

Offline-first isn't just about working without WiFi. It's about building apps that are instant and responsive, regardless of network conditions. When your app reads and writes to a local database first, every interaction feels immediate. Network sync happens in the background.

This matters especially for:

  • Mobile apps where connectivity is unreliable
  • Field service applications used in warehouses, construction sites, or rural areas
  • Collaborative tools where users expect real-time updates
  • Progressive web apps that need to work anywhere

The traditional approach—showing spinners while waiting for server responses—creates a sluggish user experience. Local-first development eliminates that friction.

The Self-Hosted Advantage

When you self-host Supabase, you gain full control over your infrastructure. This extends to offline-first architecture. You can:

  • Self-host your sync service alongside Supabase
  • Keep all data on your infrastructure for compliance requirements
  • Customize sync behavior without vendor restrictions
  • Avoid per-MAU pricing on sync services

Let's look at the main options for adding offline capabilities to your self-hosted Supabase deployment.

Option 1: PowerSync (Recommended for Most Use Cases)

PowerSync is a drop-in sync layer built specifically for Supabase. It connects to your Postgres database via logical replication (WAL) and streams changes to local SQLite databases in your app.

How PowerSync Works

  1. PowerSync reads your Supabase Postgres WAL
  2. You define Sync Rules that specify which data each client receives
  3. The PowerSync SDK maintains a local SQLite database
  4. Your app reads/writes locally—sync happens automatically

The key advantage: PowerSync doesn't require schema changes or write permissions on your Supabase database. It's truly non-invasive.

Self-Hosting PowerSync

PowerSync Open Edition can be self-hosted alongside your Supabase instance. The setup involves:

# docker-compose.yml addition
powersync:
  image: journeyapps/powersync-service:latest
  environment:
    POWERSYNC_DATABASE_URL: postgres://postgres:password@db:5432/postgres
    POWERSYNC_JWT_SECRET: your-jwt-secret
  ports:
    - "8080:8080"

You'll need to configure sync rules that define how data flows to clients:

# sync-rules.yaml
bucket_definitions:
  user_data:
    parameters: SELECT token->>'sub' as user_id FROM jwt
    data:
      - SELECT * FROM todos WHERE user_id = bucket.user_id

Client SDK Integration

PowerSync provides SDKs for React Native, Flutter, Swift, Kotlin, and web:

import { PowerSyncDatabase } from '@powersync/web';
import { SupabaseConnector } from './SupabaseConnector';

const db = new PowerSyncDatabase({
  schema: AppSchema,
  database: { dbFilename: 'app.db' }
});

const connector = new SupabaseConnector();
await db.connect(connector);

// All reads are local and instant
const todos = await db.getAll('SELECT * FROM todos');

// Writes go to local DB, sync in background
await db.execute('INSERT INTO todos (id, title) VALUES (?, ?)', [uuid(), 'New task']);

PowerSync gives you Last-Write-Wins conflict resolution by default, with options to customize behavior for complex scenarios.

Option 2: ElectricSQL (Open Source, Self-Hosted)

ElectricSQL takes a different approach. It's fully open source, self-hosted, and provides what they call "formally proven" consistency guarantees through CRDT-based sync.

Setting Up ElectricSQL

ElectricSQL runs as a service alongside your Supabase Postgres:

docker run \
  -e DATABASE_URL=postgres://postgres:password@host:5432/postgres \
  -e ELECTRIC_WRITE_TO_PG_MODE=direct_writes \
  -p 3000:3000 \
  electricsql/electric

The Electric service uses Postgres logical replication to stream changes bidirectionally.

Shape-Based Sync

ElectricSQL uses "Shapes" to define partial replication—what data syncs to each client:

import { useShape } from '@electric-sql/react'

function TodoList({ projectId }) {
  const { data: todos } = useShape({
    url: 'http://localhost:3000/v1/shape/todos',
    where: `project_id = ${projectId}`
  })
  
  return todos.map(todo => <TodoItem key={todo.id} {...todo} />)
}

ElectricSQL's strength is its conflict-free programming model. You don't need to think about conflict resolution because the system handles it mathematically.

Trade-offs

ElectricSQL is newer and still maturing. The client SDKs are currently focused on web/TypeScript, with mobile support in development. For production mobile apps, PowerSync has more mature SDKs.

Option 3: WatermelonDB (DIY Approach)

If you prefer building your own sync logic, WatermelonDB is a reactive database for React Native that includes a sync protocol you can connect to Supabase.

The Architecture

WatermelonDB stores data in SQLite locally. You implement sync using Supabase RPC calls:

-- In Supabase, create push/pull functions
CREATE OR REPLACE FUNCTION sync_push(changes jsonb)
RETURNS void AS $$
BEGIN
  -- Process incoming changes
  -- Handle conflicts
  -- Update timestamps
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION sync_pull(last_pulled_at timestamp)
RETURNS jsonb AS $$
BEGIN
  -- Return all changes since last sync
END;
$$ LANGUAGE plpgsql;

Your app calls these via Supabase client:

const { data } = await supabase.rpc('sync_pull', { 
  last_pulled_at: lastSync 
});

When to Use WatermelonDB

This approach makes sense when:

  • You need complete control over sync logic
  • You're already using WatermelonDB
  • Your sync patterns are simple (basic CRUD, last-write-wins)
  • You want to avoid additional infrastructure

The downside: you're responsible for building and maintaining the sync logic, handling edge cases, and dealing with conflicts.

Option 4: RxDB (Cross-Platform JavaScript)

RxDB provides a Supabase replication plugin that handles sync automatically. It's a good choice for web apps and can work with React Native.

import { replicateSupabase } from 'rxdb/plugins/replication-supabase';

const replicationState = replicateSupabase({
  collection: myCollection,
  supabaseClient: supabase,
  table: 'todos',
  pull: {},
  push: {}
});

RxDB handles the replication protocol, using PostgREST for sync and Supabase Realtime for live updates.

Choosing the Right Solution

Here's a practical decision framework:

ScenarioRecommended Solution
Mobile app (React Native/Flutter) with complex syncPowerSync
Web app with real-time collaborationElectricSQL
Simple sync, existing WatermelonDBKeep WatermelonDB
JavaScript/TypeScript cross-platformRxDB
Maximum control, simple patternsBuild custom with Supabase RPC

For most teams deploying self-hosted Supabase, PowerSync offers the best balance of features, maturity, and ease of integration.

Managing Multiple Projects with Offline-First

If you're running multiple Supabase projects—perhaps one per customer or environment—you'll need to manage multiple sync service instances too. This is where infrastructure complexity grows.

Supascale simplifies managing multiple self-hosted Supabase projects with features like automated backups and one-click deployments. While offline sync requires additional services like PowerSync, having solid infrastructure management for your Supabase instances is the foundation.

Practical Considerations

Storage and Performance

Local SQLite databases grow with synced data. Plan for:

  • Selective sync: Only sync what users need
  • Pagination: Use PowerSync buckets or Electric shapes to limit data
  • Cleanup: Implement data retention policies client-side

Conflict Resolution

Most apps do fine with Last-Write-Wins. But if you're building collaborative tools:

  • Document your conflict strategy
  • Consider operational transforms for text
  • Test edge cases: simultaneous edits, offline for days, etc.

Security

Even with offline-first, security rules still apply:

  • Row Level Security protects data at the source
  • Sync rules (PowerSync) or shapes (Electric) filter what reaches clients
  • Local data should be encrypted on device

Conclusion

Offline-first isn't built into Supabase, but the ecosystem has excellent solutions. For self-hosted deployments, you can run the entire stack—Supabase, sync service, everything—on your own infrastructure.

PowerSync and ElectricSQL are the mature options. PowerSync is battle-tested for mobile apps; ElectricSQL offers a compelling open-source, CRDT-based approach. Both integrate cleanly with self-hosted Supabase.

The combination of self-hosted Supabase and a proper sync layer gives you the best of both worlds: Postgres reliability, full data control, and apps that work instantly whether online or off.


Further Reading