# Rails Integration

Use KeyEnv with Ruby on Rails applications for secure secrets management.

Source: https://keyenv.dev/docs/sdks/rails/

Integrate KeyEnv with your Ruby on Rails application to securely manage secrets like database credentials, API keys, and Rails secret key base.

## Features

- Load secrets before Rails initializes
- Environment-aware configuration (development, test, production)
- Works with Rails credentials and encrypted secrets
- Supports database.yml, storage.yml, and other config files
- Built-in caching for serverless environments

## Installation

Add the KeyEnv gem to your Gemfile:

```ruby
# Gemfile
gem 'keyenv'
```

Then install:

```bash
bundle install
```

## Quick Start

Create an initializer to load secrets at boot:

```ruby
# config/initializers/keyenv.rb
if ENV['KEYENV_TOKEN'].present?
  require 'keyenv'

  client = KeyEnv.new(token: ENV['KEYENV_TOKEN'])
  client.load_env(
    project_id: ENV['KEYENV_PROJECT'],
    environment: Rails.env
  )
end
```

Now use secrets in your configuration:

```ruby
# config/application.rb
config.secret_key_base = ENV['SECRET_KEY_BASE']
```

## Rails Initializer Setup

### Basic Initializer

The simplest approach loads all secrets into `ENV` at startup:

```ruby
# config/initializers/keyenv.rb
if ENV['KEYENV_TOKEN'].present?
  require 'keyenv'

  # Map Rails.env to KeyEnv environment
  keyenv_environment = case Rails.env
                       when 'development' then 'development'
                       when 'test' then 'development'
                       when 'staging' then 'staging'
                       when 'production' then 'production'
                       else Rails.env
                       end

  client = KeyEnv.new(token: ENV['KEYENV_TOKEN'])
  count = client.load_env(
    project_id: ENV['KEYENV_PROJECT'],
    environment: keyenv_environment
  )

  Rails.logger.info "KeyEnv: Loaded #{count} secrets for #{keyenv_environment}" if defined?(Rails.logger)
end
```

### With Caching (Serverless)

For serverless environments like AWS Lambda, enable caching:

```ruby
# config/initializers/keyenv.rb
if ENV['KEYENV_TOKEN'].present?
  require 'keyenv'

  client = KeyEnv.new(
    token: ENV['KEYENV_TOKEN'],
    cache_ttl: 300  # Cache for 5 minutes
  )

  client.load_env(
    project_id: ENV['KEYENV_PROJECT'],
    environment: Rails.env
  )
end
```

### Early Loading with boot.rb

For secrets needed before initializers run (like database credentials), load in `boot.rb`:

```ruby
# config/boot.rb
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)

require 'bundler/setup'
require 'bootsnap/setup' if defined?(Bootsnap)

# Load KeyEnv secrets before Rails initializes
if ENV['KEYENV_TOKEN'].present?
  require 'keyenv'

  rails_env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
  client = KeyEnv.new(token: ENV['KEYENV_TOKEN'])
  client.load_env(
    project_id: ENV['KEYENV_PROJECT'],
    environment: rails_env
  )
end
```

## Environment-Aware Configuration

### Automatic Environment Detection

Map Rails environments to KeyEnv environments:

```ruby
# config/initializers/keyenv.rb
if ENV['KEYENV_TOKEN'].present?
  require 'keyenv'

  def detect_keyenv_environment
    # Check explicit override first
    return ENV['KEYENV_ENV'] if ENV['KEYENV_ENV'].present?

    # Map Rails.env to KeyEnv environment
    case Rails.env
    when 'development', 'test'
      'development'
    when 'staging', 'review'
      'staging'
    when 'production'
      'production'
    else
      Rails.env
    end
  end

  client = KeyEnv.new(token: ENV['KEYENV_TOKEN'])
  client.load_env(
    project_id: ENV['KEYENV_PROJECT'],
    environment: detect_keyenv_environment
  )
end
```

### Platform-Specific Detection

Detect environment from deployment platforms:

