docs(schema): say why the device ledger keeps a first-sighting instant

`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.
This commit is contained in:
Marc-André Bombeck committed 2026-09-03 06:05:58 +02:00
1 parent 7dbec3bc4d
commit 5df2b36068
1 file changed
+14
+14
View File
@@ -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")