# Railway Integration

Deploy to Railway with KeyEnv secrets auto-sync.

Source: https://keyenv.dev/docs/guides/railway/

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

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](https://railway.app/account/tokens)
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

```bash
# 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

```bash
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:

```bash
#!/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:

```bash
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:

```yaml
# .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

```yaml
# .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:

```toml
# 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:

```bash
curl -fsSL https://keyenv.dev/install.sh | bash && export PATH="$HOME/.keyenv/bin:$PATH" && keyenv pull -e production && npm run build
```

### Using Dockerfile

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

```typescript
// 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

```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

```graphql
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

```graphql
mutation {
  variableDelete(input: {
    projectId: "YOUR_PROJECT_ID",
    environmentId: "YOUR_ENVIRONMENT_ID",
    name: "OLD_SECRET"
  })
}
```

### List All Variables

```graphql
query {
  variables(
    projectId: "YOUR_PROJECT_ID",
    environmentId: "YOUR_ENVIRONMENT_ID"
  )
}
```

### curl Example

```bash
# 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:

```bash
# Add between API calls
sleep 0.5
```

### Sync script fails silently

Add error handling to your sync script:

```bash
#!/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

> **Warning**
>
> 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

```yaml
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:**
| 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

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.
