# Two outside companies help draft your mail. Someone inside Beemy could open your silo too, and that costs them a row ordinary access cannot touch.

> Who inside Beemy can open your silo, what that costs in an audit row, why the row can't be quietly removed, and what we have not built yet.

Canonical: https://beemy.co/who-can-read-it/ · Last updated: 2026-09-24

Who Can Read It

No support console exists. A person with database access reads your message bodies as ciphertext. They read the names extracted from those messages in the clear.

11 min read

The short version

Opening your silo writes one key_unwrap row, every time, and the database itself blocks any in-band attempt to change or remove that row. A hash chain detects a row whose links were not recomputed, though it cannot rule out a writer who did recompute them. Your message bodies are encrypted, but the names extracted from them sit in a plain column. A privileged database role can still switch the guard off, you cannot read that log yourself yet, and we have not published a transparency report.

The short answer

## No support console, and no shortcut past the key.

A person inside Beemy has no easier route to a message body than the one described below.

No support console exists.

Beemy's staff have no console that opens your silo in their place. The only route to a committed message body is the audited key-unwrap path that every legitimate read takes. Mail still in intake is the one exception, and the limits section below says why.

SiloKeyService.openContext · backend/src/crypto/siloKeyService.ts, SiloKeyService.openContext (the only producer of a SiloContext)

The message body is encrypted under your silo's own key.

A message body is written as ciphertext, under a key held by your own silo. A person who queries the database directly cannot read that body without unwrapping the key, and unwrapping it writes a row.

SiloContext · backend/src/crypto/siloContext.ts, SiloContext.encrypt/decrypt; backend/src/db/schema/items.ts, bodyEncrypted

What opening your silo costs

## One audited row, every time your silo opens.

Opening your silo writes one key_unwrap row, every time.

SiloKeyService.openContext writes a key_unwrap event before it hands back a usable context. No path to a committed message body skips this write. Mail still in intake sits under a second key, and that key writes no row.

SiloKeyService.openContext · backend/src/crypto/siloKeyService.ts, SiloKeyService.openContext

One method writes the audit table, and nothing else does.

AuditLog.append is the only place in the backend that inserts into audit_events. No other code path writes that table directly.

AuditLog.append · backend/src/audit/auditLog.ts, AuditLog.append (the sole insert(auditEvents) call site)

Why that row cannot be quietly removed

## A trigger blocks the edit. A hash chain detects a link that was not recomputed.

The database itself refuses to change or remove a row.

Two triggers block every in-band UPDATE and every in-band DELETE on audit_events, at the database level. An application bug cannot rewrite history, and nor can an ordinary query.

forbid_audit_mutation · backend/drizzle/0001_invariants.sql, audit_events_no_update and audit_events_no_delete triggers; backend/src/audit/auditLog.ts ("in-band")

A hash chain detects a row whose links were not recomputed.

Each row's hash covers its own fields plus the previous row's hash. AuditLog.verifyChain walks the chain and names the exact row where it breaks. That catches a careless direct insert, a gap or a reorder, and it does not catch a writer who recomputed the links.

computeEventHash · backend/src/audit/hash.ts, computeEventHash; backend/src/audit/auditLog.ts, AuditLog.verifyChain (returns brokenAtSeq and a reason)

What else the log records

## The full action taxonomy, grouped by kind.

Every one of these writes through the same append-only, hash-chained path above.

create, update.

Linking your work and personal identity, or changing your consent to it, writes a create or an update row.

IdentityLinkGateway · backend/src/db/gateway/identityLinkGateway.ts, IdentityLinkGateway.setConsent and createLink

key_unwrap, key_rotate, key_destroy.

Unwrapping, rotating and destroying a silo's key each write their own row, including the moment a key is destroyed for good.

SiloKeyService · backend/src/crypto/siloKeyService.ts, SiloKeyService.openContext, rotateKey and cryptoShred

classify, classification_correct.

A triage decision, and any later correction to it, both leave a row.

AUDIT_ACTION · backend/src/pipeline/mover.ts, AUDIT_ACTION (ReclassificationService)

