local_first_websocket
A real-time bidirectional synchronization strategy plugin for the LocalFirst framework. This plugin provides instant, WebSocket-based synchronization for building collaborative and real-time applications.
Note: This is a companion package to
local_first. You need to install the core package first.
Why local_first_websocket?
- Real-time synchronization: Changes propagate instantly to all connected clients
- Bidirectional sync: Both push (local → remote) and pull (remote → local) operations
- Production-ready: Automatic reconnection, heartbeat monitoring, and event queuing
- Flexible authentication: Support for tokens and custom headers with dynamic updates
- Connection resilience: Handles network issues gracefully with configurable retry logic
- Developer-friendly: Clear protocol, comprehensive logging, and easy integration
Features
- ✅ Instant Sync: Changes are pushed immediately via persistent WebSocket connection
- ✅ Automatic Reconnection: Handles connection loss with configurable retry delay
- ✅ Event Queue: Queues pending events during disconnection for later sync
- ✅ Heartbeat Monitoring: Keeps connection alive with periodic ping/pong messages
- ✅ Connection State Tracking: Reports connection status to the UI in real-time
- ✅ Dynamic Authentication: Update tokens and headers without reconnecting
- ✅ Conflict Resolution: Integrates with LocalFirst's conflict resolution strategies
- ✅ Event Acknowledgment: Server confirms successful event processing
Installation
Add the dependency to your pubspec.yaml:
dependencies:
local_first: ^0.6.0
local_first_websocket: ^1.0.0
# Choose your storage adapter
local_first_hive_storage: ^0.2.0 # or
local_first_sqlite_storage: ^0.2.0
Then install it with:
flutter pub get
Quick Start
import 'package:local_first/local_first.dart';
import 'package:local_first_websocket/local_first_websocket.dart';
// 1) Create WebSocket sync strategy
final wsStrategy = WebSocketSyncStrategy(
websocketUrl: 'ws://your-server.com/sync',
reconnectDelay: Duration(seconds: 3),
heartbeatInterval: Duration(seconds: 30),
authToken: 'your-auth-token', // Optional
);
// 2) Initialize client with WebSocket strategy
final client = LocalFirstClient(
repositories: [userRepository, todoRepository],
localStorage: HiveLocalFirstStorage(),
syncStrategies: [wsStrategy],
);
await client.initialize();
await wsStrategy.start();
// 3) Listen to connection state changes
wsStrategy.connectionChanges.listen((isConnected) {
print('WebSocket ${isConnected ? "connected" : "disconnected"}');
});
// 4) Use repositories normally - sync happens automatically
await todoRepository.upsert(
Todo(id: '1', title: 'Buy milk'),
needSync: true, // Event pushed immediately to server
);
How It Works
Architecture

