{"protocol":"clank-doc/1","frameworkVersion":"0.21.0","slug":"offline","title":"Offline mutations","description":"Enable server receipts and create one browser queue for the current application/account:","group":{"id":"full-stack","title":"Full stack"},"url":"https://docs.clank.run/docs/offline","source":"docs/offline.md","headings":[],"tableOfContents":[],"markdown":"# Offline mutations\n\nEnable server receipts and create one browser queue for the current application/account:\n\n```ts\n// Server: successful receipts share the mutation's SQLite transaction.\nconst backend = await openBackend(definition, { path: \"app.sqlite\", offlineMutations: {} });\n\n// Browser: use your existing authenticated SyncClient and auth state.\nimport { createOfflineQueue, renderOfflineQueue } from \"@clank.run/framework/offline\";\nconst queue = createOfflineQueue({\n  namespace: \"my-app\", userId: currentUserId, storage: localStorage, client,\n  currentUser: () => auth.user.peek()?.id ?? null,\n});\nawait queue.enqueue(api.todos.add, { title: \"Finish report\" });\nconst unsubscribe = queue.subscribe(items => {\n  pendingElement.innerHTML = renderOfflineQueue(items);\n});\nawait queue.flush();\nconst reconnect = () => queue.flush().catch(reportQueueError);\nwindow.addEventListener(\"online\", reconnect);\n```\n\n`enqueue` persists before returning. `flush` sends in order, removes acknowledged work, and leaves\nnetwork failures pending with exponential retry delays from one second to one minute. Call\n`flush` on reconnect, application startup, and your retry timer; there is no hidden background\nworker. A pending item exposes `attempts`, `nextAttemptAt`, and a bounded error code. Rendering\nshows only operation paths/statuses; mutation arguments and credentials are omitted.\n\nAn unsuccessful 409 response pauses the queue in `conflict`. Read the latest server value and\nlet the user reconcile it, then call `queue.retry(id, mergedInput)` and `queue.flush()`. A replacement\ngets a new key; plain `retry(id)` preserves the original key. Permanent failures pause in `failed`;\ninspect/reconcile their server outcome before `discard(id)` and enqueueing a new operation.\nLater queued edits wait behind a conflict or failure. `clear()` discards pending work explicitly.\n\nReceipts bind keys to the authenticated user, operation, and parsed input. They commit alongside\nthe mutation, so a lost response or server restart can safely replay the stored result. Auth,\norigin, CSRF, and function authorization checks still run. Keys expire after seven days by default;\nexpired keys are rejected rather than executed again. Reconcile expired work before creating a\nnew key. Retention accepts one minute to 30 days and is fixed after the database's first receipt\ninitialization, preventing a later retention increase from resurrecting deleted keys. Receipts\nare pruned on successful keyed mutations. They retain parsed arguments and results in the\napplication database; use its normal backup/access protections.\n\nThe server keeps at most 10,000 receipts by default (configurable to 100,000), at most 1,000 per\naccount, and at most 64 KiB per result. Capacity errors leave work pending. Browser storage holds\nat most 100 items and 1 MiB per application/account. Storage failures are surfaced, and corrupt\nqueues are never silently overwritten. Input is persisted as JSON: do not queue credentials,\nfiles, or values inappropriate for same-origin browser storage.\n\nEvery send carries its original account ID, checked against the authenticated server session.\nOn logout, call `queue.dispose()` and remove reconnect/timer/subscription handlers. Clear while\nthe original account is still authenticated if pending work should be deleted. A disposed queue\nstops further sends; an already dispatched request may still commit. Create a new queue on login.\nWeb Locks serialize reads, writes, and sends across tabs where supported. Without Web Locks,\nserialization covers this JavaScript context only: use one active tab or supply storage isolated\nto that context, such as `sessionStorage`.\n\nUse `client.mutateOnce(reference, args, { key, userId })` for custom queue implementations. Keys\nare `<13-digit epoch milliseconds>.<UUID v4>`. Deduplication covers the transactional database\nmutation and transactionally enqueued jobs. Keep external effects in durable jobs; arbitrary\nsynchronous external side effects cannot be rolled back with SQLite.\n"}