coverage
| Subpath | @noy-db/hub/coverage |
| Factory | withCoverage() → coverageStrategy |
| Cluster | Audit |
⚠️ Not the same thing as cover
Hub publishes both @noy-db/hub/cover and @noy-db/hub/coverage, and they
are unrelated:
| subpath | what it is |
|---|---|
/cover | owner-curated plaintext metadata — name, description, icon — readable before unlock |
/coverage | read accounting — how much of a collection a principal has decrypted |
The collision is named deliberately rather than renamed away, because renaming a
published subpath is a breaking change for everyone already importing it. If you
arrived here looking for the pre-unlock labels on a vault, you want
cover.
What it does
Answers a question no access check can: this principal is allowed to read the collection — how much of it have they actually read? A viewer entitled to any single record is still worth noticing when they have decrypted 90% of the corpus.
Accounting happens at the decrypt point, and the state is sketches rather than a log:
- a HyperLogLog per
(principal, vault, collection)estimates distinct records ever decrypted — the coverage horizon, which never resets; - a Bloom filter per window estimates records novel to that window.
⭐ Both under-report by construction and never over-report. That direction is the point: an alert that fires late is a missed signal, while one that fires on phantom reads destroys trust in the alert.
Prerequisites
- A
corpusSizeper collection you want a fraction for — coverage isdistinct / corpusSizeand the size is declared, not counted (see below). A collection without one still accumulates counts but can never cross a threshold. - Somewhere for the event to go — the signal is a
coverage:thresholdemission, so it is only useful if something is listening. - Nothing else. The sensor never reads the store.
Opting in
import { createNoydb } from '@noy-db/hub'
import { withCoverage } from '@noy-db/hub/coverage'
const db = await createNoydb({
user: 'alice',
secret: 'a-real-passphrase-2026',
coverageStrategy: withCoverage(),
})
WithCoverageOptions tunes the sketches — precision (HLL registers, default
14 ⇒ 16 KiB per key), bloomBits (default 16384), bloomHashes (default 7) and
windowMs (the novelty window, default one hour).
⛔ windowMs is not the coverage horizon. The window governs novelty; the
coverage fraction accumulates for all time and never resets. Shortening the
window makes bursts easier to see and does nothing to coverage.
Declaring a corpus, and why it is declared
// per collection
{ corpusSize: 12_000, alertAt: [0.6, 0.9] }
Coverage is distinct / corpusSize, and corpusSize is declared, not
counted — two deliberate reasons, both worth keeping:
- Counting means a store read, on a sensor whose whole design is that it never touches the store.
- A corpus size that moves with every write makes an alert threshold un-reproducible: the same read pattern would cross 0.9 on Tuesday and not on Wednesday.
A collection that declares no corpusSize still accumulates distinct and
served; it simply has no fraction, so CoverageStats.coverage is absent and no
threshold can fire.
Each fraction in alertAt fires at most once.
The event
Crossing a threshold emits coverage:threshold carrying a CoverageEvent:
interface CoverageEvent {
readonly principal: string // keyring.userId the reads are attributed to
readonly vault: string
readonly collection: string
readonly novel: number // records novel to this window (Bloom-estimated)
readonly served: number // decrypts served to this principal, all time
readonly coverage: number // distinct-ever / declared corpus, 0..1 (HLL)
readonly window: string // ISO-8601 start of the observing window
readonly source: string // which sensor produced it — 'hub/coverage' here
}
⭐ CoverageEvent is a published seam, not an internal shape. Two
consequences for anyone building on it:
sourceis astring, never an enum, and values are shaped<publishing package>/<sensor>— the package, not the repo. An enum would make every new sensor a breaking change to hub.- New sensors add fields; they never redefine
served/novel/coverage/window. Mixed horizons across sensors are by design, so readsourcebefore comparing two events' numbers.
Limits worth knowing before you alert on it
- The estimates are sketches. Treat a crossing as a threshold was reached, never as an exact count.
- Accounting is per
(principal, vault, collection). One principal reading broadly across many collections crosses nothing if each collection stays below its fraction — alert on the pattern you care about, not only on this one. - ⚠️ An eagerly hydrated collection decrypts its whole corpus when it loads, so a decrypt-point sensor sees full coverage immediately. That is honest reporting of what happened, and it is not the signal you wanted; the eager case is handled in hub's accounting rather than by ignoring it, but a threshold on an eager collection tells you about hydration, not about a reader.
See also
- cover — the similarly-named, unrelated pre-unlock metadata subpath
- tiers — the access controls coverage complements: entitlement versus volume
- session-tiers — the session side of the same question, per operation rather than per corpus