Push Flow
When data changes locally:
- User Action: Create/update/delete data via repository
- Event Creation: LocalFirst creates event with
SyncStatus.pending - Immediate Push:
WebSocketSyncStrategy.onPushToRemote()is called - Send or Queue:
- If connected: sends event via WebSocket immediately
- If disconnected: adds event to pending queue
- Server Acknowledgment: Server responds with ACK message
- Mark Synced: Strategy marks event as
SyncStatus.ok
Pull Flow
When server has new data:
- Server Push: Server sends
eventsmessage with new/updated data - Apply Locally: Strategy calls
pullChangesToLocal()to apply changes - Conflict Resolution: LocalFirst merges remote changes, handling conflicts
- Confirm Receipt: Strategy sends confirmation to server
- UI Update: Repository streams automatically notify UI
Reconnection Flow
When connection is lost:
- Connection Lost: Network issue, server restart, etc.
- Report State: Strategy reports
connectionState: false - Queue Events: Pending events are queued locally
- Retry Logic: After
reconnectDelay, attempts to reconnect - Catch Up: On reconnect, requests missed events via
request_events - Flush Queue: Sends all pending events to server
- Resume: Reports
connectionState: true, normal operation resumes
Configuration Options
Reconnection Delay
Control how long to wait before attempting to reconnect:
final wsStrategy = WebSocketSyncStrategy(
websocketUrl: 'ws://your-server.com/sync',
reconnectDelay: Duration(seconds: 5), // Wait 5s before reconnecting
);
Recommendations:
- 📱 Mobile apps: 3-5 seconds (balance between responsiveness and battery)
- 💻 Desktop apps: 2-3 seconds (faster reconnection expected)
- 🌐 Web apps: 3-5 seconds (similar to mobile)
Heartbeat Interval
Configure how often to send ping messages to keep connection alive:
final wsStrategy = WebSocketSyncStrategy(
websocketUrl: 'ws://your-server.com/sync',
heartbeatInterval: Duration(seconds: 60), // Ping every 60s
);
Recommendations:
- Default: 30 seconds (good balance)
- Behind proxy/load balancer: 20-30 seconds (prevent timeout)
- Direct connection: 45-60 seconds (reduce overhead)
Custom Headers
Add authentication or custom headers:
final wsStrategy = WebSocketSyncStrategy(
websocketUrl: 'ws://your-server.com/sync',
headers: {
'Authorization': 'Bearer your-token',
'X-Custom-Header': 'value',
},
);
Authentication Token
Simplified authentication with token:
final wsStrategy = WebSocketSyncStrategy(
websocketUrl: 'ws://your-server.com/sync',
authToken: 'your-auth-token',
);
Dynamic Authentication Updates
Update authentication credentials without disconnecting:
Update Only Token
wsStrategy.updateAuthToken('new-token-here');
Update Only Headers
wsStrategy.updateHeaders({
'Authorization': 'Bearer new-token',
'X-Custom-Header': 'value',
});
Update Both at Once
wsStrategy.updateCredentials(
authToken: 'new-token',
headers: {'X-Custom-Header': 'value'},
);
Note: If the WebSocket is currently connected, updating credentials will automatically re-authenticate with the server using the new credentials.
Example: JWT Token Refresh
class AuthService {
final WebSocketSyncStrategy wsStrategy;
Timer? _refreshTimer;
AuthService(this.wsStrategy);
void startTokenRefresh() {
// Refresh token every 50 minutes (assuming 60-minute expiry)
_refreshTimer = Timer.periodic(Duration(minutes: 50), (_) async {
final newToken = await refreshJWTToken();
wsStrategy.updateAuthToken(newToken);
print('Token refreshed and WebSocket re-authenticated');
});
}
void stopTokenRefresh() {
_refreshTimer?.cancel();
}
Future<String> refreshJWTToken() async {
// Your token refresh logic here
final response = await http.post(
Uri.parse('https://api.example.com/auth/refresh'),
headers: {'Authorization': 'Bearer $oldRefreshToken'},
);
return jsonDecode(response.body)['token'];
}
}
WebSocket Server Protocol
The server must implement the following message protocol:
Client → Server Messages
Authentication
{
"type": "auth",
"token": "your-auth-token"
}
Heartbeat
{
"type": "ping"
}
Push Single Event
{
"type": "push_event",
"repository": "users",
"event": {
"eventId": "event-uuid",
"operation": 0,
"createdAt": "2025-01-23T10:00:00.000Z",
"data": { ... }
}
}
Push Multiple Events (Batch)
{
"type": "push_events_batch",
"repository": "users",
"events": [ ... ]
}
Request Events Since Timestamp
{
"type": "request_events",
"repository": "users",
"since": "2025-01-23T10:00:00.000Z"
}
Request All Events
{
"type": "request_all_events"
}
Confirm Events Received
{
"type": "events_received",
"repository": "users",
"count": 5
}
Server → Client Messages
Authentication Success
{
"type": "auth_success"
}
Heartbeat Response
{
"type": "pong"
}
Send Events to Client
{
"type": "events",
"repository": "users",
"events": [
{
"eventId": "event-uuid",
"operation": 0,
"createdAt": "2025-01-23T10:00:00.000Z",
"data": { ... }
}
]
}
Acknowledge Received Events
{
"type": "ack",
"eventIds": ["event-uuid-1", "event-uuid-2"],
"repositories": {
"users": ["event-uuid-1"],
"todos": ["event-uuid-2"]
}
}
Sync Complete
{
"type": "sync_complete",
"repository": "users"
}
Error
{
"type": "error",
"message": "Error description"
}
Comparison with Other Strategies
| Feature | WebSocketSyncStrategy | PeriodicSyncStrategy |
|---|---|---|
| Sync Timing | Real-time on event | Periodic intervals |
| Push Pattern | Immediate | Batched |
| Pull Pattern | Server-initiated | Client-initiated |
| Connection | Stateful WebSocket | Stateless HTTP/REST |
| Best For | Real-time collaboration, chat, live updates | REST APIs, batch sync, polling |
| Complexity | Moderate | Simple |
| Battery Usage | Higher (always connected) | Low (configurable interval) |
| Latency | Lowest (~10-50ms) | Medium (depends on interval) |
| Scalability | Requires WebSocket support | Works with any REST API |
Running the Example
The example includes a complete WebSocket server implementation using the Dart server and MongoDB.
Prerequisites
- Flutter SDK (>= 3.10.0)
- Docker (for MongoDB)
- Dart SDK (for server)
1. Start the Server
From the monorepo root, run:
melos server:start
This command automatically:
- ✅ Starts MongoDB with Docker Compose
- ✅ Starts the Dart WebSocket server on port 8080
- ✅ Configures networking between services
- ✅ Shows real-time logs
Or manually:
# Start MongoDB
docker run -d --name local_first_mongodb -p 27017:27017 \
-e MONGO_INITDB_ROOT_USERNAME=admin \
-e MONGO_INITDB_ROOT_PASSWORD=admin mongo:7
# Start the server
cd server && dart run websocket_server.dart
The server will start on ws://localhost:8080/sync
2. Run the Flutter App
cd local_first_websocket/example
flutter pub get
flutter run
Best Practices
1. Handle Connection State in UI
Show connection status to users:
StreamBuilder<bool>(
stream: wsStrategy.connectionChanges,
builder: (context, snapshot) {
final isConnected = snapshot.data ?? false;
return Banner(
message: isConnected ? 'Connected' : 'Offline',
color: isConnected ? Colors.green : Colors.red,
);
},
)
2. Implement Token Refresh
For JWT or expiring tokens, refresh before expiry:
// Refresh every 50 minutes for 60-minute tokens
Timer.periodic(Duration(minutes: 50), (_) async {
final newToken = await refreshToken();
wsStrategy.updateAuthToken(newToken);
});
3. Use Appropriate Heartbeat Intervals
- Too frequent: Wastes battery and bandwidth
- Too infrequent: Connection may be closed by proxies
- Recommended: 30 seconds for most scenarios
4. Handle Offline Mode Gracefully
Let LocalFirst handle offline state:
// Events are automatically queued when offline
await todoRepository.upsert(todo, needSync: true);
// Will sync automatically when connection is restored
5. Monitor Performance
Log sync times to identify bottlenecks:
wsStrategy.connectionChanges.listen((isConnected) {
if (isConnected) {
final syncStart = DateTime.now();
// Monitor how long catch-up sync takes
}
});
Troubleshooting
WebSocket Connection Fails
Symptoms: App shows "disconnected", no syncing happens
Solutions:
- Verify server is running and accessible:
curl http://localhost:8080/api/health - Check WebSocket URL format:
// Correct websocketUrl: 'ws://localhost:8080/sync' // NOT websocketUrl: 'http://localhost:8080/sync' - For mobile/emulator, use correct host:
- Android emulator:
ws://10.0.2.2:8080/sync - iOS simulator:
ws://localhost:8080/sync - Physical device:
ws://192.168.x.x:8080/sync
- Android emulator:
Authentication Fails
Symptoms: Connection opens but immediately closes
Solutions:
- Verify auth token is valid
- Check server logs for authentication errors
- Ensure server responds with
auth_successmessage
Events Not Syncing
Symptoms: Local changes don't appear on server
Solutions:
- Ensure
needSync: truewhen creating/updating data - Check connection state is
true - Enable verbose logging:
flutter run -v - Verify server ACK messages are being sent
Frequent Reconnections
Symptoms: Connection constantly dropping and reconnecting
Solutions:
- Increase heartbeat interval if behind proxy:
heartbeatInterval: Duration(seconds: 20) - Check network stability
- Verify server timeout settings match heartbeat interval
High Battery Usage
Symptoms: App draining battery quickly
Solutions:
- Increase heartbeat interval to reduce overhead
- Consider using
PeriodicSyncStrategyinstead for less critical data - Pause sync when app is backgrounded:
wsStrategy.stop(); // When app backgrounds wsStrategy.start(); // When app resumes
Duplicate Events
Symptoms: Same event appears multiple times
Solutions:
- Verify server implements event deduplication by
eventId - Ensure server sends ACK for received events
- Check LocalFirst marks events as synced after ACK
Advanced Usage
Multiple Strategies
Combine WebSocket for real-time data with periodic sync for background data:
final client = LocalFirstClient(
repositories: [messageRepo, userRepo, settingsRepo],
localStorage: HiveLocalFirstStorage(),
syncStrategies: [
// Real-time for messages
WebSocketSyncStrategy(
websocketUrl: 'ws://api.example.com/sync',
// ... config
),
// Periodic for settings
PeriodicSyncStrategy(
syncInterval: Duration(minutes: 5),
repositoryNames: ['settings'],
// ... config
),
],
);
Custom Error Handling
Listen to connection changes and handle errors:
wsStrategy.connectionChanges.listen(
(isConnected) {
if (!isConnected) {
// Show retry button, enable offline mode, etc.
showOfflineBanner();
} else {
hideOfflineBanner();
}
},
onError: (error) {
logger.error('WebSocket error: $error');
showErrorDialog('Connection error: $error');
},
);
Contributing
Contributions are welcome. See CONTRIBUTING.md for guidelines.
Support the Project 💰
Your contributions help us enhance and maintain our plugins. Donations are used to procure devices and equipment for testing compatibility across platforms and versions.
License
This project is available under the MIT License. See LICENSE for details.
Libraries
- local_first_websocket
- Real-time bidirectional synchronization strategy for LocalFirst framework using WebSockets.