```ruby
# config/initializers/keyenv.rb
def detect_keyenv_environment
  return ENV['KEYENV_ENV'] if ENV['KEYENV_ENV'].present?

  # Heroku
  if ENV['HEROKU_APP_NAME'].present?
    return ENV['HEROKU_APP_NAME'].include?('staging') ? 'staging' : 'production'
  end

  # Render
  return 'production' if ENV['RENDER'].present?

  # Railway
  return ENV['RAILWAY_ENVIRONMENT'] if ENV['RAILWAY_ENVIRONMENT'].present?

  # Fly.io
  return 'production' if ENV['FLY_APP_NAME'].present?

  # Fall back to Rails.env mapping
  case Rails.env
  when 'development', 'test' then 'development'
  when 'staging' then 'staging'
  else 'production'
  end
end
```

## Database Configuration

### Using database.yml with ERB

Reference secrets in `database.yml`:

```yaml
# config/database.yml
default: &default
  adapter: postgresql
  encoding: unicode
  pool: <%= ENV.fetch("DB_POOL", 5) %>

development:
  <<: *default
  database: myapp_development

test:
  <<: *default
  database: myapp_test

production:
  <<: *default
  url: <%= ENV['DATABASE_URL'] %>
```

Store in KeyEnv:
```
DATABASE_URL=postgres://user:password@host:5432/myapp_production
DB_POOL=25
```

### Individual Database Credentials

For more control, use individual variables:

```yaml
# config/database.yml
production:
  adapter: postgresql
  encoding: unicode
  host: <%= ENV['DB_HOST'] %>
  port: <%= ENV.fetch('DB_PORT', 5432) %>
  database: <%= ENV['DB_NAME'] %>
  username: <%= ENV['DB_USER'] %>
  password: <%= ENV['DB_PASSWORD'] %>
  pool: <%= ENV.fetch('DB_POOL', 5) %>
```

Store in KeyEnv:
```
DB_HOST=your-db-host.amazonaws.com
DB_PORT=5432
DB_NAME=myapp_production
DB_USER=myapp
DB_PASSWORD=secure-password
```

## Storage Configuration

### Active Storage with S3

```yaml
# config/storage.yml
amazon:
  service: S3
  access_key_id: <%= ENV['AWS_ACCESS_KEY_ID'] %>
  secret_access_key: <%= ENV['AWS_SECRET_ACCESS_KEY'] %>
  region: <%= ENV.fetch('AWS_REGION', 'us-east-1') %>
  bucket: <%= ENV['AWS_S3_BUCKET'] %>
```

Store in KeyEnv:
```
AWS_ACCESS_KEY_ID=AKIA...
AWS_SECRET_ACCESS_KEY=secret...
AWS_REGION=us-east-1
AWS_S3_BUCKET=myapp-production
```

### Active Storage with Google Cloud

```yaml
# config/storage.yml
google:
  service: GCS
  project: <%= ENV['GCS_PROJECT'] %>
  credentials: <%= ENV['GCS_CREDENTIALS'] %>
  bucket: <%= ENV['GCS_BUCKET'] %>
```

## Rails Credentials Integration

### Hybrid Approach

Use KeyEnv for deployment secrets and Rails credentials for development:

```ruby
# config/initializers/keyenv.rb
if ENV['KEYENV_TOKEN'].present?
  require 'keyenv'

  client = KeyEnv.new(token: ENV['KEYENV_TOKEN'])
  client.load_env(
    project_id: ENV['KEYENV_PROJECT'],
    environment: Rails.env
  )
else
  # Fall back to Rails credentials in development
  Rails.logger.info 'KeyEnv: Using Rails credentials (KEYENV_TOKEN not set)'
end
```

### Accessing Secrets

Create a helper that checks ENV first, then falls back to credentials:

```ruby
# config/initializers/secrets.rb
module Secrets
  class << self
    def fetch(key, default = nil)
      ENV[key.to_s.upcase] || Rails.application.credentials.dig(key) || default
    end

    def [](key)
      fetch(key)
    end
  end
end
```

Usage:
```ruby
Secrets.fetch(:stripe_secret_key)
Secrets[:aws_access_key_id]
```

