Railway Integration
Deploy to Railway with KeyEnv secrets auto-sync.
Railway Integration
This guide shows how to use KeyEnv secrets with Railway deployments through automatic syncing via the Railway GraphQL API.
Overview
There are two ways to use KeyEnv with Railway:
| Method | Best For | How It Works |
|---|---|---|
| Auto-sync | Production apps | Push secrets to Railway when they change |
| Build-time injection | Full control | Pull secrets during Railway build |
Prerequisites
- A KeyEnv project with secrets configured
- A Railway project with services
- A Railway API token
Getting Your Railway API Token
- Go to Railway Dashboard
- Click Create Token
- Name it (e.g., "KeyEnv Integration")
- Copy the token
Getting Railway Project and Environment IDs
You'll need your Railway Project ID and Environment ID for the integration.
Using Railway CLI
# Install Railway CLI
npm install -g @railway/cli
# Login and link project
railway login
railway link
# Get project info
railway whoamiUsing Railway Dashboard
- Open your Railway project
- The Project ID is in the URL:
railway.app/project/YOUR_PROJECT_ID - Click on an environment to get its ID from the URL
Using GraphQL API
curl -X POST https://backboard.railway.app/graphql/v2 \
-H "Authorization: Bearer $RAILWAY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"query": "query { me { projects { edges { node { id name environments { edges { node { id name } } } } } } } }"}'Option 1: Auto-Sync (Recommended)
Automatically sync secrets from KeyEnv to Railway when they change.
Manual Sync Script
Create a script to sync secrets from KeyEnv to Railway:
#!/bin/bash
# sync-to-railway.sh
set -e
# Configuration - replace with your values
KEYENV_TOKEN="${KEYENV_TOKEN}"
RAILWAY_TOKEN="${RAILWAY_TOKEN}"
RAILWAY_PROJECT_ID="${RAILWAY_PROJECT_ID:-YOUR_RAILWAY_PROJECT_ID}"
RAILWAY_ENV_ID="${RAILWAY_ENV_ID:-YOUR_RAILWAY_ENVIRONMENT_ID}"
KEYENV_PROJECT_ID="${KEYENV_PROJECT_ID:-YOUR_KEYENV_PROJECT_ID}"
KEYENV_ENV="${KEYENV_ENV:-production}"
# Fetch secrets from KeyEnv
echo "Fetching secrets from KeyEnv..."
secrets=$(curl -sf -H "Authorization: Bearer $KEYENV_TOKEN" \
"https://api.keyenv.dev/api/v1/projects/$KEYENV_PROJECT_ID/environments/$KEYENV_ENV/secrets/export")
# Sync each secret to Railway
echo "$secrets" | jq -c '.secrets[]' | while read -r secret; do
key=$(echo "$secret" | jq -r '.key')
value=$(echo "$secret" | jq -r '.value')
# Upsert variable to Railway
curl -sf -X POST https://backboard.railway.app/graphql/v2 \
-H "Authorization: Bearer $RAILWAY_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"query\": \"mutation { variableUpsert(input: { projectId: \\\"$RAILWAY_PROJECT_ID\\\", environmentId: \\\"$RAILWAY_ENV_ID\\\", name: \\\"$key\\\", value: \\\"$value\\\" }) { id } }\"
}" > /dev/null
echo "Synced: $key"
done
echo "Sync complete!"Make it executable and run:
chmod +x sync-to-railway.sh
# Set environment variables
export KEYENV_TOKEN="srv_your_keyenv_token"
export RAILWAY_TOKEN="your_railway_token"
export RAILWAY_PROJECT_ID="your-railway-project-id"
export RAILWAY_ENV_ID="your-railway-environment-id"
export KEYENV_PROJECT_ID="your-keyenv-project-id"
# Run the sync
./sync-to-railway.shGitHub Actions Workflow
Automatically sync on push to main:
# .github/workflows/sync-secrets.yml
name: Sync Secrets to Railway
on:
push:
branches: [main]
workflow_dispatch:
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Sync KeyEnv to Railway
env:
KEYENV_TOKEN: ${{ secrets.KEYENV_TOKEN }}
RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
RAILWAY_PROJECT_ID: ${{ vars.RAILWAY_PROJECT_ID }}
RAILWAY_ENV_ID: ${{ vars.RAILWAY_ENV_ID }}
KEYENV_PROJECT_ID: ${{ vars.KEYENV_PROJECT_ID }}
KEYENV_ENV: production
run: |
# Fetch secrets from KeyEnv
secrets=$(curl -sf -H "Authorization: Bearer $KEYENV_TOKEN" \
"https://api.keyenv.dev/api/v1/projects/$KEYENV_PROJECT_ID/environments/$KEYENV_ENV/secrets/export")
# Sync each secret to Railway
echo "$secrets" | jq -c '.secrets[]' | while read -r secret; do
key=$(echo "$secret" | jq -r '.key')
value=$(echo "$secret" | jq -r '.value')
curl -sf -X POST https://backboard.railway.app/graphql/v2 \
-H "Authorization: Bearer $RAILWAY_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"query\": \"mutation { variableUpsert(input: { projectId: \\\"$RAILWAY_PROJECT_ID\\\", environmentId: \\\"$RAILWAY_ENV_ID\\\", name: \\\"$key\\\", value: \\\"$value\\\" }) { id } }\"
}" > /dev/null
echo "Synced: $key"
doneGitLab CI Workflow
# .gitlab-ci.yml
sync_secrets:
stage: deploy
image: alpine:latest
variables:
KEYENV_TOKEN: $KEYENV_TOKEN
RAILWAY_TOKEN: $RAILWAY_TOKEN
RAILWAY_PROJECT_ID: "YOUR_RAILWAY_PROJECT_ID"
RAILWAY_ENV_ID: "YOUR_RAILWAY_ENVIRONMENT_ID"
KEYENV_PROJECT_ID: "YOUR_KEYENV_PROJECT_ID"
KEYENV_ENV: "production"
before_script:
- apk add --no-cache curl jq
script:
- |
secrets=$(curl -sf -H "Authorization: Bearer $KEYENV_TOKEN" \
"https://api.keyenv.dev/api/v1/projects/$KEYENV_PROJECT_ID/environments/$KEYENV_ENV/secrets/export")
echo "$secrets" | jq -c '.secrets[]' | while read -r secret; do
key=$(echo "$secret" | jq -r '.key')
value=$(echo "$secret" | jq -r '.value')
curl -sf -X POST https://backboard.railway.app/graphql/v2 \
-H "Authorization: Bearer $RAILWAY_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"query\": \"mutation { variableUpsert(input: { projectId: \\\"$RAILWAY_PROJECT_ID\\\", environmentId: \\\"$RAILWAY_ENV_ID\\\", name: \\\"$key\\\", value: \\\"$value\\\" }) { id } }\"
}" > /dev/null
echo "Synced: $key"
done
only:
- mainOption 2: Build-Time Injection
Pull secrets from KeyEnv during your Railway build.
Using Nixpacks (Default Builder)
Create a nixpacks.toml file in your project root:
# nixpacks.toml
[phases.setup]
cmds = [
"curl -fsSL https://keyenv.dev/install.sh | bash",
"export PATH=\"$HOME/.keyenv/bin:$PATH\"",
"keyenv pull -e production"
]Using Build Command
In Railway service settings, set a custom build command:
curl -fsSL https://keyenv.dev/install.sh | bash && export PATH="$HOME/.keyenv/bin:$PATH" && keyenv pull -e production && npm run buildUsing Dockerfile
# Dockerfile
FROM node:20-alpine
# Install dependencies for KeyEnv
RUN apk add --no-cache curl bash
# Install KeyEnv CLI
RUN curl -fsSL https://keyenv.dev/install.sh | bash
ENV PATH="/root/.keyenv/bin:$PATH"
WORKDIR /app
COPY package*.json ./
RUN npm ci
# Pull secrets at build time
ARG KEYENV_TOKEN
ENV KEYENV_TOKEN=$KEYENV_TOKEN
RUN keyenv pull -e production
COPY . .
RUN npm run build
CMD ["npm", "start"]In Railway, add the build argument:
- Go to Service Settings > Build
- Add Build Argument:
KEYENV_TOKEN - Set the value to your KeyEnv service token
SDK Integration
Node.js
Use the KeyEnv SDK for runtime secret loading:
// src/config.ts
import { KeyEnv } from '@keyenv/node';
const client = new KeyEnv({
token: process.env.KEYENV_TOKEN!,
});
// Load secrets at startup
await client.loadEnv('YOUR_PROJECT_ID', 'production');
// Now use your secrets
console.log(process.env.DATABASE_URL);Python
# config.py
import os
from keyenv import KeyEnv
client = KeyEnv(token=os.environ['KEYENV_TOKEN'])
# Load secrets at startup
client.load_env('YOUR_PROJECT_ID', 'production')
# Now use your secrets
print(os.environ['DATABASE_URL'])Environment Mapping
Map KeyEnv environments to Railway environments:
| KeyEnv Environment | Railway Environment |
|---|---|
development | Development |
staging | Staging |
production | Production |
Use different tokens and environment IDs for each mapping.
Railway GraphQL API Reference
Upsert a Variable
mutation {
variableUpsert(input: {
projectId: "YOUR_PROJECT_ID",
environmentId: "YOUR_ENVIRONMENT_ID",
name: "DATABASE_URL",
value: "postgres://user:pass@host:5432/db"
}) {
id
}
}Delete a Variable
mutation {
variableDelete(input: {
projectId: "YOUR_PROJECT_ID",
environmentId: "YOUR_ENVIRONMENT_ID",
name: "OLD_SECRET"
})
}List All Variables
query {
variables(
projectId: "YOUR_PROJECT_ID",
environmentId: "YOUR_ENVIRONMENT_ID"
)
}curl Example
# Upsert a variable
curl -X POST https://backboard.railway.app/graphql/v2 \
-H "Authorization: Bearer $RAILWAY_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"query": "mutation { variableUpsert(input: { projectId: \"YOUR_PROJECT_ID\", environmentId: \"YOUR_ENVIRONMENT_ID\", name: \"DATABASE_URL\", value: \"postgres://...\" }) { id } }"
}'Troubleshooting
"Unauthorized" error from Railway
- Check that your Railway token is valid
- Ensure the token has access to the project
- Verify the Project ID and Environment ID are correct
Secrets not available at runtime
Railway caches environment variables. After syncing:
- Trigger a new deployment, or
- Use the Railway CLI:
railway redeploy
GraphQL query fails
Ensure you're using the correct API endpoint:
- GraphQL v2:
https://backboard.railway.app/graphql/v2
Rate limiting
Railway's API has rate limits. For large numbers of secrets, add delays between requests:
# Add between API calls
sleep 0.5Sync script fails silently
Add error handling to your sync script:
#!/bin/bash
set -e # Exit on error
# Add -f flag to curl to fail on HTTP errors
secrets=$(curl -sf -H "Authorization: Bearer $KEYENV_TOKEN" \
"https://api.keyenv.dev/api/v1/projects/$KEYENV_PROJECT_ID/environments/$KEYENV_ENV/secrets/export") || {
echo "Failed to fetch secrets from KeyEnv"
exit 1
}Best Practices
- Use service tokens - Never use personal credentials in CI/CD
- Scope tokens narrowly - Production token should only access production
- Sync on deploy - Trigger sync as part of your deployment pipeline
- Use environment mapping - Map KeyEnv environments to Railway environments consistently
- Monitor sync status - Log sync operations for debugging
- Rotate tokens periodically - Update Railway and KeyEnv tokens regularly
Security Considerations
Store your Railway API token securely. Never commit tokens to version control.
- Use GitHub Secrets or Railway's secret management for tokens
- Rotate tokens periodically
- Use separate tokens for different environments
- Audit token usage regularly
Complete Example
Here's a complete setup for a Node.js application with GitHub Actions sync.
Project Structure
my-railway-app/
├── .github/
│ └── workflows/
│ └── sync-secrets.yml
├── scripts/
│ └── sync-to-railway.sh
├── src/
│ └── index.ts
├── package.json
└── Dockerfile.github/workflows/sync-secrets.yml
name: Sync Secrets to Railway
on:
push:
branches: [main]
workflow_dispatch:
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Sync KeyEnv to Railway
env:
KEYENV_TOKEN: ${{ secrets.KEYENV_TOKEN }}
RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
RAILWAY_PROJECT_ID: ${{ vars.RAILWAY_PROJECT_ID }}
RAILWAY_ENV_ID: ${{ vars.RAILWAY_ENV_ID }}
KEYENV_PROJECT_ID: ${{ vars.KEYENV_PROJECT_ID }}
KEYENV_ENV: production
run: |
secrets=$(curl -sf -H "Authorization: Bearer $KEYENV_TOKEN" \
"https://api.keyenv.dev/api/v1/projects/$KEYENV_PROJECT_ID/environments/$KEYENV_ENV/secrets/export")
echo "$secrets" | jq -c '.secrets[]' | while read -r secret; do
key=$(echo "$secret" | jq -r '.key')
value=$(echo "$secret" | jq -r '.value')
curl -sf -X POST https://backboard.railway.app/graphql/v2 \
-H "Authorization: Bearer $RAILWAY_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"query\": \"mutation { variableUpsert(input: { projectId: \\\"$RAILWAY_PROJECT_ID\\\", environmentId: \\\"$RAILWAY_ENV_ID\\\", name: \\\"$key\\\", value: \\\"$value\\\" }) { id } }\"
}" > /dev/null
echo "Synced: $key"
doneGitHub Repository Settings
Add these secrets and variables:
Secrets:
| Name | Value |
|---|---|
KEYENV_TOKEN | srv_your_keyenv_token |
RAILWAY_TOKEN | your_railway_api_token |
Variables:
| Name | Value |
|---|---|
RAILWAY_PROJECT_ID | your-railway-project-id |
RAILWAY_ENV_ID | your-railway-environment-id |
KEYENV_PROJECT_ID | your-keyenv-project-id |
KeyEnv Environments
Create environments in KeyEnv:
| Environment | Secrets |
|---|---|
development | Local development values |
staging | Preview deployment values |
production | Production values |
Deploy Flow
- Update secrets in KeyEnv dashboard
- Push to
mainbranch (or manually trigger workflow) - GitHub Actions syncs secrets to Railway
- Railway redeploys with new secrets
Coming Soon
We're building a native Railway integration that will:
- Connect via OAuth (no API tokens needed)
- Automatically sync on secret changes
- Provide a dashboard to manage environment mappings
- Support webhooks for real-time sync
Join our waitlist for early access.