ad-go / sync
Active-active database synchronisation for two CodeIgniter 4 instances over HTTPS (no JSON, no schema changes to existing tables).
Requires
- php: ^8.2
- codeigniter4/framework: ^4.7
- codeigniter4/queue: ^1.0
- codeigniter4/shield: ^1.4
- codeigniter4/tasks: ^1.0
Requires (Dev)
- phpunit/phpunit: ^10.5 || ^11.0
Suggests
- codeigniter4/settings: Its shared table can be synchronised through a 'where' filter
Provides
None
Conflicts
None
Replaces
None
README
Active-active synchronisation of CodeIgniter 4 applications over HTTPS: two or more nodes keep their databases and a shared directory identical, including nodes behind NAT. No JSON on the wire, no schema changes to existing tables, no SSH or root needed.
- Database sync. A change journal and a per-row registry (uuid, revision counters, last agreed state) drive a three-way reconciliation: fast-forward, field-level merge, last-writer-wins for tolerant tables, quarantine for sensitive ones.
- File sync. A directory (with sub-directories) is mirrored with a manifest, SHA-256 verification and atomic writes; concurrent edits keep both versions.
- Topologies. Two dial-in nodes, a hub with passive nodes behind NAT (outbound-only), and relaying hubs.
- Operations. Conflict admin UI, Telegram and e-mail alerts, a reachability check (
sync:check), a distributed edit lock for Shield users. - Built on the official packages. Shield, Tasks, Queue, CodeIgniter's Email, Migrations, Language and Services.
Requires PHP 8.2+ and CodeIgniter 4.7. Tested with Shield 1.4, Tasks 1.0, Queue 1.0. Portable across MySQL/MariaDB, PostgreSQL and SQLite (query builder and Forge only).
Quick start
On every node:
composer require ad-go/sync
php spark sync:install # publishes app/Config/Sync.php, migrates, seeds the registry
.env (the secret is identical on all nodes; node names are not):
sync.node = site1 sync.peerNode = site2 sync.peerUrl = https://www.example-b.com sync.secret = <64+ random characters>
Declare what is synchronised in app/Config/Sync.php:
public array $tables = [ 'categories' => ['policy' => 'tolerant'], 'products' => [ 'policy' => 'tolerant', 'sensitive_fields' => ['price'], 'fk' => ['category_id' => 'categories'], ], ];
Journal writes made through models by adding the trait:
class ProductModel extends \CodeIgniter\Model { use \Q1\Sync\Traits\Syncable; }
Schedule the jobs in app/Config/Tasks.php:
public function init(Scheduler $schedule) { \Q1\Sync\Tasks\SyncTasks::register($schedule); }
and keep the usual single cron entry: * * * * * php /path/to/spark tasks:run.
Exempt the API from CSRF ('csrf' => ['except' => ['sync-api/*']]) and serve it over HTTPS only.
Documentation
| Document | Contents |
|---|---|
| docs/ARCHITECTURE.md | Data model, reconciliation algorithm, transport, file sync |
| docs/CONFIGURATION.md | Every .env key and table option |
| docs/INTEGRATION.md | Shield, Tasks, Queue, Email, Settings, Services, Language |
| docs/OPERATIONS.md | Commands, NAT and relays, reachability check, locks, conflicts, housekeeping |
| docs/sync-db.html | Illustrated walkthrough of the database sync |
| docs/sync-files.html | Illustrated walkthrough of the file sync |
Tests
composer install
composer test
The suite runs against in-memory SQLite. The real HTTPS round trip, the Telegram and SMTP calls and the admin views are exercised by hand.
Limits
- Two nodes cannot be strictly consistent under a network partition without a third voter; the design minimises, logs and quarantines conflicts instead of hiding them.
- Last-writer-wins depends on the nodes' clocks: keep NTP running, and mark data where that matters as
sensitive. - Fields in
ignore_fieldsare not transferred and need database defaults. - Out-of-band writes (query builder, raw SQL,
insertBatch) are found by the scanner, not instantly.
License
MIT