[{"data":1,"prerenderedAt":522},["ShallowReactive",2],{"navigation":3,"\u002Fdocs\u002Fv1\u002Fhow-it-works":121,"\u002Fdocs\u002Fv1\u002Fhow-it-works-surround":517},[4],{"title":5,"path":6,"stem":7,"children":8,"page":44},"Docs","\u002Fdocs","docs",[9],{"title":10,"path":11,"stem":12,"children":13,"page":44},"V1","\u002Fdocs\u002Fv1","docs\u002Fv1",[14,18,22,45,58,80,93,111],{"title":15,"path":16,"stem":17},"Introduction","\u002Fdocs\u002Fv1\u002Fintroduction","docs\u002Fv1\u002F0.introduction",{"title":19,"path":20,"stem":21},"How It Works","\u002Fdocs\u002Fv1\u002Fhow-it-works","docs\u002Fv1\u002F1.how-it-works",{"title":23,"icon":24,"path":25,"stem":26,"children":27,"page":44},"Client","i-lucide-laptop","\u002Fdocs\u002Fv1\u002Fclient","docs\u002Fv1\u002F2.client",[28,32,36,40],{"title":29,"path":30,"stem":31},"Client Setup","\u002Fdocs\u002Fv1\u002Fclient\u002Fsetup","docs\u002Fv1\u002F2.client\u002F1.setup",{"title":33,"path":34,"stem":35},"Syncing","\u002Fdocs\u002Fv1\u002Fclient\u002Fsyncing","docs\u002Fv1\u002F2.client\u002F2.syncing",{"title":37,"path":38,"stem":39},"Events","\u002Fdocs\u002Fv1\u002Fclient\u002Fevents","docs\u002Fv1\u002F2.client\u002F3.events",{"title":41,"path":42,"stem":43},"Configuration","\u002Fdocs\u002Fv1\u002Fclient\u002Fconfiguration","docs\u002Fv1\u002F2.client\u002F4.configuration",false,{"title":46,"icon":47,"path":48,"stem":49,"children":50,"page":44},"Server","i-lucide-server","\u002Fdocs\u002Fv1\u002Fserver","docs\u002Fv1\u002F3.server",[51,55],{"title":52,"path":53,"stem":54},"Server Setup","\u002Fdocs\u002Fv1\u002Fserver\u002Fsetup","docs\u002Fv1\u002F3.server\u002F1.setup",{"title":41,"path":56,"stem":57},"\u002Fdocs\u002Fv1\u002Fserver\u002Fconfiguration","docs\u002Fv1\u002F3.server\u002F2.configuration",{"title":59,"icon":60,"badge":61,"path":62,"stem":63,"children":64,"page":44},"NativePHP","i-lucide-smartphone","Commercial","\u002Fdocs\u002Fv1\u002Fnativephp","docs\u002Fv1\u002F4.nativephp",[65,69,73,76],{"title":66,"path":67,"stem":68},"Overview","\u002Fdocs\u002Fv1\u002Fnativephp\u002Foverview","docs\u002Fv1\u002F4.nativephp\u002F1.overview",{"title":70,"path":71,"stem":72},"Setup","\u002Fdocs\u002Fv1\u002Fnativephp\u002Fsetup","docs\u002Fv1\u002F4.nativephp\u002F2.setup",{"title":41,"path":74,"stem":75},"\u002Fdocs\u002Fv1\u002Fnativephp\u002Fconfiguration","docs\u002Fv1\u002F4.nativephp\u002F3.configuration",{"title":77,"path":78,"stem":79},"Lifecycle & Events","\u002Fdocs\u002Fv1\u002Fnativephp\u002Flifecycle","docs\u002Fv1\u002F4.nativephp\u002F4.lifecycle",{"title":81,"icon":82,"badge":61,"path":83,"stem":84,"children":85,"page":44},"Server Pro","i-lucide-bar-chart-2","\u002Fdocs\u002Fv1\u002Fserver-pro","docs\u002Fv1\u002F5.server-pro",[86,89],{"title":66,"path":87,"stem":88},"\u002Fdocs\u002Fv1\u002Fserver-pro\u002Foverview","docs\u002Fv1\u002F5.server-pro\u002F1.overview",{"title":90,"path":91,"stem":92},"Features","\u002Fdocs\u002Fv1\u002Fserver-pro\u002Ffeatures","docs\u002Fv1\u002F5.server-pro\u002F2.features",{"title":94,"icon":95,"path":96,"stem":97,"children":98,"page":44},"Advanced","i-lucide-settings-2","\u002Fdocs\u002Fv1\u002Fadvanced","docs\u002Fv1\u002F6.advanced",[99,103,107],{"title":100,"path":101,"stem":102},"Service Container","\u002Fdocs\u002Fv1\u002Fadvanced\u002Fservice-container","docs\u002Fv1\u002F6.advanced\u002F0.service-container",{"title":104,"path":105,"stem":106},"Conflict Resolution","\u002Fdocs\u002Fv1\u002Fadvanced\u002Fconflict-resolution","docs\u002Fv1\u002F6.advanced\u002F1.conflict-resolution",{"title":108,"path":109,"stem":110},"Payload Mapping","\u002Fdocs\u002Fv1\u002Fadvanced\u002Fpayload-mapping","docs\u002Fv1\u002F6.advanced\u002F2.payload-mapping",{"title":112,"icon":113,"path":114,"stem":115,"children":116,"page":44},"Examples","i-lucide-book-open","\u002Fdocs\u002Fv1\u002Fexamples","docs\u002Fv1\u002F7.examples",[117],{"title":118,"path":119,"stem":120},"Task Manager","\u002Fdocs\u002Fv1\u002Fexamples\u002Ftask-manager","docs\u002Fv1\u002F7.examples\u002F1.task-manager",{"id":122,"title":19,"body":123,"description":509,"extension":510,"links":511,"meta":512,"navigation":513,"path":20,"seo":514,"stem":21,"__hash__":516},"docs\u002Fdocs\u002Fv1\u002F1.how-it-works.md",{"type":124,"value":125,"toc":497},"minimark",[126,135,138,143,154,156,160,167,248,250,254,259,329,336,338,342,348,362,364,368,376,379,397,399,403,408,417,423,425,429,435,446,462,464,468],[127,128,129,130,134],"p",{},"Tether is built on an ",[131,132,133],"strong",{},"offline sync architecture for Laravel",": mutation-log push, state-snapshot pull, client-generated ULID identity, idempotent retries, and server-side conflict handling. Client writes are captured locally first, pushed to the server as immutable mutations, and reconciled by pulling authoritative server snapshots back to the client.",[136,137],"hr",{},[139,140,142],"h2",{"id":141},"data-flow","Data flow",[144,145,151],"pre",{"className":146,"code":148,"language":149,"meta":150},[147],"language-text","[Client local DB]\n      -> every write intercepted by Syncable trait\n[Client mutation log]\n      -> push (client-initiated, batched)\n[Server POST \u002Ftether\u002Fpush]\n      -> PushSyncProcessor -> ConflictDetector -> MutationApplicator -> ServerMutationLog\n[Server Eloquent models]  (authoritative state)\n      -> StateSnapshotGenerator (updated_at \u002F deleted_at cursor)\n[Server POST \u002Ftether\u002Fpull response]\n      -> SnapshotApplicator (forceFill + save by tether_id, without events)\n[Client local DB]  (reconciled)\n","text","",[152,153,148],"code",{"__ignoreMap":150},[136,155],{},[139,157,159],{"id":158},"push-sending-mutations-to-the-server","Push - sending mutations to the server",[127,161,162,163,166],{},"When the client calls ",[152,164,165],{},"TetherClient::push()",":",[168,169,170,182,188,191,218,233],"ol",{},[171,172,173,174,177,178,181],"li",{},"Failed mutations rejected with reason ",[152,175,176],{},"error"," are retried while below ",[152,179,180],{},"max_retry_attempts",".",[171,183,184,185,181],{},"Pending mutations are loaded and chunked into batches of ",[152,186,187],{},"push_batch_size",[171,189,190],{},"Each batch is sent as JSON to the configured push URL.",[171,192,193,194],{},"The server processes each mutation:\n",[195,196,197,200,203,206,212],"ul",{},[171,198,199],{},"Checks duplicate mutation IDs for idempotent retry handling.",[171,201,202],{},"Runs the registered server push mutation mapper, if any.",[171,204,205],{},"Detects conflicts against the current server record.",[171,207,208,209,181],{},"Applies the mutation via ",[152,210,211],{},"MutationApplicator",[171,213,214,215,181],{},"Records the applied mutation in ",[152,216,217],{},"tether_server_mutations",[171,219,220,221,224,225,228,229,232],{},"The server responds with ",[152,222,223],{},"applied",", ",[152,226,227],{},"rejected",", and ",[152,230,231],{},"conflicts"," arrays.",[171,234,235,236,239,240,243,244,247],{},"Applied mutations become ",[152,237,238],{},"synced","; rejected mutations become ",[152,241,242],{},"failed","; conflicts become ",[152,245,246],{},"conflict"," and the current server state is applied locally.",[136,249],{},[139,251,253],{"id":252},"pull-receiving-server-state","Pull - receiving server state",[127,255,162,256,166],{},[152,257,258],{},"TetherClient::pull()",[168,260,261,271,278,288,301,319],{},[171,262,263,264,267,268,181],{},"The stored ",[152,265,266],{},"last_sync_cursor"," is read from ",[152,269,270],{},"tether_sync_state",[171,272,273,274,277],{},"A pull request is sent with the integer microsecond cursor, or ",[152,275,276],{},"null"," for a full first pull.",[171,279,280,281,284,285,181],{},"The server queries registered models using their scopes and filters rows by ",[152,282,283],{},"updated_at"," \u002F ",[152,286,287],{},"deleted_at",[171,289,290,291,294,295,228,298,181],{},"The response contains ",[152,292,293],{},"Snapshot"," entries, ",[152,296,297],{},"new_sync_cursor",[152,299,300],{},"has_more",[171,302,303,304,307,308,311,312,315,316,181],{},"Each snapshot is applied locally by finding the model by ",[152,305,306],{},"tether_id",", then using ",[152,309,310],{},"forceFill()"," and ",[152,313,314],{},"save()"," inside ",[152,317,318],{},"Model::withoutEvents()",[171,320,321,322,324,325,328],{},"If ",[152,323,300],{}," is ",[152,326,327],{},"true",", the client repeats the pull using the updated cursor until all pages are consumed.",[127,330,331,332,335],{},"Pull is ",[131,333,334],{},"state-based",", not mutation replay. This is why server-created records can reach clients even though they were never present in a client mutation log.",[136,337],{},[139,339,341],{"id":340},"ulid-identity","ULID identity",[127,343,344,345,347],{},"Every syncable record is assigned a client-generated ULID in its ",[152,346,306],{}," column at creation time. This is separate from the model's primary key unless you explicitly override the sync key.",[195,349,350,353,359],{},[171,351,352],{},"Records created offline sync later without ID remapping.",[171,354,355,356,358],{},"The server matches incoming mutations by ",[152,357,306],{},", not by local auto-increment keys.",[171,360,361],{},"Client and server tables can keep their own primary-key strategies.",[136,363],{},[139,365,367],{"id":366},"sync-cursor","Sync cursor",[127,369,370,371,284,373,375],{},"The sync cursor is an integer microsecond timestamp derived from syncable models' ",[152,372,283],{},[152,374,287],{}," values. The client stores the last seen cursor and sends it with every pull request.",[127,377,378],{},"This means:",[195,380,381,388,391,394],{},[171,382,383,384,387],{},"The first pull (",[152,385,386],{},"last_sync_cursor = null",") fetches everything in scope.",[171,389,390],{},"Subsequent pulls fetch only rows changed after the last cursor.",[171,392,393],{},"Cursors stay integers in requests, responses, logs, and client state.",[171,395,396],{},"Standard Laravel timestamps are required on syncable client and server models.",[136,398],{},[139,400,402],{"id":401},"conflict-detection","Conflict detection",[127,404,405,406,181],{},"A conflict occurs when a client pushes a non-create mutation for a server record that has been updated more recently than the incoming mutation. The default detector compares the mutation timestamp with the server record's ",[152,407,283],{},[127,409,410,411,414,415,181],{},"Default resolution: ",[131,412,413],{},"server wins",". The server returns the current server state; the client applies it locally and marks the mutation as ",[152,416,246],{},[127,418,419,420,181],{},"Custom resolvers can be registered per model - see ",[421,422,104],"a",{"href":105},[136,424],{},[139,426,428],{"id":427},"idempotency","Idempotency",[127,430,431,432,166],{},"All push operations are safe to retry. The server deduplicates by ",[152,433,434],{},"mutation_id",[195,436,437,440],{},[171,438,439],{},"Within a batch - duplicate mutation IDs in the same request are handled before they can be applied twice.",[171,441,442,443,445],{},"Across requests - previously applied mutation IDs are recognised and returned as ",[152,444,223],{}," without re-applying.",[127,447,448,449,452,453,455,456,459,460,181],{},"Enable ",[152,450,451],{},"strict_duplicates"," mode to return duplicates as ",[152,454,227],{}," with reason ",[152,457,458],{},"duplicate"," instead of ",[152,461,223],{},[136,463],{},[139,465,467],{"id":466},"concurrency-protection","Concurrency protection",[127,469,470,471,474,475,478,479,224,482,485,486,489,490,493,494,181],{},"The client uses Laravel's ",[131,472,473],{},"atomic cache lock"," (",[152,476,477],{},"tether_sync_lock",") to ensure only one sync cycle runs at a time. If a cycle is already running, ",[152,480,481],{},"sync()",[152,483,484],{},"push()",", or ",[152,487,488],{},"pull()"," returns a ",[152,491,492],{},"SyncResult"," with ",[152,495,496],{},"skipped = true",{"title":150,"searchDepth":498,"depth":499,"links":500},1,2,[501,502,503,504,505,506,507,508],{"id":141,"depth":499,"text":142},{"id":158,"depth":499,"text":159},{"id":252,"depth":499,"text":253},{"id":340,"depth":499,"text":341},{"id":366,"depth":499,"text":367},{"id":401,"depth":499,"text":402},{"id":427,"depth":499,"text":428},{"id":466,"depth":499,"text":467},"Understand Tether's offline sync architecture for Laravel: mutation-log push, state-snapshot pull, ULID identity, idempotency, and conflict handling.","md",null,{},true,{"title":515,"description":509},"How Offline Sync Works in Laravel - Laravel Tether","OTiXlPJ4ynMrO6xmpqd_KzEexz76vMgZdLGNeWLDD5g",[518,520],{"title":15,"path":16,"stem":17,"description":519,"children":-1},"Learn how Laravel Tether helps build offline-first apps with local writes, mutation logs, client-driven push\u002Fpull sync, and server reconciliation.",{"title":29,"path":30,"stem":31,"description":521,"children":-1},"Install tether\u002Fclient, configure a local-first Laravel client app, add Syncable models, and record offline mutations for later sync.",1780481013060]