diff --git a/docs/telemetry-contract.md b/docs/telemetry-contract.md
new file mode 100644
index 0000000..446d3a3
--- /dev/null
+++ b/docs/telemetry-contract.md
@@ -0,0 +1,55 @@
+# Client diagnostics (`client_logs`)
+
+Background diagnostics from prod devices (feedback 2 §4). The family runs Amber
+on hardware that can't be debugged directly, so the app batches a low-volume
+stream of events to the `client_logs` collection. Designed to be invisible on
+the device (in-memory ring buffer, slow best-effort flush, never on a playback
+thread) and safe (owner-create-only, superuser-read-only, redacted at source).
+
+## Collection `client_logs`
+Migration `1788000000_client_logs.js`. Fields: `user` (relation), `kind`,
+`event`, `message`, `meta` (json), `appVersion`, `platform`, `device`, `ts`
+(device clock), `created` (server clock).
+
+Rules: `create` = owner (`user = @request.auth.id`); list/view/update/delete =
+**null** (superusers only). Retention: `client_logs.pb.js` cron trims rows
+older than 14 days nightly.
+
+## Event kinds
+| kind | event | meta |
+|-----------|---------------------|--------------------------------------------------|
+| `error` | `uncaught` | `{library}` — Flutter framework errors |
+| `player` | `exo_error` | `{host, anime}` — native player error |
+| `player` | `av_delay_applied` | `{audioMs, host}` — user dialed in an audio offset (the "a track falls behind" signal) |
+| `session` | `session_summary` | `{host, durationS, watchedS, stalls, audioDelayMs, anime}` on player close |
+
+`host` is only the stream's `scheme://host` — **never** a full signed URL or an
+addon token (redacted client-side in `TelemetryService.redactUrl` before write).
+
+## How Claude queries it
+Superuser token (same as releases publishing), then filter/sort the collection:
+
+```bash
+TOKEN=$(...auth as superuser...)
+# recent player errors + desync signals across the fleet, newest first
+curl -s "$PB/api/collections/client_logs/records?perPage=100&sort=-created&filter=$(python3 -c '
+import urllib.parse;print(urllib.parse.quote("kind='"'"'player'"'"'"))')" \
+ -H "Authorization: $TOKEN" | python3 -m json.tool
+```
+Useful filters: `kind='player'` (errors + desync), `event='av_delay_applied'`
+(who's fighting sync and by how much — the reported "track falls behind" bug),
+`kind='session' && meta.stalls > 3` (hitchy playback). Group by `device` /
+`platform` / `appVersion` to see which hardware struggles.
+
+## What it deliberately does NOT capture (yet)
+True per-track A/V PTS drift needs native instrumentation on both players
+(ExoPlayer exposes one clock; mpv would need `audio-pts`/`video-pts` sampling).
+v1 uses **proxies**: buffering-stall counts and the manual audio-delay the user
+applies to fix desync — which directly answers "is a track falling behind, on
+which sources/devices, and by how much". Deeper PTS sampling is a follow-up if
+the proxies point somewhere specific.
+
+## Privacy / kill switch
+On by default; a per-device Settings toggle ("Diagnostika") disables it and
+drops the pending buffer. Only sends while signed in. No addon credential or
+full stream URL ever leaves the device.
diff --git a/pb_hooks/client_logs.pb.js b/pb_hooks/client_logs.pb.js
new file mode 100644
index 0000000..a83d480
--- /dev/null
+++ b/pb_hooks/client_logs.pb.js
@@ -0,0 +1,14 @@
+///
+
+// Retention for the client_logs diagnostics (feedback 2 §4). These are for
+// live debugging, not an archive — trim anything older than 14 days daily so
+// the collection can't grow unbounded. Runs in PocketBase's cron scheduler.
+cronAdd("trimClientLogs", "0 4 * * *", () => {
+ try {
+ const cutoff = new Date(Date.now() - 14 * 24 * 60 * 60 * 1000)
+ .toISOString().replace("T", " ").slice(0, 19) + ".000Z"
+ const stale = $app.findRecordsByFilter(
+ "client_logs", "created < {:c}", "created", 500, 0, { c: cutoff })
+ for (const r of stale) $app.delete(r)
+ } catch (_) { /* nothing to trim / collection absent */ }
+})
diff --git a/pb_migrations/1788000000_client_logs.js b/pb_migrations/1788000000_client_logs.js
new file mode 100644
index 0000000..1992efa
--- /dev/null
+++ b/pb_migrations/1788000000_client_logs.js
@@ -0,0 +1,65 @@
+///
+
+// Background diagnostics from prod devices (feedback 2 §4).
+//
+// The family runs Amber on hardware I can't attach a debugger to, so the app
+// batches a small stream of diagnostic events here: uncaught errors, player
+// error codes, per-session playback summaries (host/resolution/stalls), and —
+// the signal for the "a track falls behind" reports — the audio/subtitle delay
+// the user dialed in to fix desync, plus stall counts. It's deliberately
+// low-volume and best-effort so it never slows the device (see TelemetryService).
+//
+// Privacy/security: rows are owned by the authenticated user (create-only for
+// them, no read/update/delete), and the client redacts addon tokens / stream
+// URLs to their host before anything is written — never a credential or a full
+// signed URL. Reading is superusers-only (that's how I query it).
+
+migrate((app) => {
+ const users = app.findCollectionByNameOrId("users")
+
+ const logs = new Collection({
+ type: "base",
+ name: "client_logs",
+ // Create-only for the owner; nobody but a superuser can read them back.
+ listRule: null,
+ viewRule: null,
+ createRule: "@request.auth.id != '' && user = @request.auth.id",
+ updateRule: null,
+ deleteRule: null,
+ fields: [
+ {
+ type: "relation",
+ name: "user",
+ required: true,
+ collectionId: users.id,
+ cascadeDelete: true,
+ maxSelect: 1,
+ },
+ // "error" | "player" | "session" | "info" — a coarse bucket for filtering.
+ { type: "text", name: "kind", required: true, max: 20 },
+ // Short event slug, e.g. "uncaught", "exo_error", "session_summary",
+ // "av_delay_applied".
+ { type: "text", name: "event", required: true, max: 80 },
+ // Human-ish message / error text (already redacted client-side).
+ { type: "text", name: "message", max: 4000 },
+ // Structured payload (redacted): host, resolution, stalls, delayMs, etc.
+ { type: "json", name: "meta", maxSize: 20000 },
+ // Device/app context so I can tell platforms apart without a debugger.
+ { type: "text", name: "appVersion", max: 40 },
+ { type: "text", name: "platform", max: 40 },
+ { type: "text", name: "device", max: 120 },
+ // Client-set event time (device clock); `created` is the server clock.
+ { type: "date", name: "ts" },
+ { type: "autodate", name: "created", onCreate: true },
+ ],
+ indexes: [
+ "CREATE INDEX `idx_client_logs_created` ON `client_logs` (`created`)",
+ "CREATE INDEX `idx_client_logs_kind` ON `client_logs` (`kind`)",
+ ],
+ })
+ app.save(logs)
+}, (app) => {
+ try {
+ app.delete(app.findCollectionByNameOrId("client_logs"))
+ } catch (_) { /* already gone */ }
+})