From 5df2b36068e0b00ddda438faf771fc4c66e3f8ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 3 Sep 2026 05:46:54 +0200 Subject: [PATCH] docs(schema): say why the device ledger keeps a first-sighting instant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `UserKnownDevice.firstSeenAt` has a database default, no application writer and no reader anywhere in the tree, and it is invisible to the column sweep because the sweep skips a column nothing writes. Annotated rather than dropped: the first sighting a user sees is the audit row the login-alert path writes, and that row goes on the audit-retention schedule, while the ledger row does not. This column is the ledger's own copy of the same instant, reachable through the database only — the model holds salted hashes and a coarse label precisely so it has no user-facing surface, and the backup excludes it wholesale. --- prisma/schema.prisma | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 11fce841a..0c6e0a00a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1302,6 +1302,20 @@ model UserKnownDevice { userId String @map("user_id") deviceHash String @map("device_hash") label String? + /// @internal: the row's own creation instant, supplied by this default — + /// `recordSignInDevice` (`src/lib/auth/login-alert.ts`) inserts the + /// fingerprint and the label and passes no timestamp. Nothing reads it. The + /// first sighting a USER sees is the `auth.login.new_device` audit row the + /// same function writes, rendered by the security-activity card, and that + /// row is removed on the audit-retention schedule. This column is the + /// ledger's own copy of the same instant, kept because the ledger outlives + /// the audit trail and an operator asking when a device first appeared on an + /// account has nowhere else to look. Deliberately reachable through the + /// database only: the ledger stores salted hashes and a coarse label + /// precisely so it has no user-facing surface. The backup does NOT select it + /// — the whole model is excluded (`src/lib/export/backup-plan.ts`), because + /// restoring a device-recognition ledger would pre-trust devices on a host + /// that has never seen them. firstSeenAt DateTime @default(now()) @map("first_seen_at") lastSeenAt DateTime @default(now()) @map("last_seen_at")