Unify your external services
Every API behind one stable URL. hodor injects the credentials; your keys never leave your server.
One URL per service
openai.example.com is an integration. Subdomain routing — no path
config, nothing to remember.
Credentials handled
Store the key once, encrypted. hodor builds the auth header at request time — callers never see a credential.
Plain HTTP in and out
curl, scripts, agents, browsers. If it speaks HTTPS it works. No SDK, no protocol, no MCP.
Four services, four credentials, four leak surfaces — vs the same four calls with one
hodor key and nothing else. Same curl, same JSON, same responses; the URL changed and the
keys left the room. (hcurl below is a tiny executable wrapping curl with your key; see Example.)
curl -X POST https://api.linear.app/graphql \
-H "Authorization: lin_api_xxxx" \
-d '{"query":"{ viewer { name } }"}'
curl -H "Authorization: Bearer phx_xxxx" \
https://eu.posthog.com/api/projects/213853/feature_flags/
curl -H "Authorization: Bearer cfat_xxxx" \
https://api.cloudflare.com/client/v4/user/tokens/verify
curl -H "Authorization: Bearer xa-xxxx" \
https://api.axiom.co/v1/datasets
hcurl linear.example.com/graphql -X POST \
-d '{"query":"{ viewer { name } }"}'
hcurl posthog.example.com/api/projects/213853/feature_flags/
hcurl cloudflare.example.com/user/tokens/verify
hcurl axiom.example.com/v1/datasets
The four upstream credentials stay encrypted on your server — no caller ever holds one. Each caller gets its own key, minted at the finest grain you want (see Keys & permissions). Rotating a service key upstream is one admin call, not a hunt through every tool that ever touched it.
Shell setup
Two files, then hcurl openai.example.com/v1/models | jq. No rc
function — those never reach agents, cron, or non-interactive ssh.
~/.local/bin/hcurl
On PATH (/usr/local/bin for shared boxes).
#!/bin/bash
. ~/.hodor-env
exec curl -sS -H "X-Authorization: Bearer $HPT" "https://$1" "${@:2}"
~/.hodor-env
Never commit. Point BASH_ENV here for non-interactive shells.
export HPT='<proxy-token>'
export HAT='<admin-token>'
One worker, two secrets, one DNS record. No installer, no lock-in — it's ordinary Cloudflare Workers (or a plain Node/Bun process).
Local dev first:
Clone
git clone git@github.com:butttons/hodor.git && cd hodor
Install
pnpm install
Fill the two keys
cp apps/proxy-worker/.dev.vars.example apps/proxy-worker/.dev.vars
Run
→ localhost:8787
pnpm --filter @hodor/proxy-worker dev
Deploy in four steps:
-
Fill the two secrets:
cp apps/proxy-worker/.dev.vars.example apps/proxy-worker/.dev.vars -
Create the KV namespace:
npx wrangler kv namespace create HODOR_KV -
Add the DNS record (see DNS & TLS — do not skip):
proxiedArecord*.example.com→192.0.2.0 -
Deploy — first deploy uploads secrets, later ones are plain:
npx wrangler deploy --secrets-file .dev.vars(first time only)npx wrangler deploy(every time after)
Full steps in llms.txt — including the production-vs-local secret
warning (minting against the deployed worker needs the production
HODOR_JWT_SECRET, not .dev.vars) and troubleshooting.
Configure once through a small admin API — no code, no redeploys. Four calls, one-time setup (minting is the only open path — everything after needs the key):
Mint an admin key
Knowledge-based: secret must equal HODOR_JWT_SECRET (production, not .dev.vars).
curl -X POST https://example.com/_/keys \
-H 'content-type: application/json' \
-d '{"secret":"$HODOR_JWT_SECRET","name":"admin","scopes":["admin"]}'
Store the key
Encrypted at rest; hodor is the only reader.
curl -X PUT -H "X-Authorization: Bearer $HAT" \
--data-binary 'sk-...' \
https://example.com/_/admin/secrets/OPENAI_API_KEY
Register the integration
Target, auth header (pick a secret helper), optional probe.
curl -X PATCH https://example.com/_/admin/registry/openai \
-H "X-Authorization: Bearer $HAT" \
-H 'content-type: application/json' \
--data-binary @- <<'JSON'
{
"id": "openai",
"url": { "host": "api.openai.com", "path": "/v1" },
"headers": { "Authorization": "'Bearer ' + kv('OPENAI_API_KEY')" },
"meta": { "label": "OpenAI", "description": "OpenAI REST API" },
"probe": { "method": "GET", "path": "/v1/models" }
}
JSON
Call it
Credentials injected for you, upstream. Canonical CLI (see Shell setup above):
hcurl openai.example.com/v1/models
One signed JWT per consumer. Restrictions use a single vocabulary — only /
except — globally and per integration. except always wins;
per-item rules narrow the globals, never widen.
{
"scopes": ["proxy:call"],
"integrations": [
{"id": "stripe"},
{"id": "razorpay", "only": {"methods": ["GET", "HEAD"]}}
]
}
Stripe stays broad, Razorpay is read-only — on the same key. Mint at
POST /_/keys, revoke one key without touching the rest, and read every
integration in the catalog.
Header values are JEXL expressions. Seven helpers, picked per deployment:
-
kv('NAME')— hodor's own encrypted KV store. Portable, works everywhere. -
secret('NAME')— plain worker secret binding. Zero extra dependencies. -
secret_store('NAME')— Cloudflare Secrets Store viaenv.NAME.get(). Strongest isolation. -
variable('NAME')— plain-text env var. IDs, usernames, account IDs — never secrets. -
process_env('NAME')— hostprocess.env. Standalone Node/Bun only. -
jwt({alg, key, claims[, kid, typ, ttl]})— sign a compact JWS at request time via WebCrypto ('ES256'/'RS256', PKCS8 PEM key,iat/expinjected). Apple App Store Server API with zero refresher infra. -
google_oauth({account, scope[, ttl]})— service-account JWT-bearer exchange atoauth2.googleapis.com/token, token cached in-isolate. Google Play Developer API with zero refresher infra.
Rule: passwords/tokens/keys → secrets; IDs/usernames → vars +
variable(). Example: 'Bearer ' + kv('OPENAI_API_KEY'). Reserved
helper names can't be shadowed.
Do not skip this. The wildcard (*.example.com) is a worker
route, and routes do not create DNS — you must add the record by hand: a
proxied A record for * pointing at the dummy address
192.0.2.0. Without it the worker deploys fine and serves nothing. The apex
(example.com) is the opposite: attach it as a custom domain and
Cloudflare provisions DNS + certificate automatically.
-
Free tier works out of the box. The proxy runs at
*.example.com(first level), which Cloudflare's free Universal SSL covers — apex and first level need zero certificate setup. -
Deeper subdomains (
a.b.example.com) are a TLS thing, not a DNS thing — DNS routes at any depth, certificates don't. Those need Total TLS (paid) or a custom certificate (Business plan).
- Keys stay encrypted. Credentials are AES-GCM ciphertext in KV; only hodor can decrypt them, and only at request time. Access itself is scoped per key (see Keys & permissions) — revoke one in ~30 seconds, or rotate the signing secret to void everything at once.
- Complete audit log. Every request is recorded — who called which integration, when, and what came back — including rejections and upstream failures. Query it over SQL, dashboard or API (see llms.txt).
hodor