Migrating from Redis to Adapters
Amber 2.0 replaces hard-coded Redis dependencies with a pluggable adapter system. You can now choose the best storage backend for your deployment: cookies, Redis, memory, or custom implementations.
Why the Change?
| Aspect | Amber 1.x | Amber 2.0 | |--------|-----------|-----------| | Session Storage | Redis required | Cookie, Redis, Memory, or custom | | WebSocket PubSub | Redis required | Memory, Redis, or custom | | Dependencies | Always need Redis | Use what fits your needs | | Development | Redis must be running | Works out of the box | | Deployment | More infrastructure | Deploy anywhere |
Session Migration
Before (Amber 1.x)
# config/application.cr
Amber::Server.configure do |app|
app.session = {
:redis => Redis.new(url: ENV["REDIS_URL"]),
:key => "session_id",
:secret => ENV["SECRET_KEY_BASE"]
}
end
After (Amber 2.0)
Option 1: Cookie Store (Recommended for most apps)
# config/application.cr
require "amber/session_adapters/cookie_store"
Amber::Server.configure do |app|
app.session = Amber::SessionAdapters::CookieStore.new(
secret_key: ENV["SECRET_KEY_BASE"],
session_key: "_myapp_session",
expire_after: 24.hours
)
end
Benefits:
- No external dependencies
- Scales horizontally without shared state
- Session travels with the request
Limitations:
- 4KB size limit
- Data visible to client (encrypted, but visible)
Option 2: Redis Store (For existing Redis users)
# config/application.cr
require "amber/session_adapters/redis_store"
Amber::Server.configure do |app|
app.session = Amber::SessionAdapters::RedisStore.new(
redis_url: ENV["REDIS_URL"],
session_key: "_myapp_session",
expire_after: 24.hours
)
end
Use when:
- You already have Redis infrastructure
- Sessions need to exceed 4KB
- You need server-side session invalidation
Option 3: Memory Store (Development/Testing)
# config/application.cr
require "amber/session_adapters/memory_store"
Amber::Server.configure do |app|
app.session = Amber::SessionAdapters::MemoryStore.new(
session_key: "_myapp_session"
)
end
Note: Memory store doesn't persist across restarts and doesn't scale horizontally.
Environment-Based Configuration
# config/application.cr
Amber::Server.configure do |app|
app.session = case ENV["AMBER_ENV"]?
when "production"
if ENV["REDIS_URL"]?
Amber::SessionAdapters::RedisStore.new(
redis_url: ENV["REDIS_URL"],
session_key: "_myapp_session"
)
else
Amber::SessionAdapters::CookieStore.new(
secret_key: ENV["SECRET_KEY_BASE"],
session_key: "_myapp_session"
)
end
when "test"
Amber::SessionAdapters::MemoryStore.new(
session_key: "_myapp_session"
)
else # development
Amber::SessionAdapters::CookieStore.new(
secret_key: ENV["SECRET_KEY_BASE"]? || "dev_secret_key_at_least_32_chars",
session_key: "_myapp_session"
)
end
end
WebSocket PubSub Migration
Before (Amber 1.x)
# Hard-coded Redis pub/sub
class ChatSocket < Amber::WebSockets::Channel
def on_connect
subscribe("chat_room_#{@room_id}")
end
def on_message(action, message)
# Uses Redis internally
broadcast("chat_room_#{@room_id}", message)
end
end
After (Amber 2.0)
Option 1: Memory Adapter (Single Server)
# config/initializers/pubsub.cr
require "amber/pubsub_adapters/memory_adapter"
PUBSUB = Amber::PubSubAdapters::MemoryAdapter.new
class ChatSocket < Amber::WebSockets::Channel
def on_connect
PUBSUB.subscribe("chat_room_#{@room_id}") do |message|
send_to_client(message)
end
end
def on_message(action, message)
PUBSUB.publish("chat_room_#{@room_id}", message)
end
def on_disconnect
PUBSUB.unsubscribe("chat_room_#{@room_id}")
end
end
Use when:
- Single server deployment
- Development/testing
- Low message volume
Option 2: Redis Adapter (Multi-Server)
# config/initializers/pubsub.cr
require "amber/pubsub_adapters/redis_adapter"
PUBSUB = Amber::PubSubAdapters::RedisAdapter.new(
url: ENV["REDIS_URL"]
)
class ChatSocket < Amber::WebSockets::Channel
def on_connect
PUBSUB.subscribe("chat_room_#{@room_id}") do |message|
send_to_client(message)
end
end
def on_message(action, message)
PUBSUB.publish("chat_room_#{@room_id}", message)
end
end
Use when:
- Multiple app servers
- High message volume
- Need message persistence
Environment-Based PubSub
# config/initializers/pubsub.cr
PUBSUB = case ENV["AMBER_ENV"]?
when "production"
Amber::PubSubAdapters::RedisAdapter.new(
url: ENV["REDIS_URL"]
)
else
Amber::PubSubAdapters::MemoryAdapter.new
end
Migration Steps
Step 1: Update shard.yml
dependencies:
amber:
github: amberframework/amber
version: 2.0.0-beta.2
# Redis now optional
redis:
github: stefanwille/crystal-redis
version: ~> 2.8.0
Step 2: Update Session Configuration
- Remove old Redis session config
- Choose and configure new adapter
- Test session functionality
# Remove this:
app.session = {
:redis => Redis.new(...),
...
}
# Add this:
app.session = Amber::SessionAdapters::CookieStore.new(...)
Step 3: Update WebSocket Code
- Create PubSub adapter instance
- Update channels to use adapter
- Test real-time features
Step 4: Remove Redis (if no longer needed)
# shard.yml - remove if not using Redis adapter
# redis:
# github: stefanwille/crystal-redis
shards update
Custom Adapters
Custom Session Adapter
class MySessionAdapter < Amber::SessionAdapters::Base
def initialize(@connection : MyDatabase)
end
def load(session_id : String) : Hash(String, String)
@connection.get_session(session_id) || {} of String => String
end
def save(session_id : String, data : Hash(String, String)) : Nil
@connection.set_session(session_id, data, ttl: 24.hours)
end
def destroy(session_id : String) : Nil
@connection.delete_session(session_id)
end
def generate_id : String
Random::Secure.hex(32)
end
end
# Use it
Amber::Server.configure do |app|
app.session = MySessionAdapter.new(DB_CONNECTION)
end
Custom PubSub Adapter
class MyPubSubAdapter < Amber::PubSubAdapters::Base
def initialize(@broker : MessageBroker)
end
def subscribe(channel : String, &block : String -> Nil) : Nil
@broker.subscribe(channel, &block)
end
def unsubscribe(channel : String) : Nil
@broker.unsubscribe(channel)
end
def publish(channel : String, message : String) : Nil
@broker.publish(channel, message)
end
end
Session Data Migration
If migrating from Redis sessions to cookies, existing sessions will be lost. Options:
Option 1: Accept Session Loss
For most apps, users simply log in again. Plan migration during low-traffic period.
Option 2: Gradual Migration
Support both adapters temporarily:
class HybridSessionAdapter < Amber::SessionAdapters::Base
def initialize(@redis : RedisStore, @cookie : CookieStore)
end
def load(session_id : String) : Hash(String, String)
# Try cookie first, fall back to Redis
data = @cookie.load(session_id)
return data unless data.empty?
# Migrate from Redis to cookie
redis_data = @redis.load(session_id)
unless redis_data.empty?
@cookie.save(session_id, redis_data)
@redis.destroy(session_id) # Clean up Redis
end
redis_data
end
def save(session_id : String, data : Hash(String, String)) : Nil
@cookie.save(session_id, data)
end
def destroy(session_id : String) : Nil
@cookie.destroy(session_id)
@redis.destroy(session_id)
end
end
Option 3: Coordinate Session Migration
For critical sessions (admin, long-running workflows):
- Export important sessions from Redis
- Notify affected users
- Migrate, requiring re-authentication
Removing Redis Dependency
Once migrated, you can remove Redis entirely:
1. Update shard.yml
# Remove or comment out
# redis:
# github: stefanwille/crystal-redis
2. Remove Redis Config
# Remove Redis-related environment variables
# REDIS_URL, REDIS_HOST, etc.
3. Update Docker/Infrastructure
# Remove from docker-compose.yml
# redis:
# image: redis:alpine
4. Run Tests
crystal spec
# Ensure no Redis references remain
grep -r "Redis" src/
grep -r "REDIS" src/
Troubleshooting
Sessions Not Persisting (Cookie Store)
Check cookie size - cookies have a 4KB limit:
# Log session size
puts "Session size: #{session.to_json.bytesize} bytes"
# Consider storing only essential data
session["user_id"] = user.id.to_s # Good
session["user"] = user.to_json # Bad - too large
WebSocket Messages Not Broadcasting (Multi-Server)
Ensure Redis adapter is used in production:
if ENV["AMBER_ENV"] == "production" && !ENV["REDIS_URL"]?
raise "REDIS_URL required for WebSocket pub/sub in production"
end
Performance Issues
Compare adapter performance:
# Benchmark session operations
require "benchmark"
Benchmark.ips do |x|
x.report("cookie") { cookie_adapter.load("test") }
x.report("redis") { redis_adapter.load("test") }
x.report("memory") { memory_adapter.load("test") }
end
Cookie is typically fastest for small sessions; Redis for large sessions with many servers.
Security Considerations
Cookie Store Security
- Always use
SECRET_KEY_BASEof at least 32 characters - Session data is encrypted but may be visible to determined attackers
- Don't store sensitive data directly in sessions
# Good
session["user_id"] = user.id.to_s
# Bad - sensitive data
session["credit_card"] = card_number
Redis Store Security
- Use
REDIS_URLwith authentication - Enable TLS in production:
rediss://... - Consider Redis ACLs for additional security