# Handles and fingerprints (/concepts/handles-and-fingerprints)



Zumik uses two completely different kinds of identifier, and keeping them separate is a security boundary, not a style choice.

* **Public handles** are what you hold. They are opaque, random, and safe to log.
* **Internal fingerprints** are keyed HMACs the platform computes for deduplication and trace analysis. They never leave the tenant's isolation boundary and are never returned to a caller.

## Public handles [#public-handles]

Every public object has an opaque, prefixed, random ID:

```
art_01jy7n3q8v...   bnd_01jy...   ses_01jy...
br_01jy...          snp_01jy...   rsp_01jy...
```

The suffix is 26 random Crockford-base32 characters - roughly 130 bits of entropy - so handles are unguessable and never collide. The prefix only tells you the object kind. It tells you nothing about the content, and two artifacts with identical bytes get entirely unrelated IDs.

## Why raw hashes are never handles [#why-raw-hashes-are-never-handles]

It is tempting to use a content hash as a reusable handle: `ctx_sha256:91af...`. Zumik refuses to, because a content hash leaks equality.

<Warning>
  A raw content hash creates real risks:

  * Callers treat it as a durable, reusable capability that the platform never promised.
  * Identical hashes across tenants make accidental cross-tenant correlation easy.
  * Deletion gets confusing: re-uploading the same bytes silently revives the old hash and any cache relationship attached to it.
  * A leaked hash reveals that two systems hold identical content.
</Warning>

Opaque random handles have none of these properties. Re-uploading identical content after a delete produces a *new* handle and restores nothing.

## Internal fingerprints [#internal-fingerprints]

Inside the boundary, the platform still needs to recognize "this is the same content we saw before" to deduplicate storage and analyze prefix families. It does this with a tenant-scoped keyed HMAC:

```
logical_fingerprint =
  HMAC-SHA256(
    tenant_fingerprint_key,
    canonicalization_version || artifact_type || normalized_content
  )
```

Three properties make this safe:

<AccordionGroup>
  <Accordion title="Tenant-scoped key">
    Each tenant has its own secret fingerprint key, held only in memory or a secret manager. Two tenants with byte-identical content produce different fingerprints, so equality never crosses the isolation boundary.
  </Accordion>

  <Accordion title="Domain separation">
    The [artifact type](/concepts/artifacts) and a canonicalization version are bound into the HMAC input. Identical bytes registered as a `policy` and as a `document` fingerprint differently, on purpose.
  </Accordion>

  <Accordion title="Conservative normalization">
    Normalization is minimal - trim trailing whitespace per line, normalize line endings - so distinct content is never accidentally treated as equal.
  </Accordion>
</AccordionGroup>

## Key rotation breaks linkage on purpose [#key-rotation-breaks-linkage-on-purpose]

The fingerprint key is rotatable, and rotating it is a feature. After rotation, previously equal content no longer fingerprints equal, which intentionally severs old equality relationships. This is one of the mechanisms a [purge](/concepts/retention-and-purge) relies on: once linkage is broken, stale dedup relationships cannot quietly reconnect deleted content.

<Info>
  Fingerprints exist only for internal deduplication and diagnostics. They are never part of any request or response, never appear in logs you can read, and never serve as a handle. If you can see an identifier, it is an opaque public handle.
</Info>

<CardGroup cols="2">
  <Card title="Identity model" icon="diagram-project" href="/concepts/identity-model">
    How handles relate to materialization and KV keys.
  </Card>

  <Card title="Tenant isolation" icon="shield" href="/security/tenant-isolation">
    The boundary fingerprints never cross.
  </Card>
</CardGroup>
