hodor

Unify your external services

Every API behind one stable URL. hodor injects the credentials; your keys never leave your server.

api.openai.comopenai.example.com
api.stripe.comstripe.example.com
api.linear.applinear.example.com
How it works

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.

Same calls, one key

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.

Today — one key per service
linear · lin_api_xxxx
curl -X POST https://api.linear.app/graphql \
  -H "Authorization: lin_api_xxxx" \
  -d '{"query":"{ viewer { name } }"}'
posthog (EU) · phx_xxxx
curl -H "Authorization: Bearer phx_xxxx" \
  https://eu.posthog.com/api/projects/213853/feature_flags/
cloudflare · cfat_xxxx
curl -H "Authorization: Bearer cfat_xxxx" \
  https://api.cloudflare.com/client/v4/user/tokens/verify
axiom (EU) · xa-xxxx
curl -H "Authorization: Bearer xa-xxxx" \
  https://api.axiom.co/v1/datasets
With hodor — $HODOR_KEY, read-only*
linear.example.com
curl -X POST https://linear.example.com/graphql \
  -H "X-Authorization: Bearer $HODOR_KEY" \
  -d '{"query":"{ viewer { name } }"}'
posthog.example.com
curl -H "X-Authorization: Bearer $HODOR_KEY" \
  https://posthog.example.com/api/projects/213853/feature_flags/
cloudflare.example.com
curl -H "X-Authorization: Bearer $HODOR_KEY" \
  https://cloudflare.example.com/user/tokens/verify
axiom.example.com
curl -H "X-Authorization: Bearer $HODOR_KEY" \
  https://axiom.example.com/v1/datasets

The four upstream credentials stay encrypted on your server — no caller ever holds one. Each caller (agent, cron, script) gets its own hodor key, minted with the finest grain you want: read-only vs write, restricted to specific integrations, restricted to specific paths, revocable in ~30 seconds. Rotating a service key upstream is one admin call, not a hunt through every tool that ever touched it.

Install

One worker, two secrets, one DNS record. No installer, no lock-in — it's ordinary Cloudflare Workers (or a plain Node/Bun process).

Deploy to Cloudflare

Local dev first:

git clone git@github.com:butttons/hodor.git && cd hodor
pnpm install
cp apps/proxy-worker/.dev.vars.example apps/proxy-worker/.dev.vars   # fill the two keys
pnpm --filter @hodor/proxy-worker dev                                # → localhost:8787

Deploy: create the HODOR_KV namespace, add a proxied DNS record (*.example.com192.0.2.0), then npx wrangler deploy --secrets-file .dev.vars for the first deploy (uploads the two required secrets); later deploys are plain wrangler deploy. Full steps in llms.txt.

Example

Configure once through a small admin API — no code, no redeploys. Three calls, one-time setup:

STEP 1

Store the key

Encrypted at rest; hodor is the only reader.

curl -X PUT -H "X-Authorization: Bearer $TOKEN" \
  --data-binary 'sk-...' \
  https://example.com/_/admin/secrets/OPENAI_API_KEY
STEP 2

Register the integration

The full payload — target, the JEXL header that resolves the secret at request time, and an optional probe.

curl -X PATCH https://example.com/_/admin/registry/openai \
  -H "X-Authorization: Bearer $TOKEN" \
  -H 'content-type: application/json' \
  --data-binary @- <<'JSON'
{
  "id": "openai",
  "description": "OpenAI REST API",
  "url": { "host": "api.openai.com", "path": "/v1" },
  "headers": { "Authorization": "'Bearer ' + kv('OPENAI_API_KEY')" },
  "probe": { "method": "GET", "path": "/v1/models" }
}
JSON
STEP 3

Call it

Credentials injected for you, upstream.

curl https://openai.example.com/v1/models
Keys & permissions

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.

DNS & TLS
Security