Service Container
Tether registers its offline sync runtime services in Laravel's service container. Most applications only need the model traits, config files, facade, and artisan commands, but the container services are useful when you want to wire the sync engine into your own application services, controllers, auth layer, or diagnostics UI.
As a rule of thumb, registries and high-level services are the intended extension points. Processors and applicators are lower-level implementation services; use them when building a custom sync surface, and only replace them when you intentionally want to change package behaviour.
Client services
These services are registered by tether/client.
| Service | Purpose | Common use |
|---|---|---|
SyncEngine | Runs sync(), push(), pull(), and syncStatus() | Inject into your own services or controllers |
SyncHttpClient | Sends push and pull HTTP requests to the server | Add auth headers, request signing, or HTTP middleware |
ClientSyncRegistry | Registers client-side payload and mutation mappers | Transform inbound snapshots or outbound mutations |
ClientIdResolver | Resolves the client_id sent with every sync request | Runtime device/user identity |
SyncStateStore | Persists local sync state such as last_sync_cursor | Status screens and diagnostics |
PendingSyncQueue | Reads and updates local mutation log state | Pending/failed/conflict counters |
Injecting the sync engine
SyncEngine is the service behind the TetherClient facade. Inject it when your own application code needs a concrete dependency.
namespace App\Services;
use Tether\Client\SyncEngine;
use Tether\Client\SyncResult;
class ManualSyncService
{
public function __construct(
private readonly SyncEngine $sync,
) {}
public function run(): SyncResult
{
return $this->sync->sync();
}
}
Use push() when you only want to send local mutations, and pull() when you only want to reconcile from the server.
SyncHttpClient
SyncHttpClient is the client-side HTTP transport used by SyncEngine. It is constructed from:
tether-client.server_routes.pushtether-client.server_routes.pull- the resolved client ID
It exposes:
$http->push($mutations); // returns PushResult
$http->pull($cursor, $limit); // returns PullResult
Most applications should not call those methods directly. The main developer-facing hook is withMiddleware(), which applies Guzzle-compatible middleware to every Tether push and pull request.
// AppServiceProvider::boot()
use GuzzleHttp\Middleware;
use Tether\Client\SyncHttpClient;
app(SyncHttpClient::class)->withMiddleware(
Middleware::mapRequest(
fn ($request) => $request->withHeader(
'Authorization',
'Bearer '.config('services.tether.token'),
),
),
);
Use this for bearer tokens, request signing, tenant headers, request logging, or any transport-level concern that should apply to both sync endpoints.
Client identity
The ClientIdResolver decides the client_id included in push and pull requests. Tether resolves it in this order:
- A runtime callable registered with
TetherClientServiceProvider::resolveClientIdUsing(). tether-client.client_id.- An auto-generated ULID persisted in
tether_sync_state.
// AppServiceProvider::boot()
use Tether\Client\TetherClientServiceProvider;
TetherClientServiceProvider::resolveClientIdUsing(
fn () => 'user-'.auth()->id(),
);
For config-cached applications, prefer an invokable class string in tether-client.client_id_resolver.
Client mappers
Use ClientSyncRegistry to transform data at the client boundary.
// AppServiceProvider::boot()
use App\Models\Task;
use Tether\Client\ClientSyncRegistry;
use Tether\Core\Mutation\Mutation;
use Tether\Core\Sync\Snapshot;
app(ClientSyncRegistry::class)->register(
modelClass: Task::class,
payloadMapper: fn (Snapshot $snapshot) => $snapshot,
mutationMapper: fn (Mutation $mutation) => $mutation,
);
Use payloadMapper for inbound pull snapshots before they are written locally. Use mutationMapper for outbound push mutations before they are sent to the server.
For more examples, see the Payload Mapping page.
Status and diagnostics
SyncStateStore and PendingSyncQueue are useful when building a status panel or debug page.
use Tether\Client\PendingSyncQueue;
use Tether\Client\SyncStateStore;
$pending = app(PendingSyncQueue::class)->count();
$failed = app(PendingSyncQueue::class)->failedCount();
$conflicts = app(PendingSyncQueue::class)->conflictCount();
$cursor = app(SyncStateStore::class)->get('last_sync_cursor');
For most UI cases, SyncEngine::syncStatus() is the simpler API because it returns the same information as a single DTO.
Server services
These services are registered by tether/server.
| Service | Purpose | Common use |
|---|---|---|
SyncRegistry | Registers syncable server models and callbacks | Main server extension point |
StateSnapshotGenerator | Builds pull snapshots from registered models | Custom pull controllers |
PushSyncProcessor | Applies push requests and records results | Custom push controllers |
ConflictDetector | Evaluates timestamp conflicts and resolvers | Internal unless replacing conflict behaviour |
MutationApplicator | Applies create/update/delete mutations to Eloquent | Internal unless replacing mutation application |
Registering server models
SyncRegistry is the primary server-side extension point. Use it when you want explicit registration instead of the server Syncable trait, or when you want to centralise model sync rules in a service provider.
// AppServiceProvider::boot()
use App\Models\Task;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Tether\Core\Conflict\ConflictResolution;
use Tether\Core\Mutation\Mutation;
use Tether\Core\Sync\Snapshot;
use Tether\Server\SyncRegistry;
app(SyncRegistry::class)->register(
modelClass: Task::class,
scope: fn (Builder $query, string $clientId, Request $request) => $query
->where('user_id', $request->user()->id),
pullSnapshotMapper: fn (Snapshot $snapshot, Task $row) => $snapshot,
pushMutationMapper: fn (Mutation $mutation, Request $request) => $mutation,
conflictResolver: fn (Mutation $mutation, Task $record, Request $request) => ConflictResolution::reject(),
);
If a model also uses the server Syncable trait, explicit SyncRegistry::register() calls take precedence over trait auto-registration.
Custom sync controllers
The default server package registers POST /tether/push and POST /tether/pull. If you disable route registration, you can build your own controller and inject the same services the package controller uses.
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Tether\Core\Sync\PullRequest;
use Tether\Core\Sync\PushRequest;
use Tether\Server\PushSyncProcessor;
use Tether\Server\StateSnapshotGenerator;
class CustomTetherController
{
public function push(Request $request, PushSyncProcessor $processor)
{
return response()->json(
$processor->process(PushRequest::fromArray($request->all()), $request)->toArray(),
);
}
public function pull(Request $request, StateSnapshotGenerator $snapshots)
{
return response()->json(
$snapshots->since(PullRequest::fromArray($request->all()), $request)->toArray(),
);
}
}
This is useful when you need a different route shape, additional authorization, tenant setup, or custom request logging while keeping Tether's core push and pull behaviour.
Replacing services
Because Tether uses Laravel's container, you can replace services in your application service provider. Do this sparingly: replacing processors, applicators, or detectors can change sync semantics.
// AppServiceProvider::register()
use Tether\Client\SyncHttpClient;
$this->app->extend(SyncHttpClient::class, function (SyncHttpClient $client) {
// Prefer withMiddleware() for transport changes.
return $client;
});
Prefer the built-in extension points first:
- Use
SyncHttpClient::withMiddleware()for transport concerns. - Use
ClientSyncRegistryandSyncRegistryfor payload transformation. - Use
SyncRegistryconflict resolvers for per-model conflict behaviour. - Use custom controllers with
PushSyncProcessorandStateSnapshotGeneratorfor route-level changes.
Features
Explore tether/pro-server features: sync observability, mutation inspection, conflict debugging, replay previews, live replay, and health telemetry.
Conflict Resolution
Learn how Laravel Tether detects offline sync conflicts, applies server-wins defaults, returns server state, and supports custom conflict resolvers.