Skip to main content
Cloudstic is built on a content-addressable storage (CAS) model, where every object is identified by a cryptographic hash of its contents. This enables automatic deduplication, structural sharing across snapshots, and strong integrity guarantees.

What is Content Addressing?

In a content-addressable system, the name of an object is derived from its content, not chosen arbitrarily. This has powerful implications:
Same content → Same hash → Stored onceIf two files (or file chunks) have identical content, they produce the same hash and are stored only once, regardless of their original names or locations.

Contrast with Traditional Storage

Object Addressing Scheme

All Cloudstic objects follow a flat namespace convention:
Location: AGENTS.md:70-80 Examples:
  • chunk/a3f5b8c9d1e2f4a6... - A file data chunk
  • content/7d9e2c5f8b3a1d4e... - A content manifest
  • filemeta/4b8f1a2c3d5e6f7a... - File metadata
  • node/c9d2e5f7a8b1c3d4... - HAMT tree node
  • snapshot/6e3a9f1c4b7d2e5a... - A backup snapshot

Hash Functions by Object Type

Different object types use different hash functions for security and deduplication purposes:

Chunks: HMAC-SHA256 (when encrypted)

Chunks use keyed HMAC-SHA256 to prevent storage providers from confirming file contents by hashing known plaintext.
When encryption is enabled:
Location: pkg/crypto/crypto.go:124-133 and internal/engine/chunker.go:168-169 When encryption is disabled, plain SHA-256 is used:
Location: internal/engine/chunker.go:171

Content Manifests: Raw File Hash

Content manifests (content/) use the plain SHA-256 or MD5 of the raw, unencrypted file content. This is a performance optimization: it allows Cloudstic to fetch the file hash directly from a remote source’s API (like Google Drive’s MD5), perform an Exists("content/<hash>") check, and completely skip downloading, chunking, and uploading the file if it’s already in the backup.

Metadata Objects: SHA-256(JSON)

Metadata objects (filemeta/, node/, snapshot/) use plain SHA-256 of their canonical JSON representation:
Location: internal/core/models.go:45-51

Deduplication at Two Levels

Cloudstic performs deduplication at both the chunk level and the content level:

Chunk-Level Deduplication

Before writing a chunk, the engine checks if it already exists:
Location: internal/engine/chunker.go:166-186 This means:
  • Identical 1MB regions across different files share a single stored chunk
  • Within a single large file, any repeated 1MB segments are stored once
  • Across snapshots, unchanged chunks are never re-uploaded
Example: If you back up a 1GB database file and only 10MB changes, the next backup uploads only ~10 new chunks (10MB), not the entire 1GB.

Content-Level Deduplication

Before chunking a file, the engine checks if the entire file’s content already exists:
Location: internal/engine/backup.go (BackupManager) This means:
  • A “new” file that’s identical to a previously backed-up file uploads zero bytes
  • Only a new filemeta object is created (a few hundred bytes)
  • The existing content and chunk objects are reused
Cross-Tenant Deduplication:
When encryption is enabled, each tenant has a unique dedup key, so cross-tenant deduplication does not occur. This is by design for privacy.
Location: docs/encryption.md:157-170

The Content Addressing Pipeline

Here’s how a file flows through the content-addressing pipeline during backup: Location: internal/engine/chunker.go:46-164

Structural Sharing via Merkle Tree

The HAMT (Hash Array Mapped Trie) is a Merkle tree, where:
  • Each node’s hash depends on its children’s hashes
  • Only nodes along the path of a change need to be rewritten
  • Unchanged subtrees are reused by reference
Merkle trees enable structural sharing: two snapshots that differ in only 10 files share 99%+ of their tree nodes.

Example: Modifying One File

When a single file changes:
  1. A new filemeta object is created (different content hash)
  2. The HAMT leaf containing that file is rewritten
  3. All parent nodes up to the root are rewritten (hashes change)
  4. All other nodes (A, C, D, F) are reused by reference
For a 1 million file backup with 100 changed files:
  • 100 new filemeta objects (~10KB each = 1MB)
  • ~20 new HAMT nodes (changed leaves + ancestors = ~5KB)
  • Total metadata overhead: ~1MB instead of re-writing the entire tree
Location: internal/hamt/hamt.go:183-303 and docs/spec.md:306-320

Content Addressing and Security

Preventing Plaintext Hash Leakage

If chunks were addressed by plain SHA-256, a malicious storage provider could:
  1. Hash known plaintext (e.g., “password123”)
  2. Check if chunk/<hash> exists in your backup
  3. Confirm you have that content without decrypting anything
This is called a “confirmation-of-a-file” attack.
Cloudstic prevents this by using HMAC-SHA256 keyed by a secret dedup key derived from your encryption key.
Without the dedup key, the provider cannot reproduce your chunk references, even if they have the plaintext. Location: docs/encryption.md:150-155

Dedup Key Derivation

Location: pkg/crypto/crypto.go:111-126 and docs/encryption.md:106-134

Integrity Verification

Content addressing provides built-in integrity checking:
  1. When you fetch chunk/a3f5b8c9..., you compute HMAC-SHA256(dedupKey, data)
  2. If the result doesn’t match a3f5b8c9..., the data is corrupted or tampered with
  3. The read fails immediately
No separate checksums or signatures are needed - the addressing scheme is the integrity check.

Small File Optimization

Very small files (< 4KB) are stored inline instead of chunked:
This avoids the overhead of creating a separate chunk object for tiny files. Location: internal/core/models.go:26

Immutability Guarantees

All content-addressed objects are write-once, read-many. Once chunk/a3f5b8c9... is written, it never changes.
Benefits:
  • Concurrent backups are safe: Two backups can write the same chunk simultaneously - they’ll both succeed because the content is identical
  • Crash safety: Partial writes create orphaned objects that don’t affect existing snapshots
  • No delete-then-write races: Deduplication via Exists() check means chunks are never deleted during active backups
The only mutable objects are:
  • index/latest - Points to the current snapshot
  • index/snapshots - Catalog of all snapshots (self-healing)
  • index/packs - Packfile catalog (rebuilt on load if corrupted)
Location: docs/storage-model.md:1-105

Garbage Collection

Because objects are immutable and shared across snapshots, deletion requires mark-and-sweep garbage collection:

Prune Algorithm

Location: docs/storage-model.md:64-78 and docs/spec.md:276-285
Prune requires an exclusive lock - no concurrent backups are allowed during garbage collection.

Content Addressing Benefits Summary

Further Reading

  • Snapshots - How content-addressed objects are assembled into point-in-time backups
  • Encryption - How HMAC-keyed hashing prevents plaintext confirmation attacks
  • Storage Model - How the object store manages content-addressed objects