Proxy between open-webui and document to text conversion backends with versioned and persistant cache management.
Project description
MarkGate
MarkGate is a proxy gateway between any HTTP client and document-to-Markdown conversion backends.
It provides persistent, content-addressed caching via S3 and prevents duplicate processing with Redis distributed locks.
MarkGate is compatible with the Open WebUI ExternalDocumentLoader format.
For Users & Operators
What it does
- Accepts a raw file over HTTP and routes it to the appropriate backend converter
- Returns the converted Markdown (and optionally a tar.zst archive with images and metadata)
- Caches results in S3 by content hash — sending the same file twice never re-processes it
- Exposes versioned endpoints so you can run multiple backends or configurations in parallel
Supported backends
| Version group | Backend | Status |
|---|---|---|
v1.0.0 – v1.3.0 |
Foil Serve | Production-ready (ish) |
v4.0.0 |
Docling (docling-serve) | Tested in early stages |
v2.x.x |
Marker | Planned (or maybe not) |
v3.x.x |
Chandra | Planned (or maybe not) |
Endpoint
PUT /md/{version}/process
- Body: raw file bytes (
application/octet-stream) - Headers:
Authorization: Bearer <CLIENT_API_KEY>— key specific to the version (see config)Content-Type— declared MIME type (the app always re-detects from bytes; this is informational only)X-Filename— URL-encoded original filename (e.g.my%20report.pdf)
- Response:
{ "page_content": "...", "metadata": { ... } }
A second endpoint returns a downloadable archive (content.md + images + metadata):
PUT /md/{version}/process/download → tar.zst archive
Force re-processing (bypass cache):
PUT /md/{version}/process?force_reprocess=true
Health endpoints
| Route | Description |
|---|---|
GET /health |
Liveness — always 200 if the app is up |
GET /health/dependencies |
Redis, S3, and upstream backend status (200 / 207 / 503) |
Running with Docker
# Production stack (MarkGate + Valkey/Redis)
docker compose -f docker/compose.yaml up
# With Docling backend (untested in latest developpement stages)
docker compose -f docker/compose.yaml --profile dev-tools-docling up
Configuration lives in docker/.env.d/markgate.env (copy from docker/.env.example).
Configuration reference
All variables are loaded from .env and .env_secret (both optional, merged).
Client authentication (Open WebUI → MarkGate):
| Variable | Description |
|---|---|
CLIENT_API_KEY_V100 … CLIENT_API_KEY_V400 |
Bearer token expected from the client per version |
S3 cache (any S3-compatible storage, tested with Garage):
| Variable | Default | Description |
|---|---|---|
S3_ENDPOINT |
http://localhost:3900 |
S3 endpoint URL |
S3_ACCESS_KEY / S3_SECRET_KEY |
— | Credentials |
S3_BUCKET |
markgate-cache |
Bucket name |
S3_CACHE_ENABLED |
true |
Set false to disable caching entirely |
Redis / Valkey:
| Variable | Default | Description |
|---|---|---|
REDIS_HOST / REDIS_PORT |
localhost / 6379 |
Connection |
REDIS_LOCK_TIMEOUT |
300 |
Lock TTL in seconds (auto-extended during processing) |
REDIS_BLOCKING_TIMEOUT |
9999999 |
How long to wait for a lock before returning 504 |
Upstream backends:
| Variable | Description |
|---|---|
UPSTREAM_V100_URL |
Full URL to the foil-serve endpoint |
UPSTREAM_V100_API_KEY |
API key sent to the backend (never exposed to clients) |
| (same pattern for V110, V120, V130, V2, V3, V4) |
Failed requests archiving (for debugging):
| Variable | Default | Description |
|---|---|---|
FAILED_REQUESTS_S3_PREFIX |
failed_requests |
S3 prefix for failed request artifacts |
FAILED_REQUESTS_LOCAL_DIR |
/tmp/markgate_failed |
Local fallback when S3 is unavailable |
S3 bucket layout
📂 S3 Bucket
├── 📂 documents/
│ └── 📂 {sha256}/
│ ├── 📄 source.{ext} # Original file (extension from detected MIME type)
│ ├── 📄 _aliases.json # All filenames seen for this content
│ └── 📂 {version}/
│ ├── 📄 content.md # Converted Markdown
│ ├── 📄 metadata.json # Backend-provided metadata
│ ├── 📄 _metadata.json # Cache hit count, timestamps, last filename
│ └── 📂 images/ # Extracted images (jpg/png/…)
└── 📂 failed_requests/
└── 📂 {timestamp}_{hash}_{version}/
├── 📄 source.{ext} # File that failed
└── 📄 error.json # Error message, upstream duration, context
For Developers
Architecture
Client (e.g., Open WebUI)
│ PUT /md/{version}/process
▼
[ MarkGate ]
│
├── verify_api_key() — check client Bearer token for this version
├── compute_hash() + get_mime_type() — parallel, from raw bytes
├── Redis lock (hash + version) — prevent concurrent duplicate processing
│
├── S3 cache hit? ──yes──► return cached content.md
│
└── no ──► call_upstream_backend()
│
├── _merge_headers() — strip client auth, merge with config.custom_headers
└── POST to backend — foil-serve / docling / …
│
▼
update_s3_processed() — write content.md, metadata, images
background_update_s3() — write source file, _aliases, _metadata
Module responsibilities
| Module | Role |
|---|---|
main.py |
FastAPI app, route handlers, lifespan wiring |
config.py |
Settings (env vars), Version enum, ProcessingConfig (per-version backend URL + auth + query params) |
schemas.py |
Pydantic v2 models: request headers, response, internal document, S3 metadata |
services.py |
Core logic: hash + MIME detection, cache resolution, upstream call, S3 writes, header merging |
storage.py |
S3Manager + RedisManager lifecycle, all S3 I/O helpers, lifespan context manager |
security.py |
verify_api_key() FastAPI dependency |
media.py |
PIL serialization, base64 helpers, libmagic MIME detection, mime_to_ext(), tar.zst builder |
Key design decisions
- MIME type is always detected from bytes via libmagic — the client-declared
Content-Typeis never trusted. The detected MIME is used for the S3ContentType, the S3 key extension, and the upstreamContent-Typeheader. - Redis is used exclusively for distributed locking — not for caching or persistence. S3 is the single source of truth.
- Client auth headers are never forwarded to upstream backends (
Authorizationis stripped). Each backend version has its own credentials defined inProcessingConfig.custom_headers. - Header consolidation: upstream headers (with detected MIME overriding Content-Type) are merged with
config.custom_headers; the config always wins on conflicts. - The proxy is stateless except for the
S3Manager/RedisManagersingletons initialized at lifespan. - Fail fast: upstream errors are propagated to the client (502), artifacts saved to
failed_requests/for debugging.
Adding a new backend
- Add a new
Versionenum value inconfig.py - Add a
ProcessingConfigentry inVERSION_CONFIGSwith the backend URL, client API key, and backend credentials incustom_headers - Add a
case Version.vX_Y_Z:branch incall_upstream_backend()inservices.pythat calls the backend and returns aProcessedDocument
Development setup
Requires Python 3.14 and uv.
uv venv && uv sync # install all dependencies including dev
# Run locally (requires .env or .env_secret)
cd src/margate
uv run uvicorn markgate.main:app --host 0.0.0.0 --port 8080 --reload
# Lint / format / type check
uv run ruff check src/
uv run ruff format src/
uv run ty check src/
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file markgate-0.1.4.tar.gz.
File metadata
- Download URL: markgate-0.1.4.tar.gz
- Upload date:
- Size: 9.5 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3d72022a320c9ca64340269674e6712ced8d0ab3137fc52a3ddf7f87f0f1da8b
|
|
| MD5 |
fe7a8885fb303f4b5f54fd6acfafec80
|
|
| BLAKE2b-256 |
d262ed494cde0a8119dfc2b58ffd47c2432ece4fe554d06def121656d3413c4d
|
Provenance
The following attestation bundles were made for markgate-0.1.4.tar.gz:
Publisher:
pypi-publish.yaml on runyournode/MarkGate
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
markgate-0.1.4.tar.gz -
Subject digest:
3d72022a320c9ca64340269674e6712ced8d0ab3137fc52a3ddf7f87f0f1da8b - Sigstore transparency entry: 1294030480
- Sigstore integration time:
-
Permalink:
runyournode/MarkGate@987fc57bf0fd6ad247cfa68b8e40586e9b48988b -
Branch / Tag:
refs/tags/v0.1.4 - Owner: https://github.com/runyournode
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi-publish.yaml@987fc57bf0fd6ad247cfa68b8e40586e9b48988b -
Trigger Event:
push
-
Statement type:
File details
Details for the file markgate-0.1.4-py3-none-any.whl.
File metadata
- Download URL: markgate-0.1.4-py3-none-any.whl
- Upload date:
- Size: 9.4 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a756680854de14a7d981de958652e02ec47e8058bb508b3be856d39258c23e9d
|
|
| MD5 |
a118d1cfdea92f21ee79522441de5d10
|
|
| BLAKE2b-256 |
539d675dd1b8346a00fe014e78a6bcebd383494f473b7eb34d2dd9f2279f8fd7
|
Provenance
The following attestation bundles were made for markgate-0.1.4-py3-none-any.whl:
Publisher:
pypi-publish.yaml on runyournode/MarkGate
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
markgate-0.1.4-py3-none-any.whl -
Subject digest:
a756680854de14a7d981de958652e02ec47e8058bb508b3be856d39258c23e9d - Sigstore transparency entry: 1294030637
- Sigstore integration time:
-
Permalink:
runyournode/MarkGate@987fc57bf0fd6ad247cfa68b8e40586e9b48988b -
Branch / Tag:
refs/tags/v0.1.4 - Owner: https://github.com/runyournode
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi-publish.yaml@987fc57bf0fd6ad247cfa68b8e40586e9b48988b -
Trigger Event:
push
-
Statement type: