Skip to content
Document DB v7: Temporal Support and Telemetry Collections! Feed The Machine Here

Removal Strategies

Shiny.Data.Sync provides three layered strategies for removing entities the client should no longer have. All are optional and composable — combine any of them on the same endpoint.

StrategyWhen to useWhat the server provides
Inline Delete verbThe default — the server emits a delete record in the same inbox stream{ "verb": "Delete", "id": "..." } items in the pull response
TombstonesThe server can’t merge deletes into the main pullA separate URL returning ["id1","id2",...] (or a paginated { cursor, ids })
Soft-delete predicateThe server signals deletes via a flag on the entityA boolean on the entity (e.g. IsDeleted)
Expiry predicateA state change should evict the entity even though the server didn’t mark it deletedAny condition on the deserialized entity (e.g. AssignedTo == null)

This is the default. Whenever the server includes { "verb": "Delete", "id": "...", "payload": null } in its inbox response, the engine dispatches that as a SyncReceivedItem with Verb = Delete and Entity = null.

{
"cursor": "2026-06-09T10:00:00Z",
"hasMore": false,
"items": [
{ "id": "abc", "verb": "Update", "payload": { ... } },
{ "id": "xyz", "verb": "Delete" }
]
}

Some servers can’t merge deletes into the main pull (legacy CRUD APIs, separate audit pipelines, etc.). Point the endpoint at a dedicated delete URL with TombstoneUrl:

opts.RegisterEndpoint<TodoItem>("https://api.example.com/todos", ep =>
{
ep.TombstoneUrl = "https://api.example.com/todos/deleted";
ep.TombstoneCursorParameter = "since"; // default
});

After every successful pull, the engine issues a GET against the tombstone URL. Each returned id is dispatched to OnReceived with Verb = Delete and Entity = null. The cursor for the tombstone stream is persisted independently from the main pull cursor (SyncTombstoneCursor), so the two streams can advance at different rates.

The default RestSyncTransport accepts either of two response shapes:

["id1", "id2", "id3"]

or, when the server paginates / cursors deletes separately:

{ "cursor": "<opaque next cursor>", "ids": ["id1","id2","id3"] }

On iOS / Mac Catalyst the tombstone fetch also rides the background NSURLSession (tombstone:{endpointKey} task description), so it survives suspension just like the main pull.

When the server returns the entity itself but flags it as deleted, register a predicate. The engine evaluates it on the deserialized entity inside the inbox dispatch loop and — when it returns true — rewrites the verb to Delete before the delegate sees it:

opts.RegisterEndpoint<Project>("https://api.example.com/projects", ep =>
{
ep.SoftDeletePredicate = entity => entity is Project p && p.IsArchived;
});

Entity stays populated on the SyncReceivedItem, so consumers can read the final state on the way out the door (useful for showing “this project was archived” in a notification).

Like soft-delete, but for server-driven state changes that aren’t literal deletes — e.g. a work order that becomes unassigned from the current user. The engine treats it identically to a soft-delete: verb rewritten to Delete, entity preserved.

opts.RegisterEndpoint<WorkOrder>("https://api.example.com/workorders", ep =>
{
ep.ExpiryPredicate = entity => entity is WorkOrder wo && wo.AssignedTo != currentUser.Id;
});

You can layer all of them on one endpoint for maximum coverage:

opts.RegisterEndpoint<WorkOrder>("https://api.example.com/workorders", ep =>
{
// Real-time: catch soft-deletes and unassigned work orders during pull
ep.SoftDeletePredicate = wo => wo is WorkOrder w && w.IsDeleted;
ep.ExpiryPredicate = wo => wo is WorkOrder w && w.AssignedTo == null;
// Safety net: a separate stream of deleted IDs from the audit pipeline
ep.TombstoneUrl = "https://api.example.com/workorders/deleted";
});

The order of evaluation per inbox item is: deserialize → soft-delete predicate → expiry predicate → dispatch. After the main pull finishes, tombstones are fetched as a separate request and dispatched.

On iOS / Mac Catalyst the tombstone request rides the same background session as the main pull, so even a brief foreground window is enough to kick it off — the OS will complete the download after suspension. On Android the tombstone fetch piggy-backs on the foreground service while it’s running; on Windows / Linux / macOS it just runs inline in the inbox loop.