Patterns

The representation hierarchy shows how two operations scale to arbitrary complexity through conventions at each level. These are the conventions that have emerged so far: value-level, interface-level, protocol-level, and structural.

References

A reference is a value that points somewhere else in the tree.

When a store returns a collection, it doesn't have to embed every item inline. It can return references, lightweight values that say "the real data is over there." A reference is just a map with a path key:

read /users
→ {"items": [{"path": "users/alice"}, {"path": "users/bob"}]}

Each reference is a promise: follow the path and you'll find the value. The client decides which references to follow and when. This gives stores control over response size without a separate "fields" or "expand" mechanism.

References can optionally carry type information, a hint about the shape at the other end:

{"path": "users/alice", "type": {"name": "user"}}

In the hierarchy, references are a value-level convention. They're ordinary map values: no new primitives, no changes to the data model. Any store can return them, any client can detect them. The path in a reference is the same path you'd use to read the value directly.

Shallow by default

Return references instead of embedding. Clients follow what they need. Large responses become small ones.

Consistent addresses

The path in a reference is the same path you'd use to read directly. No indirection, no resolution step.

Convention, not primitive

A reference is an ordinary map. Any store can return them, any client can detect them. The convention is the path key.

Pagination

Collections that are too large for one read. Solved with paths and references, no new mechanisms.

A paginated response includes items and a reference to the next page. The client doesn't construct pagination URLs; it follows the path the store provides:

read /users
→ {
    "items": [{"path": "users/alice"}, {"path": "users/bob"}],
    "links": {"next": {"path": "users/after/bob/limit/2"}}
  }

read /users/after/bob/limit/2
→ {
    "items": [{"path": "users/charlie"}, {"path": "users/dave"}],
    "links": {"self": {"path": "users/after/bob/limit/2"}}
  }

When links.next is absent, you've reached the end. The loop is: read a page, process items, follow next.

In the hierarchy, pagination is an interface-level convention. The paginated collection is a struct whose fields (items, links) are backed by behavior; the store computes each page on demand. The client iterates by following references, making it a protocol: an interface through time.

Cursors, not offsets

Offsets break under concurrent modification. Cursors are stable: "everything after this item" stays correct even as the collection changes.

Server-controlled navigation

The store decides cursor shape, page sizes, and orderings. The client just follows references. The store can evolve without breaking clients.

Parameters are paths

Page size, ordering, cursor: all path components. /users/by/name/after/bob/limit/20. Pagination addresses work like every other address.

Byte streams

Structured values cover most traffic. File I/O, sockets, stdio, and response bodies need bytes in ranges.

The byte-stream convention serves a stream through ordinary paths. For a stream at {h}, a ranged read parks until at least one byte past the offset exists or the stream is closed, then returns up to n bytes:

read {h}/at/0/len/4096
→ Bytes (≤ 4096; empty means end-of-stream)

read {h}/len → bytes so far
write {h}/append → append bytes
write {h}/at/{offset} → positioned write (random-access stores)

The semantics are exactly read(2): a non-empty result may be short (advance your offset and read again), an empty result means end-of-stream, and offsets past the end clamp to empty rather than erroring. Because terminality travels with the read, there is no race between a data read and a separate "is it done?" check.

Random-access stores (files) also follow the store conventions: writing Null to {h} deletes it (this is how O_TRUNC is expressed), and writing Bytes replaces the contents. In the hierarchy this is a protocol: one convention covers files, streams, and everything POSIX-shaped above them.

The offset is the cursor

There is no server-side read position. Every reader tracks its own offset, so many readers consume one stream independently, and a retry re-reads the same range idempotently.

Empty means done

End-of-stream is an empty read, not an error and not a separate flag. The read(2) contract, kept exactly.

Parking, not polling

A read at the end of an open stream waits for more bytes. No busy loops, no subscription mechanism: the read itself is the wait.

The handle protocol

A codified contract for deferred operations: write a request, get a handle, read the handle for results.

Handle-based stores and the broker pattern describe the general shape. The handle protocol pins down the rules so every broker-shaped store behaves identically:

write /http {"method": "GET", "path": "https://example.com"}
→ /http/outstanding/0

read /http/outstanding → list open handles
read /http/outstanding/0
→ {"status": 200, "headers": {...}, "body": ...} (parks until complete)

write /http/outstanding/0 null → release the handle

The rules: handle ids are minted by the store and never reused; a write to an existing handle path is a conflict, never an overwrite; writing Null releases the handle and cancels its work; and releasing fails any parked reads while leaving writes open, so a producer can always finish its teardown.

A handle that streams serves atomic tail reads under a protocol-defined sub-path: one read returns {"items": [...], "next": 3, "status": "open"} — items, the next cursor, and the terminal status together. A consumer can never observe "stream closed" while items it hasn't seen still exist; the close-out race is structurally impossible.