public_read, entity_link_use.

Two reads across a silo wall write their own row each time. One is the one-way rule that lets a silo read public data. The other is the use of a link you made between a personal and a work contact.

createContextAssembler · backend/src/context/contextAssembler.ts (public_read); backend/src/db/gateway/identityLinkGateway.ts (entity_link_use)

autonomy_decision, autonomy_action.

A decision the policy engine makes, and any action it takes on your behalf, both write a row.

PolicyEngine · backend/src/policy/policyEngine.ts, PolicyEngine.evaluate and recordAction; backend/src/policy/grantService.ts

model_access.

Every outbound call to a model or embedding provider writes exactly one model_access row.

ModelGateway · backend/src/model/modelGateway.ts, ModelGateway (one append per outbound call)

data_erasure.

A completed right-to-erasure request writes its own row, carrying proof that the key behind it was destroyed too.

DeletionService · backend/src/deletion/deletionService.ts, DeletionService.eraseSilo; backend/src/audit/types.ts, ErasureProof

The walls behind it

## Backstops that hold even if the audit path were bypassed.

The database enforces silo isolation on its own.

Row-Level Security is switched on and forced for every silo-scoped table. A raw connection that skips the application entirely still cannot read across silos, as long as it authenticates as the ordinary application role. A Postgres superuser bypasses it.

FORCE ROW LEVEL SECURITY · backend/drizzle/0005_rls.sql, the beemy_app role; backend/README.md, "Requires a non-superuser role at runtime"

A context can never hold more than one silo's keys.

SiloContext has no constructor for an all-silo context. The only way to obtain one is the audited open path above.

SiloContext · backend/src/crypto/siloContext.ts, SiloContext ("There is no constructor for an 'all silos' context")

Planted tokens catch a cross-silo leak.

A honeytoken is planted per silo, and any output is scanned for a token that belongs to a different silo.

CanaryRegistry · backend/src/redteam/canary.ts, CanaryRegistry

A monitor raises an alert, it does not wait for a report.

LeakMonitor watches for a foreign canary, a cross-silo read, or a broken hash chain, and alerts on any of the three.

LeakMonitor · backend/src/redteam/leakMonitor.ts, LeakMonitor

What we have not done

## What we have not built, and what is not encrypted.

Everything above works today. These are the gaps around it.

You cannot read your own log today.

The public API exposes no audit route, and no capability on its known list grants one. The surface that would show you this log is still being built.

KNOWN_CAPABILITIES · backend/src/api/scope.ts, KNOWN_CAPABILITIES; backend README (S10 landed the mechanism, not yet the E9 user-facing transparency UI)

A privileged database role can switch the guard off.

The triggers stop an ordinary query, not the role that owns the table. That role can disable a trigger and then delete. Our own chain check cannot see rows removed from the end of a chain. It verifies the links it finds, and never asks how many there should be.

SECURITY DEFINER · backend/drizzle/0006_vector_partitioning.sql (the privileged migration role, which beemy_app is not); backend/src/audit/auditLog.ts, verifyChain ("this only catches out-of-band writes"); backend/README.md

The hash chain is unkeyed, so it cannot prove a forgery did not happen.

The chain is a plain SHA-256 over each row plus the row before it. No secret key signs it, and we publish no chain root anywhere. So a party that can write the table can insert a row, recompute every link after it, and our own check still reports the chain intact.

computeEventHash · backend/src/audit/hash.ts, computeEventHash (createHash, not createHmac — no key); no chain root is signed, published or anchored anywhere in backend/src

Not everything about your mail is encrypted.

Beemy extracts the people, companies and places named in a message, and stores those names in a plain column. A person with database access reads them in the clear, along with each item's source, its external id and its timestamps. The message body itself stays encrypted.

entities · backend/src/db/schema/entities.ts, name (a bare text column); backend/src/pipeline/mover.ts, entities.insert on the commit path

Mail still in intake is opened by a key that writes no audit row.

