Repository navigation
Configuration
Every backend is chosen by configuration and inferred when unset, so a fresh clone works before anything is decided. pnpm setup writes all of this for you; this page is the reference.
The exhaustive, commented list is .env.example. Every value there that setup does not set is written into .env as a commented line showing its default.
Sizes are written as sizes: 32MB, 512KB, 1.5GB. Units are powers of two (1KB = 1024 bytes; MB and MiB are the same), which is what ls -h and docker --memory already do. A plain number is still read as bytes. A malformed override throws rather than being ignored, because a limit somebody believes they set and which is not in force is worse than no setting at all.
ANONIFY_PROFILE selects a whole set of defaults at once.
| Profile | Meant for | Daily quotas | Rate limits | Batch / mailbox |
|---|---|---|---|---|
self-hosted (default)
|
your own machine or server | none | permissive | larger |
demo |
a shared, anonymous, public instance | small | strict | smaller |
A self-hosted install has no daily quotas. There is nobody to ration against, and a quota there is indistinguishable from the software being broken: it arrives as "processing failed" on a spreadsheet that is in no way unusual (#19).
| Variable | Options | Default |
|---|---|---|
DATABASE_DRIVER |
postgres, neon
|
inferred from the connection string |
DATABASE_URL |
a Postgres URL | required |
STORAGE_DRIVER |
s3, vercel-blob, local
|
inferred: Blob token → S3 config → local disk |
OCR_PROVIDER |
tesseract, mistral
|
tesseract (local, no account) |
AI_PROVIDER |
25 providers, see AI Providers | gateway |
ANONIFY_PROFILE |
self-hosted, demo
|
self-hosted |
ANONIFY_PUBLIC_URL |
the address people reach the instance at | Vercel's own on Vercel; http://localhost:3000 elsewhere |
Reads pick their storage driver from the stored key, not the current setting, so changing backends does not orphan documents already stored. See Storage and Encryption.
ANONIFY_PUBLIC_URL (since 1.8.0) is used only for absolute URLs in link previews. Set it on a self-hosted instance whose links get shared, and rebuild the image after changing it, because prerendered pages resolve it at build time. Compose passes it to the build.
| Variable | Driver | Notes |
|---|---|---|
S3_ENDPOINT, S3_BUCKET, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY
|
s3 |
RustFS or any S3-compatible service |
S3_REGION |
s3 |
default us-east-1
|
S3_FORCE_PATH_STYLE |
s3 |
inferred from a custom endpoint; set false for the rare case that needs it |
BLOB_READ_WRITE_TOKEN |
vercel-blob |
the browser uploads directly to it |
| (none) | local |
files land in .anonify-storage/; not usable on Vercel |
| Variable | Notes |
|---|---|
WORKFLOW_TARGET_WORLD |
@workflow/world-postgres for self-hosted. Leave unset on Vercel.
|
WORKFLOW_POSTGRES_URL |
defaults from DATABASE_URL. Leave unset on Vercel.
|
| Variable | What it does |
|---|---|
ENCRYPTION_KEY |
32 bytes (64 hex chars or base64). Wraps every per-document data key and seals global-rule patterns and the ChatGPT sign-in token. Checked at boot. Changing it makes everything already stored permanently unreadable. |
FINGERPRINT_SECRET |
32 bytes. HMAC key for identity derivation, download tokens and Hush approval signatures. |
CRON_SECRET |
Gates /api/cron/cleanup in production (Authorization: Bearer …). |
pnpm setup generates all three and offers to reuse existing ones.
| Variable | Options | Default |
|---|---|---|
OCR_PROVIDER |
tesseract, mistral
|
tesseract |
OCR_TESSERACT_MODEL |
fast (~2 MB), standard (~3 MB), best (~13 MB) |
standard |
OCR_TESSERACT_LANGUAGE |
eng, deu, fra, spa, ita, por, nld, pol, rus, tur, ara, hin, jpn, kor, chi_sim (join with +) |
eng |
MISTRAL_OCR_MODEL |
mistral-ocr-latest, mistral-ocr-2505
|
mistral-ocr-latest |
MISTRAL_API_KEY |
required for mistral
|
|
TESSERACT_CACHE_PATH |
any path (mount a volume in containers) | .cache/tesseract |
Every value is checked against a fixed list when read, because these end up in Compose files, where free text turns a typo into a container that builds, starts, accepts an upload and fails on the first scanned page. Each Tesseract variant caches in its own directory, so switching really switches. Only languages present in every variant are listed.
An explicitly requested provider that cannot run is an error, never a silent substitution, because swapping engines changes both quality and the geometry of every box. Since 1.6.3, OCR_PROVIDER=mistral without MISTRAL_API_KEY fails a document at once as configuration (not five retries and an unknown error), and a key set under another name such as MISTRAL_AI_KEY is named in the error.
Tesseract locates text per word and needs no account. Mistral locates per block (about a paragraph) and reads difficult scans better, but a redaction over a block covers the whole block. Detection is still English-shaped (#43); choosing an OCR language makes the page readable, which is the half that has to work first.
Full detail: AI Providers.
| Variable | What it does | Default |
|---|---|---|
AI_PROVIDER |
the provider | gateway |
AI_MODEL |
the model id (Azure: deployment name) |
anthropic/claude-haiku-4.5 for Gateway; required otherwise |
AI_MODEL_CAPABILITIES |
setup's verified declaration (structured output, vision), bound to provider, model and destination | legacy Gateway behaviour when unset |
AI_GATEWAY_API_KEY |
the Gateway credential (or Vercel OIDC) | unset: model pass skipped |
<PROVIDER>_API_KEY etc. |
per-provider credentials: OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_GENERATIVE_AI_API_KEY, XAI_API_KEY, MISTRAL_API_KEY, TOGETHER_API_KEY, COHERE_API_KEY, FIREWORKS_API_KEY (+ FIREWORKS_ACCOUNT_ID), DEEPINFRA_API_KEY, DEEPSEEK_API_KEY, CEREBRAS_API_KEY, GROQ_API_KEY, PERPLEXITY_API_KEY, BASETEN_API_KEY, OPENROUTER_API_KEY, SYNTHETIC_API_KEY; AZURE_API_KEY, AZURE_RESOURCE_NAME (+ AZURE_SUBSCRIPTION_ID, AZURE_RESOURCE_GROUP); AWS_REGION, AWS_PROFILE, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN, AWS_BEARER_TOKEN_BEDROCK; GOOGLE_VERTEX_PROJECT, GOOGLE_VERTEX_LOCATION, GOOGLE_APPLICATION_CREDENTIALS, GOOGLE_VERTEX_API_KEY
|
|
OLLAMA_BASE_URL |
Ollama | http://localhost:11434 |
AI_BASE_URL |
LM Studio / llama.cpp override; required for openai-compatible
|
per profile |
AI_API_KEY |
bearer token for openai-compatible
|
|
AI_MODEL_PRICES |
JSON { "provider:model": { inputPerMillion, outputPerMillion } }: the only prices enforced
|
unset |
AI_PRICE_INPUT_PER_MTOK, AI_PRICE_OUTPUT_PER_MTOK
|
legacy pair; applies to the selected model only | unset |
ANONIFY_MODEL_CACHE_PATH |
where setup and pnpm models:warm cache model lists (not read by the app) |
.cache/models |
Leave prices unset and the UI reports tokens and duration only, never a made-up dollar figure.
This is the opposite direction from the rate limits in §7. Those ration callers arriving here, and refusing is the answer. These pace this instance calling somewhere else, where refusing is not an option (the document is already uploaded), so a request that arrives too early waits.
| Variable | Bounds | Default |
|---|---|---|
ANONIFY_AI_CONCURRENCY |
model calls in flight, across every document |
4 hosted, 1 local server |
ANONIFY_AI_REQUESTS_PER_MINUTE |
sustained model-call rate; 0 = unpaced |
0 |
ANONIFY_AI_MAX_ATTEMPTS |
tries per call | 4 |
ANONIFY_AI_DAILY_SPEND_USD |
USD per UTC day; 0 = no cap |
0 |
ANONIFY_OCR_CONCURRENCY |
hosted-OCR requests in flight | 2 |
ANONIFY_OCR_REQUESTS_PER_MINUTE |
sustained hosted-OCR rate | 60 |
ANONIFY_OCR_MAX_ATTEMPTS |
tries per page | 4 |
These are not per-profile: they describe someone else's service, and an account's ceiling is the same whichever instance calls it. The OCR pair applies only to a hosted engine.
-
Pacing holds a request until the bucket can afford it, so a known limit is never hit. Retrying handles the unknown one:
429,5xxor a timeout, with jitter, honouringRetry-After.401,402and403are never retried, because no number of tries fixes a bad key or an empty balance. -
The spend cap is estimated from
AiUsagerows at each model's own price. At 80% calls go one at a time; at 100% the contextual pass and Hush stop for the rest of the UTC day, and documents say so. Without prices the cap cannot be computed, and the log says that rather than pretending.
Token buckets, refilling continuously, so there is no window boundary to burst across (#17). Stored in the database, so a restart does not reset them and replicas share them.
| Bucket | Covers | Demo | Self-hosted |
|---|---|---|---|
upload |
reserving documents, local uploads | 15 / min | 120 / min |
processing |
starting and retrying runs, rules, batch search, Hush turns, batch export | 30 / min | 300 / min |
export |
exports (charged per variant), restore, rule export | 15 / min | 120 / min |
read |
document reads, pages, redaction lists, page search hits | 300 / min | 2000 / min |
search |
whole-document search and rule previews (since 1.9.0) | 60 / min | 600 / min |
Three layers, later winning:
profile defaults → ANONIFY_RATE_LIMIT_<NAME>=<requests>/<seconds> → the CLI
pnpm rate-limit show # what is in force, and from where
pnpm rate-limit set --requests 500 --window 60 # all limits
pnpm rate-limit set upload --requests 20 --window 60
pnpm rate-limit resetThe CLI writes to the database and takes effect without a restart.
Separate from rate limits, counted per identity per UTC day, and charged once, idempotently, at extraction, the first moment the real cost is known. They exist so one anonymous visitor cannot spend a shared demo's whole budget.
| Variable | Unit | Formats | Demo default |
|---|---|---|---|
ANONIFY_QUOTA_PDF_PAGES |
pages | 10 | |
ANONIFY_QUOTA_DOCX_PAGES |
pages | DOCX | 10 |
ANONIFY_QUOTA_XLSX_CELLS |
filled cells | XLSX, CSV, TSV | 10,000 |
ANONIFY_QUOTA_TEXT_PAGES |
pages of extracted text | TXT, RTF | 40 |
ANONIFY_QUOTA_PPTX_SLIDES |
slides (notes, layouts, masters included) | PPTX | 20 |
ANONIFY_QUOTA_EMAIL_KILOBYTES |
KiB of decoded text: headers, every body, every nested message | EML, each message out of an MBOX | 512 |
ANONIFY_QUOTA_IMAGES |
one each | images | 3 |
ANONIFY_QUOTA_UPLOADS |
one each | all | 20 |
ANONIFY_QUOTA_ASSISTANT_TOKENS |
model tokens Hush spends, in and out | Hush | 300,000 |
self-hosted sets every one to 0 (unlimited). Values come from lib/security/quota-config.ts, which pnpm setup reads directly.
- An email is charged by the text it decoded, not as a page, which would charge a one-line reply the same as a forwarded thread.
- Cells are counted as cells that hold something, not the used range's area (#20).
- A mailbox is charged nothing itself, and each message costs what the same
.emlwould. - Hush is charged per token, step by step (a conversation has no pages, and every step resends it), and checked before every step. This is on top of the instance-wide spend cap, so one visitor cannot spend the cap for everybody.
| Variable | Demo | Self-hosted | Bounds |
|---|---|---|---|
ANONIFY_BATCH_MAX_FILES |
20 | 50 | documents in one batch; also the default for attachment expansion |
ANONIFY_BATCH_PROCESSING |
3 | 6 | one owner's documents processing at once (the admission queue) |
ANONIFY_BATCH_EXPORTING |
2 | 4 | documents exported at once in a batch export (wave width) |
| Variable | Default | Notes |
|---|---|---|
ANONIFY_ENCRYPTION_CHUNK_SIZE |
1MB |
a power of two from 64KB to 16MB; affects new objects only (existing ones record their chunk size in their header) |
ANONIFY_STREAM_MEMORY_BUDGET |
8 chunks per processing document (demo), 16 (self-hosted), e.g. 96MB self-hosted at the defaults |
chunk × in-flight chunks × processing concurrency ≤ budget; refused at startup if it cannot give every concurrent document two chunks |
See Streaming IO.
A message is the one format that arrives from strangers by design, and MIME is a recursive container with no natural bound. These limits are independent of the upload ceiling on purpose: raising the size a file may be must never be the same decision as allowing unlimited complexity. Exceeding one refuses the message as too-complex, never a truncated message presented as complete.
| Variable | Default |
|---|---|
ANONIFY_EML_MAX_DEPTH |
20 (multipart nesting) |
ANONIFY_EML_MAX_PARTS |
1000 |
ANONIFY_EML_MAX_TEXT_BYTES |
16MB (decoded text, all parts) |
ANONIFY_EML_MAX_HEADER_BYTES |
1MB (one part's header block) |
ANONIFY_EML_MAX_ATTACHMENTS |
200 |
ANONIFY_EML_MAX_NESTED_MESSAGES |
5 |
| Variable | Default |
|---|---|
ANONIFY_EML_EXPANSION_MAX_CHILDREN |
20 |
ANONIFY_EML_EXPANSION_MAX_EXPANDED_BYTES |
64MB across all of them |
ANONIFY_EML_EXPANSION_MAX_ATTACHMENT_BYTES |
25MB any single one |
ANONIFY_EML_EXPANSION_MAX_DEPTH |
3 (a message in a message) |
| Variable | Demo | Self-hosted | Bounds |
|---|---|---|---|
ANONIFY_MBOX_MAX_MESSAGES |
200 |
1000 |
documents one mailbox may produce |
ANONIFY_MBOX_MAX_TOTAL_BYTES |
32MB |
64MB |
content across every message |
ANONIFY_MBOX_MAX_MESSAGE_BYTES |
12MB |
25MB |
the largest single message |
ANONIFY_MBOX_MAX_DEPTH |
2 |
2 |
a mailbox reached through a mailbox |
MAX_MESSAGES is floored at ANONIFY_BATCH_MAX_FILES, so a mailbox can never produce a smaller batch than a person could assemble by hand.
| Variable | What it does |
|---|---|
MAX_UPLOAD_BYTES (constant in lib/config.ts) |
the upload ceiling: 50 MiB. Exceeding it is too-large. |
TEST_DATABASE_URL |
points pnpm test:db at a throwaway database; never DATABASE_URL
|
CLEANUP_INTERVAL_SECONDS |
the Compose scheduler's period (default 900) |
ANONIFY_URL |
where the Compose scheduler calls (default http://app:3000) |
APP_PORT, POSTGRES_PORT, RUSTFS_PORT, RUSTFS_CONSOLE_PORT
|
host-side Compose ports (the old MINIO_* names were renamed in 1.3.0) |
NEXT_OUTPUT=standalone |
produce the standalone server output (what the image uses) |
BENCH_MODELS |
models for pnpm bench:models, e.g. gateway:google/gemini-2.5-flash
|
Anonify: AI proposes, the application applies, and only what a person accepted is removed.
Repository · Report a bug · Request a feature · Security policy · Apache-2.0
This wiki describes v1.10.0 and summarises and links the in-repo documentation. Where the two disagree, the files under docs/, CHANGELOG.md and SECURITY.md are canonical.
Getting going
Using it
- Supported Formats
- Review Workspace
- Search and Rules
- Hush Assistant
- Redaction Presets
- Export and Anonymisation
- Batches and Mailboxes
How it works
- Architecture
- Processing Workflow
- Redaction Engine
- Storage and Encryption
- Streaming IO
- Data Model
- API Reference
- Security Model
When it breaks
Contributing
Repository · Issues · Releases · Changelog