Classifying a package
Companion: Families — the architecture view of the same prefixes.
noy-db is an encrypted document store built as one core package, @noy-db/hub, surrounded by
satellite npm packages whose names begin with a preposition — to-file, on-password,
as-csv, and so on. This page is the decision guide for where any package or capability
belongs in that catalog: in one of the six preposition families, inside the hub as an
opt-in service, or at the prefix-less top level (packages like @noy-db/cli that carry
no preposition at all). It assumes you have read the
Families & ports overview; the structural background is in
Architecture → Families.
The two axes
Two independent classification axes are easy to conflate:
- Package families (
to-/on-/at-/in-/by-/as-) — separate npm packages, classified by their relationship to the trust boundary: the line around@noy-db/hubinside which decrypted data and key material may exist. Everything outside that line sees ciphertext, or plaintext only as an ordinary authorized consumer. Each family binds exactly one frozen port — a contract-only import subpath such as@noy-db/hub/towhose export surface is snapshot-checked so it can only change deliberately. The preposition is written from the vault owner's perspective: data goes to a backend, the owner gets on via a method, exports go out as a format, the hub is adapted into a host runtime, sessions sync by a transport, slices are sealed at a host you control. - Hub services (
with-*clusters insidepackages/hub/src/) — opt-in capabilities of the vault itself, living inside the trust boundary, enabled one at a time by passing awith*()strategy factory tocreateNoydb(), tree-shaken out of your bundle when unused. The full catalog is the Services section; how the folders group is Service layers.
A package family answers "who is this code to the vault?". A service answers "what can the vault itself do?". Nothing is ever both.
The family table (refined glosses)
| Prefix | Reads as | Precise gloss | Sees plaintext? | May hold keys? | May write vault state? |
|---|---|---|---|---|---|
to- | data goes to a backend | a medium that persists ciphertext envelopes — a where, never a cache tier | No — ciphertext only | No | Envelopes only, as told |
on- | get on via this method | unlock / auth primitive | No | Handles unlock material, never data-encryption keys (DEKs) | No |
at- | sealed at a trusted host | sealing-key provider (the one family that is not zero-knowledge, by design) | Scoped slice | Scoped, on your infra | No |
in- | runs in a host | adapts the hub's existing public surface into a host runtime or protocol (React render loop, TanStack cache lifecycle, an LLM tool-calling loop, REST, Yjs) | As an authorized consumer (like app code) | No | Via public API only |
by- | sync by way of | session-share transport | No | No | No |
as- | export as a format | pure read-side transform: vault handle in → formatted bytes out | Plaintext (post-decrypt, by design) | Never | Never |
An envelope here is the hub's encrypted wire unit (EncryptedEnvelope): ciphertext plus the
metadata a store needs to persist it — never decryptable by the store.
Two sharpened glosses
to-is a where, not a how fast.to-memoryis not a cache — it is RAM used as a ciphertext persistence backend, holding encrypted envelopes plus every reserved internal collection (_schemas,_history,_ftindex, tombstones…). The kernel's internal record cache is the opposite object on every axis: plaintext, per-collection, inside the trust boundary, not pluggable, not a family member. They coexist because they answer different questions.to-memoryadditionally serves as the reference store: the conformance oracle every otherto-*must match, and the substrate that lets all of CI run with no real cloud service.in-means "into a host", not "UI framework".in-ai(LLM function-calling adapter),in-rest, andin-yjsare full family members: each projects the hub's existing surface into a foreign runtime's idiom and is meaningless without that runtime. The test is whose lifecycle does the code serve — never the topic.in-aisounds retrieval-flavored, but it computes no retrieval and persists no index; it emits ACL-scoped tool definitions and dispatches tool calls back through the public API. (Retrieval capability is the hub's search and indexing services, inside the trust boundary, where the encrypted index artifacts live.)
Ports vs services vs combinators
Three store-flavored things exist and none duplicates another:
src/port/to(@noy-db/hub/to) — the contract door: a behavior-free re-export barrel (the 6-methodNoydbStoreinterface —get/put/delete/list/loadAll/saveAll— plus envelope/op types and store errors). What ato-*package binds. Frozen; additive-only; every addition is surface the whole family supports forever.to-*packages — the adapters that implement the contract.src/with-store— a service of combinators over the contract:routeStore(multiplex by collection/size/age) and store middleware (withRetry,withCache,withCircuitBreaker, …), each mappingNoydbStore → NoydbStore, envelope-in/envelope-out. Its audience is application authors composing stores; a store author never touches it.
The rule: contract doors stay behavior-free; anything that does something is a service.
The moment routing logic lived in the port, every store adapter would inherit version coupling
to it. (Same shape one level up: the /as port exports diffVault / writePod / Vault —
the primitives an exporter closes over — while the exporters themselves are separate
packages.)
The classification rules
Apply in order; the first rule that bites decides.
R1 — Classify by the worst thing the code must do, not the most recognizable.
Attestation issuance contains a conversion (record → QR credential), a signature, and a
ledger write. The conversion is the recognizable part; the key custody is the classifying
part. Anything that must touch getDEK (the internal capability that hands a service its
collection's data-encryption key), mint or hold key material, or write vault state is a
with-* service, no matter how export-shaped its output looks.
R2 — Capability lives in the hub; protocol projection lives in in-*.
If the code adds an ability the vault didn't have (retrieval, history, attestation signing),
it is a service inside the trust boundary. If it re-expresses an existing ability in someone
else's idiom (hooks, query caches, tool definitions, REST routes), it is an in-* binding —
even when, like in-ai, it has no external dependency and only the role test catches it.
R3 — A family invariant outranks a single package's convenience.as-attestation would "work" only if the /as port grew getDEK access and store writes —
re-classifying all exporters from "sees plaintext, holds nothing, writes nothing"
(one-sentence auditability) to "may hold keys". One package never buys that. Where a
capability has an export-shaped tail, split it: the key-touching part is a service; a
downstream pure formatter over its already-persisted output (e.g. a printable-credential
exporter reading _attestations through the vault handle) is a legitimate as-* citizen.
R4 — Counterparty features ship as two packages.
When the primary user of some code is not the vault owner (a relying party verifying a QR
credential), no owner-centric preposition fits — and forcing one would break the grammar's
promise. The shape is: capability inside (a with-* service holding the keys) plus a
zero-dependency primitive outside (a prefix-less package the counterparty consumes without
installing the hub or holding anything). Attestation is the type specimen: withAttestation()
(the attestation service) plus the pure @noy-db/attestation package
(verify, decode, revocation-list checks, all offline).
R5 — No prefix means: holds no vault, and serves someone other than (or larger than) a
single vault owner. The prefix-less top level is a meaningful third tier, not a junk drawer:
hub (the core), cli and create-noy-db (tooling), attestation (counterparty verifier).
The fleet control plane is prefix-less for the same reason, and big enough to be its own repo:
@klum-db/lobby. If counterparty packages multiply (receipts, proofs, shared-view
credentials), a dedicated family (e.g. for-*, "verifiable for a counterparty") may earn
its place; with a single member it does not.
Case studies
| Question | Answer | Rule |
|---|---|---|
Why does to-memory exist when the kernel has a cache? | Different layers: ciphertext persistence backend vs plaintext read acceleration; also the conformance oracle and CI substrate | table gloss 1 |
Why both the /to port and with-store? | Contract door (behavior-free, frozen) vs combinators over it (routing, middleware) | R3's ancestor: doors stay behavior-free |
Why are in-ai / in-tanstack-query bindings, not lookup services? | They add no capability; they project existing surface into a host runtime (TanStack's cache lifecycle; the LLM tool-calling loop). The search/indexing services hold the encrypted indexes and must live inside | R1, R2 |
Why isn't attestation as-attestation? | Issuance mints/holds an Ed25519 signer under getDEK, appends to _attestations, and manages revocation — three things the /as port must never carry. The pure conversion tail already lives outside as @noy-db/attestation | R1, R3, R4 |
See also
- Families & ports — the usage-first overview and per-family catalogs.
- Architecture → Families — the structural trust-boundary view.
- Architecture → Ports — the golden-frozen contract subpaths.
- Architecture → Service layers — how
with-*folders group. - Services — the catalog of everything inside the trust boundary.