New mail lands in intake before Beemy picks a silo for it. There it sits under a second key, one per user, not one per silo. UserKeyService.openIntakeContext unwraps that key and writes no audit row. So one unaudited open reads every pending or quarantined item you have. Beemy clears the intake copy once the item moves into a silo. A quarantined item stays in intake until you place it.

UserKeyService.openIntakeContext · backend/src/crypto/userKeyService.ts, UserKeyService.openIntakeContext (imports nothing from src/audit); backend/src/db/schema/intakeKeys.ts (keyed on user_id); backend/src/pipeline/mover.ts (contentEncrypted: null on commit)

Your mail's embedding vector is stored unencrypted, on purpose.

Beemy turns each message into an embedding: a long list of numbers derived from the text. It is not the text, and you cannot read it back by eye. Researchers have shown that text can sometimes be recovered from one. We still store it unencrypted. Encrypted coordinates cannot be compared, and that comparison is how Beemy finds anything. The snippet beside it stays encrypted.

embeddings · backend/src/db/schema/embeddings.ts, embedding ("encrypting coordinates would break S4's HNSW similarity search"); backend/src/pipeline/mover.ts, addEmbedding on the commit path

We have not published a policy on a compelled-disclosure request.

We publish no transparency report, and no policy for a subpoena or a government request.

n/a · no transparency report or disclosure policy is published anywhere on this site today

We are not SOC 2 certified.

The enterprise page describes SOC 2 readiness, not certification. This page does not upgrade that claim.

n/a · see /enterprise, "SOC 2 readiness"

Crypto-shredding protects you after deletion, not during a live read.

cryptoShred destroys a silo's key for good, once you ask. Until then, the same KMS can unwrap that live silo's key, the way every legitimate read does.

cryptoShred · backend/src/crypto/siloKeyService.ts, SiloKeyService.cryptoShred

Clearing a key from memory is best effort, not a guarantee.

SiloContext.close overwrites its own key buffers. By its own comment, that is not a real security boundary, because the runtime may already have copied those bytes elsewhere.

close · backend/src/crypto/siloContext.ts, SiloContext.close

### Every column a database reader sees in the clear

These columns carry no encryption, including the ones nothing writes yet. Beside them sit the row ids and the timestamps, which are also in the clear. Those carry no words from your mail, so we list them as a class and not one by one.

One of these needs a word of explanation. An embedding is a long list of numbers, worked out from the text of a message. It is not the message, and you cannot read it back by eye. Research has shown that text can sometimes be recovered from one.

entities.name

the name of a person, company, topic or place named in your mail.

entities.type

whether that name is a person, a company, a topic, a place or something else.

entities.canonical_key

a deduplication key for the same entity — nothing writes this column today.

entities.metadata

anything later attached to that entity — nothing writes this column today.

items.kind

whether the item is a document, a message or neither.

items.external_id

the message's id at its source.

items.source

which connected account the item came from.

items.title

a subject line, if anything ever writes it — nothing writes this column today.

items.body

a message body, if anything ever writes it — nothing writes this column today.

items.metadata

anything later attached to that item — nothing writes this column today.

inbound_items.source

which connected account an incoming item came from.

inbound_items.external_id

the incoming message's id at its source.

inbound_items.signals

the routing signals read off an incoming item.

inbound_items.decision

where the item was routed, and why.

inbound_items.confidence

how sure the router was about that decision.

inbound_items.status

whether the item is pending, classified, quarantined, committed or rejected.

inbound_items.method

whether a prior, the classifier, both, or you yourself placed it.

embeddings.embedding

an embedding of the message — a list of numbers worked out from its text, which research has sometimes turned back into text.

embeddings.model

which embedding model produced it.

knowledge_bases.name

the name you gave a knowledge base.

knowledge_bases.path

its path.

voice_profiles.name

the name of a saved voice profile.

voice_profiles.tone_params

the tone settings learned for it.

voice_profiles.sample_stats

summary statistics over the samples behind it.

See every term this page uses, including hash chain and key unwrap, on the [lexicon](https://beemy.co/lexicon).

Last updated 24 September 2026