## Configuration Patterns

### Action Mailer

```ruby
# config/environments/production.rb
config.action_mailer.smtp_settings = {
  address: ENV.fetch('SMTP_HOST', 'smtp.gmail.com'),
  port: ENV.fetch('SMTP_PORT', 587).to_i,
  user_name: ENV['SMTP_USERNAME'],
  password: ENV['SMTP_PASSWORD'],
  authentication: :plain,
  enable_starttls_auto: true
}
```

### Redis / Sidekiq

```ruby
# config/initializers/sidekiq.rb
Sidekiq.configure_server do |config|
  config.redis = { url: ENV.fetch('REDIS_URL', 'redis://localhost:6379/0') }
end

Sidekiq.configure_client do |config|
  config.redis = { url: ENV.fetch('REDIS_URL', 'redis://localhost:6379/0') }
end
```

### Action Cable

```ruby
# config/cable.yml
production:
  adapter: redis
  url: <%= ENV.fetch('REDIS_URL', 'redis://localhost:6379/1') %>
  channel_prefix: myapp_production
```

## Complete Example

Here's a complete Rails configuration using KeyEnv:

```ruby
# config/initializers/keyenv.rb
if ENV['KEYENV_TOKEN'].present?
  require 'keyenv'

  KEYENV_ENV_MAP = {
    'development' => 'development',
    'test' => 'development',
    'staging' => 'staging',
    'production' => 'production'
  }.freeze

  keyenv_env = ENV.fetch('KEYENV_ENV') { KEYENV_ENV_MAP.fetch(Rails.env, Rails.env) }

  begin
    client = KeyEnv.new(
      token: ENV['KEYENV_TOKEN'],
      timeout: 10,
      cache_ttl: Rails.env.production? ? 300 : 0
    )

    count = client.load_env(
      project_id: ENV['KEYENV_PROJECT'],
      environment: keyenv_env
    )

    Rails.logger.info "KeyEnv: Loaded #{count} secrets for #{keyenv_env}" if defined?(Rails.logger)
  rescue KeyEnv::Error => e
    Rails.logger.error "KeyEnv: Failed to load secrets - #{e.message}" if defined?(Rails.logger)
    raise if Rails.env.production?  # Fail hard in production
  end
end
```

```yaml
# config/database.yml
default: &default
  adapter: postgresql
  encoding: unicode
  pool: <%= ENV.fetch("DB_POOL", 5) %>

development:
  <<: *default
  database: myapp_development

test:
  <<: *default
  database: myapp_test

production:
  <<: *default
  url: <%= ENV['DATABASE_URL'] %>
```

```ruby
# config/environments/production.rb
Rails.application.configure do
  # Secret key base
  config.secret_key_base = ENV['SECRET_KEY_BASE']

  # Force SSL
  config.force_ssl = true

  # Action Mailer
  config.action_mailer.smtp_settings = {
    address: ENV['SMTP_HOST'],
    port: ENV.fetch('SMTP_PORT', 587).to_i,
    user_name: ENV['SMTP_USERNAME'],
    password: ENV['SMTP_PASSWORD'],
    authentication: :plain,
    enable_starttls_auto: true
  }

  # Logging
  config.log_level = ENV.fetch('LOG_LEVEL', 'info').to_sym
end
```

```ruby
# config/initializers/stripe.rb
Stripe.api_key = ENV['STRIPE_SECRET_KEY']
```

```ruby
# config/initializers/sentry.rb
if ENV['SENTRY_DSN'].present?
  Sentry.init do |config|
    config.dsn = ENV['SENTRY_DSN']
    config.environment = Rails.env
    config.traces_sample_rate = 0.1
  end
end
```

## Setting Up Your Deployment

### Required Environment Variables

Set these in your deployment platform (not in KeyEnv):

| Variable | Description |
|----------|-------------|
| `KEYENV_TOKEN` | Your KeyEnv service token |
| `KEYENV_PROJECT` | Your KeyEnv project ID |
| `KEYENV_ENV` | Override environment detection (optional) |

