Skip to content

Troubleshooting

A restarted device pushes but never pulls. Subscriptions are not restored from disk — only your openGraph/subscribe calls create them. Call openGraph for each graph on every boot.

A graph stays waiting forever. No device that holds the key has synced since this one registered. Open the app on a device that has the key, or check whether autoGrant is disabled. If a device holds keys but is not granting, look for error events with scope keys.gaps. Under linking: 'approve', also check the other device for a device.pending it never showed anybody: a device nobody approved is never wrapped to, and it waits exactly like a device nobody has synced for.

device.pending fires for a device that is obviously ours. Possession-pinning needs that device to have written something this one could open and verify, and to have registered a selfSig. A read-only member, or a device registered by a build older than selfSig, will always need a human. trustedDevices() shows which approvals were inferred and which a person actually made.

device.conflict, or a device that will not be wrapped to again. The keys the relay publishes for a device this one already approved have changed. Device keys are set-once, so there is no benign explanation — treat it as an incident. forgetDevice(deviceId) is the way back if you establish there was one (a relay restored from a backup taken before that device registered, say), and it puts the device back to “ask me again” rather than approving it.

A rotation excluded somebody you did not expect. The graph.rotate relay event carries excluded. Those are devices the rotating device’s approval gate blocked. They keep reading up to the old epoch and nothing after; approve them and the next sync fills the gap.

linking.unenforced in the logs. linking is still 'observe', so unapproved devices were handed the key anyway. That is the default for one release. Set linking: 'approve' once your relay is new enough to understand excluding — an older one drops the field and refuses the rotation as incomplete-rotation.

status.pending never reaches zero. Look for an error event. Scope keys.behind means this device lacks the current key — another device must sync. A repeated push error with retryable: false means the relay is refusing the batch; check record.rejected for the reason.

status.rejected is climbing. The relay refused records for good. record.rejected with failure.reason === 'relay-rejected' carries the relay’s reason: rev-too-far-ahead (a clock problem), graph-record-quota or graph-byte-quota (a full graph).

Records vanish on one device. Almost always a schema mismatch: that device’s KindRegistry rejects what another device wrote. Watch record.rejected with reason schema or unknown-kind. Consider onUnknown: 'accept' during rolling deploys.

deviceOk is false. This device ID is bound to different public keys upstream — usually a keystore that was wiped while the device ID survived (a cleared browser profile with the ID still in localStorage). Mint a new device ID; never retry the registration.

409 device-key-conflict on boot. This is the same issue from the relay’s side. Use a new device ID.

403 device-not-registered. Your authenticate returned a device ID that is not registered to that member. Check the token binds the device the engine is actually using.

Two tabs disagree about unsynced writes. OPFS is one tab at a time and the second fell back to IndexedDB. Use IdbGraphStore everywhere, or run one engine in a SharedWorker.

A rotation returns 400 incomplete-rotation. Your Directory.membersOfGroup disagrees with the relay’s device roster — the rotation did not cover every active device of every current member. The response names the missing device IDs.

Everything is slow on a big import. The per-record cost is dominated by ECDSA (~310µs to sign, ~300µs to verify). An app that genuinely does not need authorship can set sign: false and roughly halve both, at the cost of by becoming a claim again.