Security
Overview
Engram is designed with multi-tenant security as a first principle. Every layer — authentication, database queries, vector search — enforces tenant isolation.
Authentication
API Key Security
- Hashed storage: Raw API keys are never stored. Only the SHA-256 hash is persisted in the database.
- One-time display: The full key is shown exactly once at creation. It cannot be retrieved after that.
- Prefix identification: The first 20 characters of each key are stored as
key_prefixso keys can be identified in dashboards without exposing the full secret. - Expiration: Keys can have an
expires_attimestamp. Expired keys are rejected during authentication. - Revocation: Keys can be revoked by setting
revoked_at. Revoked keys are rejected immediately. - Audit trail:
last_used_atis updated on every request (non-blocking viawaitUntil).
Key Validation Flow
Request arrives
│
├─ Missing Authorization header? → 401
├─ Not "Bearer" scheme? → 401
├─ Doesn't start with "engram_sk_live_"? → 401
│
▼
SHA-256 hash the raw key
│
├─ Hash not found in api_keys table? → 401
├─ Key has revoked_at set? → 403
├─ Key has expires_at in the past? → 403
│
▼
Extract organization_id → set auth context
Update last_used_at (non-blocking)
Continue to MCP handlerTenant Isolation
Database Layer
Every D1 query includes an organization_id filter:
-- Listing conversations
SELECT * FROM conversations WHERE organization_id = ?
-- Fetching messages
SELECT * FROM messages WHERE conversation_id = ? AND organization_id = ?
-- Looking up chunks
SELECT * FROM conversation_chunks WHERE organization_id = ?The organization_id is denormalized onto messages and conversation_chunks tables. This means tenant filtering happens directly on the queried table without JOINs — there’s no path to accidentally bypass the filter through a join.
Vector Search Layer
Vectorize queries include an organization_id metadata filter:
env.VECTORIZE.query(vector, {
topK: limit,
filter: { organization_id: orgId },
});Vectors from other organizations are excluded at the index level, not filtered after retrieval.
Application Layer
The organization_id is:
- Extracted from the API key during authentication
- Set once in the auth context
- Threaded through every service call as a required parameter
- Never derived from user input — always from the authenticated key
There is no API to query across organizations. No admin mode, no superuser key.
Automatic Secret Redaction
Engram runs a multi-layer redaction pipeline on every message before it’s stored, chunked, or embedded. Secrets are replaced with [REDACTED] markers at ingest time — they never enter the database.
What gets redacted
| Category | Examples |
|---|---|
| Provider API keys | OpenAI (sk-), Anthropic (sk-ant-), AWS (AKIA), GitHub (ghp_), Stripe (sk_live_), Cloudflare (cfk_), Supabase (sbp_), Slack (xox), npm, SendGrid, Twilio |
| PEM private keys | RSA, EC, DSA, OpenSSH private key blocks |
| JWTs | Three-segment base64url tokens (eyJ...) |
| Connection strings | postgres://, mongodb+srv://, redis://, amqp:// with embedded credentials |
| Secret assignments | password=, api_key:, token=, secret=, and similar key-value patterns |
| PII | Social Security numbers, credit card numbers, email addresses, phone numbers |
| High-entropy tokens | Hex strings (32+ chars), base64 strings (40+ chars) |
Pipeline order
Patterns are applied most-specific-first to minimize false positives:
1. PEM private keys (multi-line blocks)
2. Provider API keys (prefix-matched)
3. JWTs (three-segment structure)
4. Connection strings (URI schemes)
5. Secret assignments (key name heuristics)
6. PII (SSN, credit cards, emails, phones)
7. Generic high-entropy tokens (catch-all)What this means
- Search works normally. Redacted text still embeds with semantic meaning (“debugging deploy with env vars”) — just without the literal secret values.
- Secrets can’t be retrieved. There is no code path that returns the original secret after redaction. The value is destroyed, not masked.
- Everything downstream is clean. Chunks, embeddings, FTS5 indexes, and API responses all inherit the redaction.
For the full technical deep-dive, see the Secret Redaction whitepaper.
Data Protection
In Transit
All traffic uses HTTPS. The .app TLD enforces HSTS — browsers will never connect over plain HTTP.
Cloudflare Workers terminate TLS at the edge, so traffic between the client and Engram is encrypted end-to-end.
At Rest
Cloudflare D1 and Vectorize encrypt data at rest. Engram does not add application-level encryption on top of this.
Message content is gzip-compressed before storage to reduce D1 usage by ~3×. Compression is lossless — content is decompressed transparently on read and returned byte-for-byte identical to the original. See Architecture → Message Compression for details.
Content Storage
Message content is stored verbatim — exactly as sent by the client. Engram does not:
- Inspect or filter message content
- Send content to third-party services (embeddings are generated on Cloudflare’s own Workers AI)
- Log message content outside of D1 storage
- Retain deleted data (cascade deletes remove all messages, chunks, and vectors)
Embedding Privacy
Embeddings are generated using Cloudflare Workers AI (@cf/baai/bge-base-en-v1.5). This model runs on Cloudflare’s infrastructure — message content is not sent to OpenAI, Cohere, or any external embedding provider.
The embedding flow stays entirely within Cloudflare’s network:
Worker → Workers AI (same network) → Vectorize (same network)Cascade Deletion
When a conversation is deleted:
- All vector embeddings are removed from Vectorize
- All chunk records are deleted from D1
- All message records are deleted from D1
- The conversation record is deleted from D1
Foreign key constraints with ON DELETE CASCADE ensure database-level integrity. The Vectorize cleanup happens in application code before the D1 delete.
Security Headers
The MCP server runs on Cloudflare Workers, which provides:
- Automatic DDoS protection
- TLS 1.3
- HTTP/2 and HTTP/3
- IP reputation filtering
Reporting Vulnerabilities
If you discover a security vulnerability in Engram, please report it by opening a private issue at github.com/27Club/engram or contacting the maintainers directly. Do not disclose vulnerabilities publicly before they are fixed.