Hashing & storage

Partly built

Content-addressed identity, and the bring-your-own-bucket storage model it's designed around.

Content-addressed code

A Clay definition’s identity is the hash of its syntax tree, not its name — the same core idea as Unison, and the reason Clay is a fork of Unison’s semantics rather than a from-scratch design. Two consequences that follow directly:

  • Names are just labels. Renaming a definition, or having two different names point at the same code, never changes what it is. add and plus — same implementation, different local names — are the same object underneath.
  • An alias still gets its own identity. A definition is hashed together with its type (same = add is not the same hash as add, even though it evaluates identically) — a definition’s hash is never simply its target’s; only references to something from inside other code are transparent like that.

This part of the language is the one place the project treats as non-negotiable: the hash is SHA3-512, computed the same way Unison’s own reference implementation (UCM) computes it, verified against UCM on every write. If Clay’s own hasher and UCM ever disagree, the write is refused outright rather than silently trusting either one. The goal is that once something is hashed, using it should never break the way dependency hell can break a Python or Docker setup — identity that’s actually structural, not a version string someone could reuse by mistake.

The hasher and a matching typechecker exist as a native/WebAssembly implementation (so they run in a browser, not just a server), and both are checked against dozens of programs recorded directly from UCM.

Bring-your-own-bucket storage

Clay is designed to never be the place your code lives. The plan (partially built) is two primitives on top of whatever object storage you already have access to — S3, GCS, Azure Blob, R2, B2, and so on:

  1. An immutable, grow-only object plane — every hashed definition, written once, keyed by its own hash. Content-addressed objects from two different sources can simply be unioned; there’s no such thing as a conflicting write.
  2. One small mutable pointer per branch, updated only by a conditional compare-and-swap (If-Match/If-None-Match or a provider’s equivalent) — the only place real coordination is ever needed.

A zero-setup default (a Clay-managed bucket) is meant to remove first-run friction, with migrating to your own bucket always a one-click, plain-copy operation — because both ends would speak the same protocol, never a bespoke export.

Status: the storage interface — the S3-compatible backend, an in-memory backend, a local mock of S3 semantics (conditional writes, injected faults, an optional SigV4 check) and a conformance suite any provider adapter is checked against — exists and is tested, and it is wired in. A whole codebase (terms, records, variants, abilities and their derived accessors) saves to and loads from a bucket; tiers can be pulled, pinned and published across; search indexes are prebuilt so name, comment and type search work against someone else’s codebase; a definition is brought in with its closure, verified on arrival; and the editor keeps a workspace continuously synced to a bucket you configure in its Storage panel (the pasted key is encrypted in the browser and never sent to a Clay server). Offline, the editor keeps everything in the browser’s own storage.

Everything above is tested against the local S3 mock; real AWS, R2 or B2 have not been tested. Planning a bring-in and opening a single definition read lazily (only the objects and sources they need, each fetched once and verified on arrival), and Settings has a per-location Eager/Balanced/Minimal control; a stored partial replica and pinning a dependency’s exact version are designed, not built.