Most apps call APIs through an SDK: OpenAI, Stripe, Twilio and so on. With Vallkey, the app holds a ghost key instead of the real key. A small hook, loaded when your app starts, sends every request that carries a ghost key to Vallkey. Vallkey checks it against your rules, swaps in the real key and passes the answer back. Your code stays exactly as the SDK's docs show it.
How it works
Your app starts, and loads the hook
One setting, a start command or a single line loads it, depending on where the app runs. Nothing else in your code changes.
A request carries a ghost key
The hook sees the ghost key, in a header, the address or a form body, and sends the request to
api.vallkey.comwith the same path. Requests without a ghost key are not touched: your database, your own APIs and every other call go where they always went.Vallkey checks it and swaps in the real key
Your rules decide: which endpoints, how much per day, from where. Allowed requests reach the provider with the real key, which never leaves Vallkey. Blocked ones get a clear error. Every answer comes back with an
x-guard-request-idheader.
What each host needs
The hook is the same everywhere. What changes is how your host starts the app. Links open the step-by-step guide for that host and language.
| Language | Vercel | Railway | Docker or your own server |
|---|---|---|---|
| Node.js | Next.js: instrumentation file. Functions: first linetested | In the start scripttested | NODE_OPTIONS where the app runstested |
| Bun | First line of each functiontested | In the start script | BUN_OPTIONS where the app runstested |
| Python | VALLKEY_AUTO=1 in the variablestested | VALLKEY_AUTO=1 in the variablestested | VALLKEY_AUTO=1tested |
| Ruby | First line of each functiontested | In the Procfiletested | RUBYOPT where the app runstested |
| Java, Kotlin, Scala | Not available | railpack.json or a Dockerfiletested | JAVA_TOOL_OPTIONS and the agent filetested |
| .NET | Not available | railpack.json or a Dockerfiletested | DOTNET_STARTUP_HOOKS and the hook filetested |
| PHP | Not available | A Dockerfile | The extension, with PHP_INI_SCAN_DIRtested |
| Elixir, Erlang | Not available | A Dockerfile | ELIXIR_ERL_OPTIONS and the hook filetested |
| Go, Rust | The SDK's address setting | The SDK's address setting | The SDK's address setting |
Tested means we ran it on a real deployment, or, for Docker, with each language's official SDKs in a container that could reach nothing but Vallkey. The rest follows the same rules but has not been run on that host yet.
The one setting that breaks builds
Node.js, Bun, Ruby, Java and .NET are told to load the hook by a startup setting: NODE_OPTIONS, BUN_OPTIONS, RUBYOPT, JAVA_TOOL_OPTIONS, DOTNET_STARTUP_HOOKS. Every process of that language reads it, including the ones that build your app.
Vercel, Railway, Render and Heroku use your variables while they build
A startup setting in their variables reaches npm install, bundle install or the Java build before the hook is installed, and the build fails. We saw it on Vercel and Railway. Put it where only the running app reads it: the start command, a Procfile, the first line of a function, railpack.json, or the final stage of a Dockerfile, after your app is built.
| Setting | In the host's variables | Where it goes instead |
|---|---|---|
VALLKEY_AUTO=1 | Safe (Python) | Nowhere else needed |
VALLKEY_ENV | Safe | Names the place, such as production |
VALLKEY_DEBUG=1 | Safe | Logs each request sent to Vallkey, never the key |
NODE_OPTIONS | Breaks the build | The start script, or the first line of a function |
RUBYOPT | Breaks the build | The Procfile, or the first line of a function |
JAVA_TOOL_OPTIONS | Breaks the build | railpack.json, or the final stage of a Dockerfile |
DOTNET_STARTUP_HOOKS | Not where the file is | railpack.json, or the final stage of a Dockerfile |
What the hook covers
The HTTP clients each language's hook reaches, and with them the SDKs built on them:
| Language | Covers |
|---|---|
| Node.js and Bun | fetch, http and https: almost every JavaScript SDK |
| Python | httpx, requests, urllib3, boto3, aiohttp, urllib |
| Ruby | Net::HTTP, and Faraday, HTTParty and rest-client on top of it |
| Java, Kotlin, Scala | OkHttp, Apache HttpClient 4 and 5, the JDK HttpClient, HttpURLConnection |
| .NET | HttpClient, on .NET 8 and later |
| PHP | The curl extension: Guzzle, Symfony HttpClient and the SDKs' own clients |
| Elixir, Erlang | Finch (Req), hackney (HTTPoison), httpc (Tesla's default) |
Set it up from your terminal
In your project folder, the CLI tells the language from your files and does or prints each step:
npx vallkey@latest connect # the best way to connect this project; changes nothingnpx vallkey@latest hook # sets up the hook: start script, Procfile, instrumentation file or hook filenpx vallkey@latest doctor # checks this project is ready on this computerIt knows the hosts in this guide: in a Vercel project it shows the first-line setup, for a Node.js app with a start script it offers to change the script, and for Java and .NET it prints the Dockerfile lines and the railpack.json.
Check every deployment
A setup that works on your computer can still miss on a host. One real request from the deployed app settles it:
npx vallkey verify KEY_ID --request-id REQUEST_ID # REQUEST_ID: the x-guard-request-id of your app's answernpx vallkey verify KEY_ID --request-id REQUEST_ID --expect denied # a request your rules block, on a test keynpx vallkey status # every place each key is used fromEach deployment shows up by itself
The hook tells Vallkey where it runs, so a key's Activity tab and vallkey status list each place, such as Vercel · Node.js 24.21 · production or Railway · Python 3.13 · production, with its requests today and anything wrong there.
When to use the address setting instead
Some code cannot load a startup hook. There, set the SDK's API address to Vallkey, one line, shown on each key's Connect tab:
- Edge functions and Next.js Edge routes: the hook covers the Node.js runtime only.
- Go and Rust: compiled apps cannot be changed once built.
- .NET native AOT or trimmed builds, which cannot load startup hooks.
- Web pages and mobile apps: use short-lived keys instead, made for code that runs on people's devices.
Step-by-step guides
- Vercel: Next.js, Node.js and Bun functions, Python, Ruby
- Railway: Node.js, Python, Ruby, Java and .NET, with and without Docker
- No-code mode in the docs: every setting, Docker lines for each language, and the CLI