Concorde is an offline-first sync platform for Rails 8 applications. It provides automatic data synchronization between clients and servers using CRDT (Conflict-free Replicated Data Types) principles, leveraging Rails 8's Solid Stack (SolidCable and SolidQueue) for real-time updates and background processing.
- Offline-First: Full functionality even without internet connection
- Automatic Sync: Changes sync automatically when connection is restored
- Conflict Resolution: Built-in CRDT-based conflict resolution (Last Write Wins by default)
- Real-Time Updates: Uses SolidCable for instant sync notifications
- Easy Integration: Simple setup with Rails models using
include Concorde::Syncable - Client SDK: JavaScript SDK with IndexedDB storage for browser apps
- Multi-Tab Support: Synchronization across browser tabs
- Minimal Dependencies: Uses Rails 8's built-in Solid Stack (no Redis required)
Add this line to your application's Gemfile:
gem "concorde"Then execute:
$ bundle install
$ rails generate concorde:install
$ rails db:migrateThe installer will:
- Create an initializer at
config/initializers/concorde.rb - Copy migrations for sync tables
- Mount the engine in your routes
- Configure SolidCable for real-time sync
- Optionally create an example Todo model with sync enabled
Add the Syncable concern to any model you want to sync:
class Task < ApplicationRecord
include Concorde::Syncable
# Optionally exclude certain attributes from sync
sync_exclude :internal_notes, :admin_only_field
endIn your JavaScript:
// Initialize Concorde
const concorde = new Concorde({
apiUrl: '/concorde/sync',
clientId: localStorage.getItem('concorde_client_id') || undefined,
userId: currentUserId
});
// Save client ID for future sessions
localStorage.setItem('concorde_client_id', concorde.config.clientId);
// Start syncing
await concorde.start();// Create a record (works offline)
const task = await concorde.create('Task', {
title: 'Buy milk',
completed: false
});
// Update a record
await concorde.update('Task', task.id, { completed: true });
// Delete a record
await concorde.delete('Task', task.id);
// Query records
const allTasks = await concorde.all('Task');
const pendingTasks = await concorde.where('Task', { completed: false });
const specificTask = await concorde.find('Task', taskId);
// Listen for changes
concorde.on('change', (event) => {
console.log('Data changed:', event);
refreshUI();
});
// Check sync status
console.log('Is online:', concorde.isOnline);
console.log('Is syncing:', concorde.isSyncing);
console.log('Pending changes:', concorde.pendingChanges);Configure Concorde in config/initializers/concorde.rb:
Concorde.configure do |config|
# Models to enable for sync
config.sync_models = [Task, Project, User]
# Custom conflict resolution
config.conflict_resolution_rules['Task'] = {
'priority' => ->(old_val, new_val) { [old_val, new_val].max },
'title' => ->(old_val, new_val) { new_val.length > old_val.length ? new_val : old_val }
}
# Authentication/Authorization
config.authorization_adapter = ->(request) {
request.env['warden'].authenticated?
}
# Callbacks
config.on_conflict = ->(change_log) {
Rails.logger.info "Conflict resolved: #{change_log.inspect}"
}
config.on_push_receive = ->(mutation, user) {
# Validate or authorize mutations
raise "Unauthorized" unless user.can_edit?(mutation['record_type'])
}
end- Local Changes: When offline, all changes are stored in IndexedDB and applied optimistically to the UI
- Change Log: The server maintains an append-only log of all mutations
- Sync Protocol:
- Push: Client sends pending mutations to server
- Pull: Client fetches changes since last sync
- Conflict Resolution: When concurrent changes occur, they're resolved using CRDT rules
- Real-Time: SolidCable broadcasts notifications when data changes
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Browser │ │ Browser │ │ Mobile │
│ (Client 1) │ │ (Client 2) │ │ Client │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
│ Push/Pull │ WebSocket │
└───────────────────┴────────────────────┘
│
┌──────┴──────┐
│ Rails 8 │
│ Server │
│ │
│ ┌─────────┐ │
│ │Concorde │ │
│ │ Engine │ │
│ └─────────┘ │
└──────┬──────┘
│
┌──────┴──────┐
│ PostgreSQL │
│ or │
│ MySQL │
└─────────────┘
# In your model
class Document < ApplicationRecord
include Concorde::Syncable
sync_conflict_strategy :custom_merge
end
# In config/initializers/concorde.rb
Concorde.configure do |config|
config.conflict_resolution_rules['Document'] = {
'content' => ->(old_content, new_content) {
# Custom merge logic (e.g., three-way merge for text)
merge_text(old_content, new_content)
}
}
endWhen making changes on the server that should sync to clients:
# Changes are automatically tracked
task = Task.create!(title: "Server-created task")
# For bulk operations, you can disable sync temporarily
Task.without_sync do
Task.import(large_dataset)
end// Subscribe to specific data streams
const concorde = new Concorde({
userId: currentUser.id,
channels: ['personal', `team_${teamId}`]
});Concorde includes test helpers for your application tests:
# In your test
class TaskSyncTest < ActionDispatch::IntegrationTest
include Concorde::TestHelper
test "syncs task between clients" do
# Simulate offline client
with_offline_client do |client|
client.create('Task', title: 'Offline task')
end
# Sync and verify
sync_all_clients
assert_equal 1, Task.count
end
end- Database Indexes: Ensure proper indexes on high-traffic tables
- Change Log Pruning: Set up a job to archive old change logs
- Security: Implement proper authentication and authorization
- Monitoring: Monitor sync performance and conflicts
- Check browser console for errors
- Verify WebSocket connection is established
- Ensure client ID is being sent with requests
- Review your conflict resolution rules
- Consider using field-level timestamps
- Implement custom merge strategies for complex data
- Enable change log pruning
- Add database indexes for your sync queries
- Consider pagination for large datasets
Bug reports and pull requests are welcome on GitHub at https://github.com/felixclack/concorde.
The gem is available as open source under the terms of the MIT License.