Syncing
Use Tether to sync offline Laravel client apps on demand, from queue jobs, or on a schedule. Each sync cycle can push local mutations, pull authoritative server snapshots, report conflicts, and keep the local database reconciled.
The TetherClient facade
All sync operations are available on the TetherClient facade:
use Tether\Client\Facades\TetherClient;
$result = TetherClient::sync(); // push, then pull
$result = TetherClient::push(); // push only
$result = TetherClient::pull(); // pull only
SyncResult
All three methods return a Tether\Client\SyncResult object:
$result->pushed; // int - mutations applied on the server
$result->pulled; // int - snapshots applied locally
$result->conflicts; // int - conflicts returned by the server
$result->failed; // int - rejected mutations
$result->rejections; // list<PushRejection>
$result->pullErrors; // int - snapshots that failed to apply
$result->skipped; // bool - true if internal Tether lock prevented the cycle (see "Concurrency protection" below)
Use helpers to inspect structured rejections:
$result->rejectionsByReason('validation_failed');
$result->validationErrors();
Transport errors from Laravel's HTTP client calling your server bubble to the caller or queued job handler; they are not stored on SyncResult.
Artisan commands
php artisan tether:sync # push then pull
php artisan tether:push # push only
php artisan tether:pull # pull only
php artisan tether:status # queue counts and last cursor
Scheduling
Schedule automatic sync in routes/console.php or your console kernel:
use Illuminate\Support\Facades\Schedule;
Schedule::command('tether:sync')->everyFiveMinutes();
For mutation-triggered push, enable auto_sync:
// config/tether-client.php
'auto_sync' => true,
auto_sync dispatches PushJob after local model writes. PushJob is push-only, so inbound snapshots do not cause feedback loops.
Queue jobs
Two jobs are available:
use Tether\Client\Jobs\PushJob;
use Tether\Client\Jobs\PullJob;
PushJob::dispatch(); // push only
PullJob::dispatch(); // pull only
Both jobs use auto_sync_queue if set. PushJob can be coalesced with auto_sync_throttle, which uses Laravel's unique job lock to avoid queueing a job for every rapid local write.
How a push cycle works
- Failed mutations rejected with reason
errorare moved back topendingwhile belowmax_retry_attempts. - Pending mutations are loaded and chunked into batches of
push_batch_size. - Each batch is sent to
TETHER_SERVER_PUSH_URLas JSON. - The server responds with
applied,rejected, andconflicts. - Applied mutations are marked
synced. - Rejected mutations are marked
failed. - Conflicted mutations are marked
conflict, and the server state is applied locally.
How a pull cycle works
- The stored
last_sync_cursoris read fromtether_sync_state. - A pull request is sent to
TETHER_SERVER_PULL_URLwith the integer microsecond cursor, ornullfor a full pull. - The server returns
snapshots,new_sync_cursor, andhas_more. - Each snapshot is applied locally by
SnapshotApplicatorusingforceFill()andsave()insideModel::withoutEvents(). - The cursor is persisted in
tether_sync_state. - If
has_moreistrue, the cycle repeats with the new cursor until all pages are consumed.
Concurrency protection
All three methods use Laravel's atomic cache lock (tether_sync_lock, 60-second TTL) when sync_lock is enabled. If the lock is already held, the method returns immediately with skipped = true.
Cache driver requirement: Atomic locks require one of:
memcached,redis,dynamodb,database,file, orarray.