### Secrets to Store in KeyEnv

Store these secrets in KeyEnv for each environment:

| Secret | Description | Example |
|--------|-------------|---------|
| `SECRET_KEY_BASE` | Rails secret key | `rails secret` output |
| `DATABASE_URL` | Database connection | `postgres://user:pass@host/db` |
| `REDIS_URL` | Redis connection | `redis://localhost:6379/0` |
| `SMTP_PASSWORD` | Email password | `app-specific-password` |
| `AWS_ACCESS_KEY_ID` | AWS credentials | `AKIA...` |
| `AWS_SECRET_ACCESS_KEY` | AWS secret | `...` |
| `STRIPE_SECRET_KEY` | Stripe API key | `sk_live_...` |
| `SENTRY_DSN` | Sentry DSN | `https://...@sentry.io/...` |

### Generate a Rails Secret Key

```bash
rails secret
```

## Local Development

### Without KeyEnv

For local development without KeyEnv, the initializer gracefully skips loading:

```ruby
# config/initializers/keyenv.rb
if ENV['KEYENV_TOKEN'].present?
  # ... KeyEnv loading
end

# Falls back to local environment variables or Rails credentials
```

### Using the CLI

Generate a `.env` file for local development:

```bash
keyenv pull --env development --output .env
```

Then use dotenv to load it:

```ruby
# Gemfile (development group)
group :development, :test do
  gem 'dotenv-rails'
end
```

The dotenv gem loads `.env` automatically before Rails initializers.

## Testing

### Test Environment Setup

Skip KeyEnv in tests by not setting `KEYENV_TOKEN`:

```ruby
# spec/rails_helper.rb
# Test environment variables
ENV['SECRET_KEY_BASE'] = 'test-secret-key-base'
ENV['DATABASE_URL'] = 'postgres://localhost/myapp_test'
```

Or use a separate test environment in KeyEnv:

```ruby
# config/initializers/keyenv.rb
keyenv_env = case Rails.env
             when 'test' then 'test'  # Use KeyEnv test environment
             # ...
             end
```

### RSpec Configuration

```ruby
# spec/support/keyenv.rb
RSpec.configure do |config|
  config.before(:suite) do
    # Ensure test secrets are available
    ENV['SECRET_KEY_BASE'] ||= 'test-secret'
    ENV['STRIPE_SECRET_KEY'] ||= 'sk_test_fake'
  end
end
```

## Troubleshooting

### "KEYENV_TOKEN not set"

In production, ensure `KEYENV_TOKEN` is set in your deployment platform's environment variables.

### "Project not found"

Verify `KEYENV_PROJECT` matches your project ID in the KeyEnv dashboard.

### "Environment not found"

Check that the environment name exists in your KeyEnv project and matches your Rails environment mapping.

### Secrets not loading in database.yml

If database credentials aren't available when Rails initializes:

1. Move KeyEnv loading to `config/boot.rb` (before database.yml is processed)
2. Or use `DATABASE_URL` which Rails handles specially

### Debugging

Enable debug logging:

```ruby
# config/initializers/keyenv.rb
if ENV['KEYENV_TOKEN'].present?
  require 'keyenv'

  client = KeyEnv.new(token: ENV['KEYENV_TOKEN'])

  begin
    secrets = client.export_secrets(
      project_id: ENV['KEYENV_PROJECT'],
      environment: Rails.env
    )
    Rails.logger.debug "KeyEnv: Found #{secrets.count} secrets"
    secrets.each { |s| Rails.logger.debug "KeyEnv: - #{s.key}" }
    client.load_env(project_id: ENV['KEYENV_PROJECT'], environment: Rails.env)
  rescue KeyEnv::Error => e
    Rails.logger.error "KeyEnv: #{e.class} - #{e.message}"
    raise
  end
end
```

### Performance

`load_env` makes a single API call at startup. Secrets are cached in `ENV` for the lifetime of the process. There's no runtime overhead after initialization.

For serverless environments, use `cache_ttl` to avoid repeated API calls on warm starts.
