The Model

StructFS is an architectural style for data access.

It defines a uniform interface (two operations over paths and structured values) and a composition model where anything implementing that interface is a store, and stores mount into a shared namespace tree.

The store

A store is anything that responds to read and write.

A tree of data in memory is a store. So is an HTTP endpoint, a system clock, a database, or a random number generator. The differences between these systems (protocol, client library, error handling, testing story) are real, but they're accidental. At the level that matters to the application, they're all the same thing: data behind an address.

StructFS makes that sameness the foundation. A store is a store is a store.

Two operations

Every store responds to the same two operations. That's the entire interface.

read(path) → value | none | error

Returns the value at that path. The path may not exist (none), or the operation may fail (error). Callers handle both cases.

write(path, value) → path | error

Sends a value to that path. On success, returns a result path, often the same path, sometimes a new one. On failure, returns an error.

No DELETE, no PATCH, no LIST, no SUBSCRIBE. This seems like a limitation until you see why it isn't: paths carry meaning.

HTTP needs many verbs because URLs are nouns: you need different verbs to do different things to the same noun. In StructFS, a path can name an action just as easily as it names a resource. Writing to users/alice/deactivate is as expressive as DELETE /users/alice, but the deactivation is itself a path, one that can be composed, proxied, or intercepted with the same mechanisms as everything else.

Two operations isn't a constraint that limits what you can express. It's a constraint that forces expressiveness into the one place where it composes: the path.

The tree

Stores compose into a tree.

Mount a store at a path prefix, and all reads and writes under that prefix route to that store. Mount several stores at different prefixes, and you have a unified namespace:

/
├── users/ ← database store
├── api/ ← HTTP client
├── cache/ ← in-memory store
└── config/ ← read-only store

A store doesn't know where it lives in the tree. When you read /api/items, the HTTP store sees items, not /api/items. This is what makes stores swappable: attach a mock at /api instead of the real client, and nothing else changes.

The tree is the wiring diagram of your system. You can rewire it without touching application code.

Write returns a path

This is the mechanism that makes two operations sufficient for complex systems.

When you write to a store, the return value isn't an acknowledgement; it's a path. That path might be the same one you wrote to, confirming the write. Or it might be a new path, a handle you can read from later.

write /jobs {"task": "resize", "input": "photo.jpg"}
→ /jobs/pending/0

read /jobs/pending/0
→ {"status": "complete", "output": "photo_resized.jpg"}

The write queued work. The read collected the result. The interface stays synchronous (no callbacks, no futures, no event loops) but the interaction is multi-step.

The same pattern models file handles, subscriptions, sessions, transactions: any interaction that spans multiple operations. You don't need OPEN, CLOSE, SUBSCRIBE, EXECUTE. Each is just a path you read from or write to.

Structured values

Classic filesystems deal in bytes. Infinite flexibility, zero interoperability.

StructFS deals in structured values: the same types your programming language already gives you, and the same model that every serialization format converges on. JSON, CBOR, MessagePack, Protocol Buffers: they all encode the same core shapes. StructFS makes that shared model native. When a store returns a value, it's already structured. When you write a value, there's no encoding step. The structure is the data.

This is the departure from Plan 9. 9P gives you a universal namespace over byte streams; you still need to agree on a wire format between every producer and consumer. StructFS gives you a universal namespace over structured data. The serialization format is an implementation detail of the transport, not a concern of the interface.

Path encoding

Structured values solve the data boundary. Paths need the same treatment.

A path component in StructFS must be a valid identifier in every language that might handle it: Rust, Go, JavaScript, Python, and anything else that touches the tree. This is the same problem HTTP faces with URLs: arbitrary strings need to be constrained to a form that survives every context they pass through.

HTTP's answer is percent-encoding. StructFS's answer is Namecode, an encoding that turns any Unicode string into a valid UAX 31 identifier. Think Punycode for variable names.

foo → foo valid identifier, passes through
café → café Unicode identifiers pass through too
hello world → _N_helloworld__fa0b space encoded
foo-bar → _N_foobar__da1d hyphen encoded

The encoding is reversible, deterministic, and idempotent. Strings that are already valid identifiers pass through unchanged; most path components in practice never get encoded at all. But when a path component comes from user input, an external system, or any source that might contain spaces, punctuation, or emoji, namecode guarantees it survives the round trip through every language boundary in the system.

This is the same role URL encoding plays for REST. Without it, HTTP URLs would break the moment they crossed a context boundary. Namecode gives StructFS paths the same guarantee: any path, any language, any store.

The representation hierarchy

Two operations and structured values. How does this scale to real systems?

Through conventions at increasing levels of abstraction:

Values
A path names a value: a string, a number, a map. Reading and writing at this level is direct data access.
Structs
A tree of named values. Read a parent, get a map. Read a child, get a field. Paths decompose into structure.
Interfaces
A struct whose fields are backed by behavior. There's no distinction between a stored field and a computed one; a consumer can't tell. An interface is a schema over actions.
Protocols
An interface through time. A state machine of reads and writes against a family of paths. Handles, brokers, pagination: all protocols. The state machine lives in the path structure and the ordering of operations.

Each level builds on the one below without new operations or new primitives, just conventions about what paths mean and how they relate. A store that implements a complex protocol is still a store that responds to read and write.

This is why two operations are sufficient. The complexity lives in the paths and the values, not in the verb set.

Design lineage

StructFS sits at an intersection of three traditions.

Plan 9 and 9P

StructFS inherits the namespace philosophy: everything accessible through paths, composition through mounting. The departure is the data model: 9P traffics in byte streams, StructFS in structured values. This eliminates the serialization boundary between components. And StructFS is a library-level abstraction, not a kernel facility.

REST

Both define an architectural style around a uniform interface. REST has four operations over representations; StructFS has two over structured values. REST puts semantics in verbs: different verbs for different actions on the same resource. StructFS puts semantics in paths: different paths for different actions, same two verbs. REST constrains network interaction; StructFS constrains data access at any layer.

Capability systems

A StructFS path is a capability: possession of the path grants the ability to read or write. The tree is a capability graph; mounting grants access to a subtree, and relative addressing means a store can't reach outside what it was given. This is structural, not an access control layer bolted on.