This episode covers privacy and data handling in ChromaDB: the embedding inversion risk that can reconstruct documents from vectors, data-at-rest encryption, PII handling, retention policies, and data access audit logs for regulatory compliance.

Technical security was solidified in episodes 14 and 15. But there is a dimension that is often overlooked: privacy and data handling. A server safe from hackers is not necessarily safe from legal problems. The documents you store in ChromaDB — including those containing personal data — must still be treated according to regulations.
Episode 16 covers four things: the embedding inversion risk, data-at-rest encryption, PII (personally identifiable information) handling, and retention policies along with audit logs. This is material that determines whether your application can pass an audit.
Most people treat embeddings as "completely transformed text". A lesser-known fact: embeddings can be partially reversed. Research in embedding inversion shows that from an embedding vector, the original document can be reconstructed — sometimes accurately enough to leak sensitive content.
This is not pure theory: an attacker with access to the collection (or a leaked backup) could try to reconstruct documents from the stored embeddings. An important implication: embeddings are not a substitute for encryption. If the original data is sensitive, treat the embeddings as sensitive too.
data = collection.get(include=["documents", "embeddings"])
print("jumlah item:", len(data["ids"]))
print("dimensi embedding:", len(data["embeddings"][0]))collection.get(include=["documents", "embeddings"]) shows that the original documents and embeddings are both stored. Even deleting the original documents still leaves embeddings that could potentially be reversed.
Danger
Do not store embeddings of personal data in an unencrypted location. Embedding inversion enables partial document reconstruction — treat embeddings as sensitive as the original text.
Several strategies reduce exposure:
The first layer: encrypt the disk where ChromaDB data lives. This handles the most common scenario — stolen physical media or cloud volumes:
cloud: aktifkan volume encryption (EBS/PD/PV terenkripsi)
lokal: gunakan LUKS atau native disk encryption
backup: enkripsi snapshot di episode 11In the cloud, enable the provider's built-in volume encryption. In Kubernetes, make sure the StorageClass uses encrypted: true. Backups moved between environments must also be encrypted — plaintext snapshots are a leak point often forgotten.
For tighter control, encrypt sensitive documents before they enter ChromaDB:
from cryptography.fernet import Fernet
kunci = Fernet.generate_key()
fernet = Fernet(kunci)
dokumen_terenkripsi = fernet.encrypt(b"isi rahasia").decode()
collection.add(ids=["doc-1"], documents=[dokumen_terenkripsi])fernet.encrypt(b"isi rahasia") stores the document in encrypted form — readable only by key holders. The trade-off: full-text search and where_document from episode 7 no longer work on encrypted data. For PII cases, this is a fair trade-off.
The first rule of PII handling: do not store what you do not need. Before embedding documents, clean the personal data:
import re
def redaksi(teks):
teks = re.sub(r"\b\d{16}\b", "[KARTU]", teks)
teks = re.sub(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}", "[EMAIL]", teks)
return teks
dokumen_aman = redaksi(dokumen_asli)redaksi(teks) replaces 16-digit card numbers and email addresses with placeholders before the document enters ChromaDB. This habit is far cheaper than cleaning up data after a leak.
If your application processes personal data, prepare minimal documentation: what is stored, for what purpose, for how long, and who can access it. This document is not merely a formality — it becomes the basis for answering regulatory and audit questions.
A retention policy determines how long data is kept before deletion. A pattern you can apply directly:
import datetime
batas = (datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=90)).isoformat()
collection.delete(where={"timestamp": {"$lt": batas}})collection.delete(where={"timestamp": {"$lt": batas}}) deletes all documents with a timestamp older than 90 days. The retention policy — using the $lt filter from episode 7 — runs on a schedule, for example nightly via cron or a Kubernetes job.
For compliance, record who accessed what and when. At the application level, wrap query operations with logging:
import logging
log = logging.getLogger("chroma-audit")
def query_dengan_audit(client, collection_name, query):
log.info("QUERY collection=%s user=%s", collection_name, get_user())
hasil = client.get_collection(collection_name).query(query_texts=query)
log.info("QUERY_DONE collection=%s n=%s", collection_name, len(hasil["ids"][0]))
return hasilThe query_dengan_audit(...) function logs the request and its results. On large deployments, route audit logs to centralized log aggregation (episode 20) so they are not easily lost.
Episode 16 closed the privacy side: understanding the embedding inversion risk that can reconstruct documents, encrypting data at rest at both disk and application levels, cleaning PII before it enters ChromaDB, applying scheduled retention policies, and recording data access audit logs for compliance.
Key takeaways:
where={"timestamp": {"$lt": ...}}.In the next episode, episode 17, we will discuss scaling and performance — serving millions of vectors with the Rust server, batch operations, index tuning, ef search optimization, metadata pre-filtering, and proper CPU and RAM resource sizing. Your application is growing; it is time to prepare the machines.