No overwrites

Handle paths are store-minted. Writing to one that exists is a conflict. Ids are never reused, so a stale handle path can't silently address new work.

Null releases

The store convention (writing Null deletes) doubles as cancellation. Release fails parked reads; writes stay open for teardown.

Tails are atomic

Items and terminal status arrive in one read. Pagination and completion share an envelope: {items, next, status}.

Append logs

Ledgers, usage records, event journals: data that grows at the end and never changes in the middle.

An append log is a store with one writable path and a read surface shaped for catching up:

write /ledger/append {"event": "deposit", "amount": 100}
→ /ledger/entries/0

read /ledger/len → 1
read /ledger/entries/from/0
→ {"items": [{"event": "deposit", "amount": 100}], "next": 1, "status": "open"}

The tail read returns the same {items, next, status} envelope as a handle's stream, so a consumer pages with a cursor instead of re-reading the whole log. Store the next value, come back later, resume from it. A log has no terminal state, so status stays "open"; the same envelope over a finite stream ends with "done".

This is pagination aimed at time instead of space: the cursor survives across sessions, and appends between reads are exactly what the next page returns.

The meta lens

Sometimes you need to know what a path does before you use it.

For any path P in a store, reading meta/P returns information about that path: what operations it supports, what shape the data takes, what sub-paths are available. The data and its description live in the same tree, accessed by the same operations.

users/alice ← the data
meta/users/alice ← what you can do with it
read /meta/users
→ {
    "readable": true,
    "writable": true,
    "paginated": true,
    "limits": {"default": 20, "max": 100}
  }

This is different from metadata on data. A file's size and modification time describe the data itself: suffix metadata (path/meta). The meta lens describes the path: what the address means, what it affords. The two are orthogonal.

In the hierarchy, the meta lens is structural: it operates across all levels. It can describe a value's type, a struct's fields, an interface's capabilities, or a protocol's state machine. It's introspection over the hierarchy itself.

Prefix, not suffix

meta/path describes capabilities. path/meta describes data properties. Same interface, different questions.

Same interface

Introspection is just reading. No reflection API, no schema registry. If you can read the tree, you can introspect it.

Optionally writable

Writing to a meta path can change the configuration of the underlying path. The lens is a control surface, not just a mirror.

The patterns compose

These aren't independent features. They're layers that stack because they share the same substrate.

References in pages

Paginated responses contain references. The page is an interface; the items are values. Follow what you need.

Meta over pagination

meta/users can describe pagination capabilities (cursor type, max page size, available orderings) before the client makes its first request.

Incremental adoption

Start with inline values. Add references when responses grow. Add pagination when collections grow. Add meta when clients need discovery. Each step is backward-compatible.

Uniform testing

Patterns are data shapes returned by read and write. Test them with the same store mocks you use for everything else.

Thought experiment: 1rec

Mount a service. Any service. Now interact with it through read and write.

The internet already has naming: DNS, URLs, service registries. What it doesn't have is a uniform interaction model. Every service has its own client library, its own pagination scheme, its own error model, its own authentication flow. You need to learn each one separately, test each one separately, and handle each one's failures separately.

Now imagine you mount a proxy store for a weather API at /weather:

read /meta/weather
→ {"readable": true, "writable": false, "fields": ["location", "forecast", "alerts"]}

read /weather/location/chicago
→ {"temp": 42, "conditions": "cloudy", "forecast": {"path": "weather/location/chicago/forecast"}}

read /weather/location/chicago/forecast
→ {
    "items": [{"day": "Mon", "high": 45}, {"day": "Tue", "high": 38}],
    "links": {"next": {"path": "weather/location/chicago/forecast/after/Tue/limit/5"}}
  }

You didn't import a client library. You didn't read API documentation. Meta told you what the store offers. References connected the current weather to the forecast. Pagination bounded the forecast into pages. The proxy store translated your reads into whatever HTTP calls the weather API actually requires. You never saw them.

Now mount another service. And another. Your tree grows:

/
├── weather/ ← proxy to weather API
├── github/ ← proxy to GitHub API
├── mail/ ← proxy to IMAP server
└── data/ ← local memory store

Every store responds to the same two operations. Every collection paginates the same way. Every path is introspectable through meta. You can read from /github/repos and write to /mail/drafts in the same session, with the same interface, and swap any of them for a mock without changing a line of application code.

This is 1rec. Not a directory of the internet: DNS already does naming. Not a database that contains everything: the data stays where it lives. 1rec is a single entry point (one record, one tree) where every service you interact with speaks the same interface. The proxy stores do the translation. The patterns do the navigation. You just read and write.

One record. The tree unfolds as far as you want to go.