Vallkey's startup hook sends every request that carries a ghost key to Vallkey, which checks it against your rules and swaps in the real key. Requests without a ghost key are left alone, and your code calls the API exactly as the provider's docs show. This guide sets it up on Railway for each language, the way we tested it on real deployments.
Before you start
- A ghost key from the dashboard, or from
npx vallkey initin your project. - In your service's Variables, put the ghost key where the real key was, under the same name, such as
OPENAI_API_KEY. - Optional:
VALLKEY_ENV=productionin the same Variables names this place invallkey statusand on the key's Activity tab.
Do not put NODE_OPTIONS, RUBYOPT or JAVA_TOOL_OPTIONS in Railway's variables
Railway uses your service's variables while it builds the app too. With a startup setting there, Railway's own install steps load the hook before it is installed, and the build fails: Cannot find module 'vallkey/auto' for Node.js, a failed gem install bundler for Ruby, Error opening zip file or JAR manifest missing for Java. Put the hook in your start command, a Procfile, railpack.json or a Dockerfile, as below. Python's VALLKEY_AUTO=1 is safe in the variables.
Set it up
Choose your language. Each tab is complete on its own.
Tested: Node.js 20 on Railway, fetch and node:https
Install the package
Terminalnpm install vallkeyLoad the hook in your start script
npx vallkey hookoffers to change your start script inpackage.json, and shows the change first:package.json"scripts": { "start": "node --require vallkey/auto server.js"}Starting with something other than
node, such as tsx? Put the setting in front of the command instead. We tested this form withnode; it works the same in front of any command:package.json"scripts": { "start": "NODE_OPTIONS='--require vallkey/auto' tsx server.ts"}Deploy
Push, or run
railway up.
Tested: Python 3.13 on Railway, requests, httpx and urllib
Add vallkey to your dependencies
requirements.txtvallkeyrequestsTurn the hook on with one variable
In the service's Variables, add
VALLKEY_AUTOwith the value1. Python loads the hook by itself when it starts. No code changes, and no start command to edit.Terminalrailway variables --set VALLKEY_AUTO=1Deploy
Setting a variable redeploys the service. Otherwise push, or run
railway up.
Tested: Ruby on Railway, Net::HTTP
Add the gem
Terminalbundle add vallkeyLoad the hook from your Procfile
npx vallkey hookreads your Procfile and shows the line to use. For a plain Ruby app:Procfileweb: bundle exec ruby -rvallkey/auto app.rbFor any other command, such as Puma or Rails, put the setting in front of it. We tested this form with a plain Ruby app; it works the same in front of any command:
Procfileweb: RUBYOPT=-rvallkey/auto bundle exec puma -C config/puma.rbFaraday, HTTParty and rest-client are built on Net::HTTP, so they are covered too.
Deploy
Push, or run
railway up.
Tested: Java 21 on Railway, HttpClient and HttpURLConnection, with and without Docker
The Java hook is a file, vallkey-agent.jar, so it has to be part of what Railway deploys. Kotlin and Scala work the same way. Two ways, both tested:
Without Docker: railpack.json
Download the hook into your project
Terminalnpx vallkey hook javaIt saves
.vallkey/vallkey-agent.jarand checks its checksum. Commit the.vallkeyfolder: Railway builds what is in your repository.Add railpack.json next to it
railpack.json{ "deploy": { "inputs": ["...", { "local": true, "include": [".vallkey"] }], "variables": { "JAVA_TOOL_OPTIONS": "-javaagent:/app/.vallkey/vallkey-agent.jar" } }}inputscopies the hook file into the deployed app: Railway's Java build keeps only target/, so without it the file would not be there.variablessets the setting for the running app only, never for the build.npx vallkey hook javaprints this file for you.Deploy
Push, or run
railway up.
With Docker
Railway builds your Dockerfile when it finds one. Add the two lines npx vallkey hook java prints to the final stage, after your app is built. They download the hook, check its checksum, and turn it on:
# The first line of the file must be: # syntax=docker/dockerfile:1ADD --checksum=sha256:<from npx vallkey hook java> https://vallkey.com/downloads/hooks/0.1.0/vallkey-agent-0.1.0.jar /opt/vallkey/vallkey-agent.jarENV JAVA_TOOL_OPTIONS="-javaagent:/opt/vallkey/vallkey-agent.jar"Tested: .NET 10 on Railway, HttpClient, with and without Docker
The .NET hook is a file, Vallkey.Auto.dll, loaded by DOTNET_STARTUP_HOOKS. It needs .NET 8 or later; apps published as native AOT or trimmed cannot load it. Two ways, both tested:
Without Docker: railpack.json
Download the hook into your project
Terminalnpx vallkey hook dotnetIt saves
.vallkey/Vallkey.Auto.dlland checks its checksum. Commit the.vallkeyfolder: Railway builds what is in your repository.Add railpack.json next to it
railpack.json{ "deploy": { "inputs": ["...", { "local": true, "include": [".vallkey"] }], "variables": { "DOTNET_STARTUP_HOOKS": "/app/.vallkey/Vallkey.Auto.dll" } }}inputscopies the hook file into the deployed app: the hook file has to be next to your app when it runs.variablessets the setting for the running app only, never for the build.npx vallkey hook dotnetprints this file for you.Deploy
Push, or run
railway up.
With Docker
Railway builds your Dockerfile when it finds one. Add the two lines npx vallkey hook dotnet prints to the final stage, after your app is built. They download the hook, check its checksum, and turn it on:
# The first line of the file must be: # syntax=docker/dockerfile:1ADD --checksum=sha256:<from npx vallkey hook dotnet> https://vallkey.com/downloads/hooks/0.1.0/Vallkey.Auto-0.1.0.dll /opt/vallkey/Vallkey.Auto.dllENV DOTNET_STARTUP_HOOKS="/opt/vallkey/Vallkey.Auto.dll"Check that it works
Every answer that went through Vallkey carries an x-guard-request-id header. With one real request from your deployed service, the CLI confirms it was that request, through that key:
# 1. Call your deployed app once, then copy the x-guard-request-id# header from the response Vallkey sent back to your service.# 2. Check that exact request:npx vallkey verify KEY_ID --request-id REQUEST_ID ✓ Verified This exact request went through Vallkey and got a successful answer.- On a test key, make a harmless request your rules block, and check it with
--expect denied. Seeing it refused proves your rules apply to the deployed service. npx vallkey statuslists every place the key is used from. A working service shows up by itself, such as Railway · Node.js 20.20 · production or Railway · Java 21.0 · production.
Every Railway service is a place of its own
Give each service its own ghost key for the same real key (Add another place on the key's page): each gets its own rules, limits and kill switch, and vallkey status shows each one apart.
If something is off
The build fails with Cannot find module 'vallkey/auto'
NODE_OPTIONS is set in the service's variables. Delete it there and put the hook in your start script. The same goes for RUBYOPT (the build fails at gem install bundler) and JAVA_TOOL_OPTIONS (it fails with Error opening zip file).
Java or .NET starts, but requests do not go through Vallkey
The hook file is not in the deployed app, or the setting points to the wrong place. Check that the .vallkey folder is committed and that railpack.json includes it. The path in the setting starts with /app/, where Railway puts your app.
The provider says the API key is invalid
The request went straight to the provider with the ghost key. The hook is not loaded: check the start command or Procfile Railway actually uses, under the service's Settings → Deploy. A custom start command set there replaces your package.json script and Procfile.
No x-guard-request-id header
That request did not go through Vallkey. Add VALLKEY_DEBUG=1 to the variables to log each request the hook sends to Vallkey (never the key), then read the deploy logs.
More
- How the hook works, for every language and host
- The same on Vercel
- No-code mode in the docs, with the HTTP clients each language covers