KeyEnvKeyEnv

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:

MethodBest ForHow It Works
Auto-syncProduction appsPush secrets to Railway when they change
Build-time injectionFull controlPull secrets during Railway build

Prerequisites

  1. A KeyEnv project with secrets configured
  2. A Railway project with services
  3. A Railway API token

Getting Your Railway API Token

  1. Go to Railway Dashboard
  2. Click Create Token
  3. Name it (e.g., "KeyEnv Integration")
  4. 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 whoami

Using Railway Dashboard

  1. Open your Railway project
  2. The Project ID is in the URL: railway.app/project/YOUR_PROJECT_ID
  3. 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 } } } } } } } }"}'

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.sh

GitHub 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"
          done

GitLab 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:
    - main

Option 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 build

Using 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:

  1. Go to Service Settings > Build
  2. Add Build Argument: KEYENV_TOKEN
  3. 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 EnvironmentRailway Environment
developmentDevelopment
stagingStaging
productionProduction

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

  1. Check that your Railway token is valid
  2. Ensure the token has access to the project
  3. Verify the Project ID and Environment ID are correct

Secrets not available at runtime

Railway caches environment variables. After syncing:

  1. Trigger a new deployment, or
  2. 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.5

Sync 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

  1. Use service tokens - Never use personal credentials in CI/CD
  2. Scope tokens narrowly - Production token should only access production
  3. Sync on deploy - Trigger sync as part of your deployment pipeline
  4. Use environment mapping - Map KeyEnv environments to Railway environments consistently
  5. Monitor sync status - Log sync operations for debugging
  6. 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"
          done

GitHub Repository Settings

Add these secrets and variables:

Secrets:

NameValue
KEYENV_TOKENsrv_your_keyenv_token
RAILWAY_TOKENyour_railway_api_token

Variables:

NameValue
RAILWAY_PROJECT_IDyour-railway-project-id
RAILWAY_ENV_IDyour-railway-environment-id
KEYENV_PROJECT_IDyour-keyenv-project-id

KeyEnv Environments

Create environments in KeyEnv:

EnvironmentSecrets
developmentLocal development values
stagingPreview deployment values
productionProduction values

Deploy Flow

  1. Update secrets in KeyEnv dashboard
  2. Push to main branch (or manually trigger workflow)
  3. GitHub Actions syncs secrets to Railway
  4. 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.

On this page