Introduction
Concepts & data model
Sectors, shards, slabs, objects, and accounts — what each one is, why the model has no filenames, and what that means for your integration.
The data model has four layers. It is worth ten minutes now, because every endpoint is shaped by it and none of it will make sense otherwise.
Sector
The unit of storage a provider deals in: 4 MiB. Every sector is addressed by its Merkle root, a 32-byte hash. That root is how you ask for it back and how integrity is verified — a provider that returns different bytes returns bytes that do not hash to the root you asked for, and your client rejects them.
Shard
One erasure-coded fragment of your data, stored as one or more sectors on one provider. With the
default geometry a slab has 15 shards: 10 data, 5 parity. Shard size is
ceil(sliceSize / minShards).
Slab
A slab is one erasure-coded stripe: a set of shards that decode together. It records:
| Field | Meaning |
|---|---|
minShards | How many shards are needed to reconstruct — 10 by default. |
sectors[] | One entry per shard: the provider's public key, the sector root, and the contract. |
encryptionKey | 32 bytes of encryption parameter for this slab. |
version | Slab format version. |
A slab's ID is a content-addressed digest of its parameters — not a random identifier, and notably not a hash of the provider set. That is deliberate: repair can move a shard to a different provider without the slab ID changing, so your object references stay valid across repairs you never see.
Slab geometry is validated
Redundancy (totalShards / minShards) must be at least 1.5× and at most 4.0×. No provider
may appear twice in one slab, duplicate sector roots are rejected, and encryptionKey must be
exactly 32 bytes. Violations are rejected at registration, not silently accepted.
Object
An object is an ordered list of slab segments, plus wrapped key material and signatures:
| Field | Meaning |
|---|---|
id | 32-byte object key. Usually a hash of the content; you choose it. |
slabs[] | { id, offset, length } — a byte range within a slab. Concatenated in order, these are your object. |
encryptedDataKey | The object's data key, wrapped. Tessera cannot unwrap it. |
encryptedMetadata | An opaque blob. Anything you want. Tessera never interprets it. |
encryptedMetadataKey | The wrapped key for that blob. |
dataSignature, metadataSignature | Your signatures over the respective parts. |
Because an object is a list of slab segments, several small objects can share a slab, and one large object can span many. Deduplication and packing are consequences of the model rather than features bolted onto it.
What the object model does not contain
There is no filename, no path, no directory, no content type, and no cleartext size. These are not fields we decline to populate; the columns do not exist.
This is the single most important thing to understand before you integrate. If your application needs a file browser, you build the naming layer:
// Your metadata blob, encrypted by you, opaque to us.
const metadata = {
name: 'quarterly-report.pdf',
contentType: 'application/pdf',
size: 8_431_233,
path: '/reports/2026/q1/',
createdAt: '2026-04-02T09:14:00Z',
}
// encrypt(metadata) → encryptedMetadata
To list a directory you decrypt the metadata of the objects you know about, which means you keep a local index. The trade is explicit: you take on an index, and in exchange no one — including us, including anyone who compels us — can enumerate what you have stored or infer anything from its names.
Design your keys deliberately
The object key is the only server-side handle you get, and you choose it. A content hash gives you free deduplication. A deterministic hash of your own path gives you lookup by path without an index. A random key gives you unlinkability. Pick before you have a million objects, not after.
Account
Your account is your credential — specifically the ed25519 public key you were issued. There is no separate account object, no username, and no email in the storage path.
Every slab and object is bound to the account that created it. A request signed by a different
credential is refused with 403, not silently emptied. Provisioning a new credential creates a new
and entirely separate namespace; there is no notion of a credential belonging to an account that
owns objects.
Consequence worth planning for
Because the credential is the account, losing the private key means losing access to everything stored under it, and we cannot restore it — we never had it. Treat the credential as you would a root key: back it up before you store anything you care about.
How the layers stack
Object "report.pdf" ← your metadata says so; we do not know
├── slab A, offset 0, length 4194304
└── slab B, offset 0, length 4236929
│
└── Slab B: minShards 10, 15 sectors
├── shard 1 → provider ed25519:a1b2… root 9f3c…
├── shard 2 → provider ed25519:c3d4… root 22ab…
├── …
└── shard 15 → provider ed25519:e5f6… root 7c1d…
Repair
Slab health is scanned continuously. When a shard's provider goes away, the slab is queued for repair: the shard is reconstructed from parity and written to a replacement provider, and the slab record is updated in place. The slab ID does not change, so nothing referencing it breaks.
You do not trigger repair, pay for it as an event, or need to read the data for it to happen.
Encoding conventions
| Type | Wire format |
|---|---|
| Slab ID, object key, sector root | Lowercase hex, 64 characters |
| Provider public key | ed25519: followed by 64 hex characters |
credential query parameter | base64url with padding |
appKey from provisioning | base64url without padding |
Byte-slice JSON fields (encryptionKey, encryptedMetadata, …) | Standard base64 with padding |
| Timestamps | RFC 3339 |