stable

Classifying a package

The two-axis model (satellite families vs. hub services) and the five rules that decide where a package or capability belongs.

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:

  1. Package families (to- / on- / at- / in- / by- / as-) — separate npm packages, classified by their relationship to the trust boundary: the line around @noy-db/hub inside 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/to whose 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.
  2. Hub services (with-* clusters inside packages/hub/src/) — opt-in capabilities of the vault itself, living inside the trust boundary, enabled one at a time by passing a with*() strategy factory to createNoydb(), 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)

PrefixReads asPrecise glossSees plaintext?May hold keys?May write vault state?
to-data goes to a backenda medium that persists ciphertext envelopes — a where, never a cache tierNo — ciphertext onlyNoEnvelopes only, as told
on-get on via this methodunlock / auth primitiveNoHandles unlock material, never data-encryption keys (DEKs)No
at-sealed at a trusted hostsealing-key provider (the one family that is not zero-knowledge, by design)Scoped sliceScoped, on your infraNo
in-runs in a hostadapts 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)NoVia public API only
by-sync by way ofsession-share transportNoNoNo
as-export as a formatpure read-side transform: vault handle in → formatted bytes outPlaintext (post-decrypt, by design)NeverNever

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-memory is 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-memory additionally serves as the reference store: the conformance oracle every other to-* 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, and in-yjs are 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-ai sounds 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-method NoydbStore interface — get / put / delete / list / loadAll / saveAll — plus envelope/op types and store errors). What a to-* 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 mapping NoydbStore → 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

QuestionAnswerRule
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 substratetable 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 insideR1, 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/attestationR1, R3, R4

See also