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:

FieldMeaning
minShardsHow 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.
encryptionKey32 bytes of encryption parameter for this slab.
versionSlab 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:

FieldMeaning
id32-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.
encryptedDataKeyThe object's data key, wrapped. Tessera cannot unwrap it.
encryptedMetadataAn opaque blob. Anything you want. Tessera never interprets it.
encryptedMetadataKeyThe wrapped key for that blob.
dataSignature, metadataSignatureYour 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

TypeWire format
Slab ID, object key, sector rootLowercase hex, 64 characters
Provider public keyed25519: followed by 64 hex characters
credential query parameterbase64url with padding
appKey from provisioningbase64url without padding
Byte-slice JSON fields (encryptionKey, encryptedMetadata, …)Standard base64 with padding
TimestampsRFC 3339