Context
Three questions prompted this record. Why does TinySync need device-level tokens at all, rather than a single user-level credential or a per-group one? The client owns the catch-up process — it holds the cursor and replays the log itself — so what is the server identity even for? And is the device × vault cardinality the server carries today over-engineering? With tens of devices and hundreds of vaults per user, a per-device-per-vault model means thousands of credential rows. Is that fanout inevitable, or a design flaw we built ourselves?
The honest answer separates into two halves. Device identity turns out to be cheap and genuinely necessary. Per-vault device credentials — issuing a fresh credential for every (device, vault) pair — are the over-engineering. This record establishes the ground truth first, then makes the case for each half.
Ground truth: what the server actually tracks
Two findings from reading the schema, presented honestly.
(a) There is no per-device sync cursor
The server keeps no per-device delivery state. The change_log table is keyed (vault_id, seq) — an append-only log per vault, nothing more. Clients call GET /log?after=N with a cursor they own; the server answers with the rows past N and forgets the exchange. Catch-up is fully client-side.
CREATE TABLE change_log (
vault_id UUID NOT NULL REFERENCES vaults(vault_id) ON DELETE CASCADE,
seq BIGINT NOT NULL CHECK (seq > 0),
op_id UUID NOT NULL,
device_id UUID NOT NULL REFERENCES devices(device_id),
...
PRIMARY KEY (vault_id, seq)
);
(b) The n×m problem exists today — as identity, not sync state
This section describes the schema as of the decision date (2026-06-10); migration 0004 has since replaced it.
The fanout is real, but it lives in the identity model, not the sync path. A row in the devices table is not a physical device. It is a (physical device × vault) registration, each with its own credential:
CREATE TABLE devices (
device_id UUID PRIMARY KEY,
vault_id UUID NOT NULL REFERENCES vaults(vault_id) ON DELETE CASCADE,
display_name TEXT NOT NULL,
credential_hash BYTEA NOT NULL CHECK (length(credential_hash) = 32),
registered_at TIMESTAMPTZ NOT NULL DEFAULT now(),
last_seen_at TIMESTAMPTZ,
revoked_at TIMESTAMPTZ,
UNIQUE (vault_id, device_id)
);
The vault_id foreign key and the per-row credential_hash, scoped by UNIQUE (vault_id, device_id), are the cardinality made concrete: one credential per vault a device participates in. Migration 0003_container_discovery.sql already felt the strain and added a second credential system alongside it:
-- One master credential per physical device, valid across all its vaults.
-- Used only for stream auth and vault discovery.
-- Per-vault mutation credentials (devices.credential_hash) are unchanged.
CREATE TABLE device_sessions (
session_id UUID PRIMARY KEY,
credential_hash BYTEA NOT NULL CHECK (length(credential_hash) = 32),
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
Two credential systems coexisting — a per-vault one and a per-physical-device one bolted on later — is the classic symptom of a cardinality model under strain. The "master credential per physical device" the comment describes is the model this record argues the whole system should move to.
Why device identity is necessary
Identity is not the problem; per-vault credentials are. Four reasons the server must know which device a request came from — each cheap to satisfy with one identity per physical device, none of them requiring a separate credential per vault.
Idempotency
The idempotency_keys table is keyed (vault_id, device_id, op_id). A retried mutation — same operation, same device, replayed after a dropped connection — dedupes against the row the issuing device already wrote, and the original response is returned instead of applying the change twice. A user-level token would erase the device_id from this key, forcing exactly-once semantics to lean on a client-asserted installation ID that the server cannot vouch for.
Echo suppression
Every row in change_log records its origin in device_id. When a device replays the log to catch up, it skips the events it authored itself rather than re-applying its own mutations as if they were remote. Without a trustworthy origin stamp the originating device cannot tell its own echo apart from a peer's change.
Revocation granularity
A user-level token copied onto N machines is a single shared secret. One stolen laptop burns the credential everywhere, and rotating it logs out every other machine the user owns. "Unlink this device" — the affordance every file-sync product ships — is only possible when each physical device holds a credential of its own that can be revoked in isolation.
Attribution and abuse control
Per-device identity answers the operational questions: which device uploaded the corrupted file, which device to rate-limit when one client misbehaves, and which device went quiet — the devices table carries last_seen_at precisely so liveness is attributable. None of this works against an anonymous user-level credential.
The decision
This keeps every benefit above while deleting the fanout. The device identity that idempotency, echo suppression, revocation, and attribution all depend on is preserved — there is still exactly one identity per physical device — but it stops multiplying by vault count. What about wake fanout? Wake hints stay ephemeral messages to the devices currently connected and subscribed to a changed vault. That fanout is the information-theoretic floor: every replica that wants a change must learn of it, whether by push or by poll. It is a property of the message, not stored state. Offline devices subscribe to nothing and cost nothing; the server holds no queue for them.
But Scribe has no device tokens — why do we?
A fair challenge, since Scribe ships offline sync without any device credential in its sync layer. The short answer: the per-device credential didn't disappear there — it moved into layers Drive doesn't have. Scribe's client lives in a browser, where the session already is a per-device credential and short-lived JWTs can be re-minted whenever online; and its CRDT machinery absorbs the identity jobs — every Y.Doc carries a clientId, and Yjs's (clientId, clock) bookkeeping provides idempotency and echo suppression for free, unauthenticated but safe because CRDT merge makes duplicated ops harmless. Drive's client is a headless daemon with no browser session layer and no self-deduplicating data model: something must be the long-lived authenticated identity, and the device token is exactly that — not machinery on top of a session, but the session itself, for a client that has no browser. The full comparison, including why Scribe's model doesn't reduce credential or data fanout relative to the group model, is in Scribe vs Drive § Identity and access.
Consequences
This shipped as migration 0004_groups.sql: physical device identities plus group/vault membership edges, the PUT /v1/groups/* management APIs, open POST /v1/devices registration, and the removal of join tokens and per-vault credentials.
Settling this model unblocks two downstream efforts. The PowerSync as Sync Transport evaluation needs exactly this shape — its JWT subject maps to the device and its claims map to group memberships, so without a stable identity model there are no stable claims to drive sync-rule bucket selection. And the The Six Elements platform integration model builds directly on device-as-subject, group-as-authorization, treating it as the foundation rather than a detail to revisit.