Skip to content

Configuration

Nabeel Wasif edited this page Sep 29, 2026 · 2 revisions

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.


1. Profiles

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).


2. Backends

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.

Storage variables

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

Durable workflow

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.

3. Secrets

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.


4. OCR: the engine and the model

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.


5. AI detection

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.


6. External service limits: pacing what you call

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, 5xx or a timeout, with jitter, honouring Retry-After. 401, 402 and 403 are never retried, because no number of tries fixes a bad key or an empty balance.
  • The spend cap is estimated from AiUsage rows 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.

7. Rate limits: rationing callers arriving here

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 reset

The CLI writes to the database and takes effect without a restart.


8. Daily quotas

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 PDF 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 .eml would.
  • 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.

9. Batches and concurrency

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)

10. Streaming and encryption (since 1.6.0)

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.


11. Email parser limits

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

Attachment expansion

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)

12. Mailbox expansion limits

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.


13. Other

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

See also

Clone this wiki locally