A production-ready, type-safe session management library for Crystal applications.
Session provides enterprise-grade session handling with multiple storage backends, built-in security features, and resilience patterns. Whether you're building a simple web application or a distributed microservices architecture, Session offers the flexibility and reliability your application demands.
- Type Safety — Define your session data as Crystal structs with compile-time guarantees
- Multiple Backends — Choose from Cookie, Memory, or Redis storage based on your needs
- Security First — AES-256 encryption, HMAC-SHA256 signatures, and configurable key derivation
- Production Ready — Circuit breakers, retry logic, and graceful degradation built-in
- Developer Experience — Clean API, comprehensive documentation, and 300+ test cases
- Installation
- Quick Start
- Configuration
- Storage Backends
- Core Features
- Security Features
- Resilience & Performance
- Observability
- HTTP Handler Integration
- API Reference
- Contributing
Add the dependency to your shard.yml:
dependencies:
session:
github: azutoolkit/sessionThen run:
shards installrequire "session"
# 1. Define your session data structure
struct UserSession
include Session::SessionData
property user_id : Int64?
property username : String?
property role : String = "guest"
end
# 2. Configure the session
Session.configure do |config|
config.secret = ENV["SESSION_SECRET"] # Use a secure 32+ character secret
config.timeout = 24.hours
config.session_key = "myapp_session"
config.provider = Session::MemoryStore(UserSession).provider
end
# 3. Use sessions in your application
provider = Session.provider
session = provider.create
session.data.user_id = 12345
session.data.username = "alice"
session.data.role = "admin"
puts session.data.username # => "alice"Session provides extensive configuration options to tailor behavior to your application's needs:
Session.configure do |config|
# Core settings
config.secret = ENV["SESSION_SECRET"] # Required: encryption key (32+ chars recommended)
config.timeout = 1.hour # Session lifetime
config.session_key = "_session" # Cookie name
# Security settings
config.use_kdf = true # Enable PBKDF2 key derivation
config.kdf_iterations = 100_000 # PBKDF2 iteration count
config.digest_algorithm = :sha256 # HMAC algorithm (:sha256 or :sha1)
config.require_secure_secret = true # Enforce strong secrets in production
# Session binding (anti-hijacking)
config.bind_to_ip = true # Bind sessions to client IP
config.bind_to_user_agent = true # Bind sessions to User-Agent
# Performance settings
config.compress_data = true # Enable gzip compression
config.compression_threshold = 256 # Minimum bytes before compressing
config.sliding_expiration = true # Extend session on each request
# Redis settings
config.encrypt_redis_data = true # Encrypt data stored in Redis
# Resilience settings
config.enable_retry = true # Enable automatic retry on failures
config.circuit_breaker_enabled = true # Enable circuit breaker pattern
# Lifecycle callbacks
config.on_started = ->(sid : String, data : Session::SessionData) {
Log.info { "Session created: #{sid}" }
}
config.on_deleted = ->(sid : String, data : Session::SessionData) {
Log.info { "Session destroyed: #{sid}" }
}
endSession offers three storage backends, each optimized for different use cases.
Best for: Simple applications, stateless deployments, serverless environments
The Cookie Store encrypts and signs session data, storing it entirely in the client's browser. Zero server-side storage required.
Session.configure do |config|
config.secret = ENV["SESSION_SECRET"]
config.provider = Session::CookieStore(UserSession).provider
endCharacteristics:
- No server-side storage required
- Automatic encryption (AES-256-CBC) and signing (HMAC-SHA256)
- 4KB size limit (with automatic validation)
- Optional gzip compression for larger payloads
Best for: Development, testing, single-server deployments
The Memory Store keeps sessions in server memory for fast access.
Session.configure do |config|
config.provider = Session::MemoryStore(UserSession).provider
endCharacteristics:
- Fastest possible access times
- Sessions lost on server restart
- Not suitable for multi-server deployments
- Built-in expired session cleanup
# Manual cleanup of expired sessions
memory_store = Session::MemoryStore(UserSession).new
expired_count = memory_store.cleanup_expired
# Get memory statistics
stats = memory_store.memory_stats
puts "Total: #{stats[:total_sessions]}, Valid: #{stats[:valid_sessions]}"Best for: Production deployments, distributed systems, high availability
The Redis Store provides persistent, distributed session storage with enterprise features.
Session.configure do |config|
config.provider = Session::RedisStore(UserSession).provider(
client: Redis.new(host: "localhost", port: 6379)
)
config.encrypt_redis_data = true # Optional: encrypt data at rest
endCharacteristics:
- Persistent across server restarts
- Shared across multiple application instances
- Automatic TTL-based expiration
- Uses SCAN instead of KEYS for production safety
- Optional data encryption at rest
- Circuit breaker and retry logic for resilience
Define your session structure as a Crystal struct for compile-time type safety:
struct UserSession
include Session::SessionData
property user_id : Int64?
property username : String?
property email : String?
property roles : Array(String) = [] of String
property preferences : Hash(String, String) = {} of String => String
property login_count : Int32 = 0
property last_seen : Time?
def admin? : Bool
roles.includes?("admin")
end
def authenticated? : Bool
!user_id.nil?
end
endFlash messages persist for exactly one request—perfect for success notifications, error messages, and redirects:
provider = Session.provider
# Set flash messages (available on next request)
provider.flash["notice"] = "Your changes have been saved."
provider.flash["error"] = "Please correct the errors below."
# In the next request, access flash messages
provider.flash.now["notice"] # => "Your changes have been saved."
provider.flash.now["error"] # => "Please correct the errors below."
# After accessing, they're automatically clearedRegenerate session IDs after authentication state changes to prevent session fixation attacks:
provider = Session.provider
# After successful login
def login(user : User)
provider.regenerate_id # New session ID, same data
provider.data.user_id = user.id
provider.data.username = user.username
end
# After logout
def logout
provider.delete # Destroys the entire session
endAutomatically extend session lifetime on each request to keep active users logged in:
Session.configure do |config|
config.timeout = 30.minutes
config.sliding_expiration = true # Reset timeout on each request
end
# Check remaining session time
session = provider.current_session
remaining = session.time_until_expiry
puts "Session expires in #{remaining.total_minutes.to_i} minutes"All session data is protected with industry-standard cryptography:
- Encryption: AES-256-CBC with random IV per encryption
- Signing: HMAC-SHA256 (with SHA1 fallback for migration)
- Encoding: URL-safe Base64
Session.configure do |config|
config.secret = ENV["SESSION_SECRET"] # 32+ characters recommended
config.digest_algorithm = :sha256 # Use SHA256 for HMAC
config.digest_fallback = true # Allow SHA1 during migration
endEnable PBKDF2 key derivation for enhanced security against brute-force attacks:
Session.configure do |config|
config.use_kdf = true # Enable key derivation
config.kdf_iterations = 100_000 # OWASP recommended minimum
endBind sessions to client characteristics to detect and prevent session hijacking:
Session.configure do |config|
config.bind_to_ip = true # Validate client IP matches
config.bind_to_user_agent = true # Validate User-Agent matches
endWhen a mismatch is detected, the session is automatically invalidated and a new one is created.
The Cookie Store automatically validates cookie size to prevent silent failures:
# Automatically raises CookieSizeExceededException if data exceeds 4KB
begin
store[session.session_id] = large_session
rescue Session::CookieSizeExceededException => e
Log.error { "Session too large: #{e.actual_size} bytes (max: #{e.max_size})" }
endProtect your application from cascading failures with the circuit breaker pattern:
Session.configure do |config|
config.circuit_breaker_enabled = true
config.circuit_breaker_config = Session::CircuitBreakerConfig.new(
failure_threshold: 5, # Open circuit after 5 failures
reset_timeout: 30.seconds, # Try again after 30 seconds
half_open_max_calls: 1 # Allow 1 test request in half-open state
)
endStates:
- Closed: Normal operation, requests flow through
- Open: Failures exceeded threshold, requests fail fast
- Half-Open: Testing recovery, limited requests allowed
Automatic retry with exponential backoff for transient failures:
Session.configure do |config|
config.enable_retry = true
config.retry_config = Session::RetryConfig.new(
max_attempts: 3, # Maximum retry attempts
base_delay: 100.milliseconds, # Initial delay
max_delay: 5.seconds, # Maximum delay cap
backoff_multiplier: 2.0, # Exponential backoff factor
jitter: 0.1 # Random jitter (±10%)
)
endReduce storage size and network transfer with automatic gzip compression:
Session.configure do |config|
config.compress_data = true
config.compression_threshold = 256 # Only compress data larger than 256 bytes
end
# Check compression effectiveness
original = session_data.to_json
compressed = Session::Compression.compress(original)
ratio = Session::Compression.compression_ratio(original, compressed)
puts "Compression ratio: #{(ratio * 100).round(1)}%"For high-traffic applications, use connection pooling with Redis:
pool = Session::ConnectionPool.new(
size: 10,
timeout: 5.seconds
) { Redis.new(host: "localhost") }
store = Session::PooledRedisStore(UserSession).new(pool)Plug in your own metrics backend for monitoring session operations:
# Use the built-in log backend
Session.configure do |config|
config.metrics_backend = Session::Metrics::LogBackend.new
end
# Or implement your own
class PrometheusBackend < Session::Metrics::Backend
def increment(metric : String, tags : Hash(String, String) = {} of String => String)
# Push to Prometheus
end
def timing(metric : String, duration : Time::Span, tags : Hash(String, String) = {} of String => String)
# Record timing histogram
end
def gauge(metric : String, value : Float64, tags : Hash(String, String) = {} of String => String)
# Set gauge value
end
end
Session.configure do |config|
config.metrics_backend = PrometheusBackend.new
endQuery and manage sessions programmatically:
store = Session::MemoryStore(UserSession).new
# Iterate all valid sessions
store.each_session do |session|
puts "User: #{session.data.username}"
end
# Find sessions matching criteria
admin_sessions = store.find_by { |s| s.data.roles.includes?("admin") }
# Bulk delete sessions
deleted = store.bulk_delete { |s| s.data.user_id == compromised_user_id }
puts "Revoked #{deleted} sessions"
# Get all session IDs
all_ids = store.all_session_idsIntegrate Session into your HTTP pipeline with the built-in handler:
require "http/server"
# Configure session
Session.configure do |config|
config.secret = ENV["SESSION_SECRET"]
config.provider = Session::MemoryStore(UserSession).provider
end
# Create the handler chain
session_handler = Session::SessionHandler.new(Session.provider)
server = HTTP::Server.new([
session_handler,
YourApplicationHandler.new,
])
server.listen(8080)The handler automatically:
- Loads session from request cookies
- Validates session binding (if enabled)
- Handles corrupted/expired sessions gracefully
- Sets updated session cookie on response
- Extends session lifetime (if sliding expiration enabled)
provider = Session.provider
provider.create # Create a new session
provider.delete # Destroy the current session
provider.regenerate_id # Generate new session ID (keeps data)
provider.valid? # Check if session is valid
provider.session_id # Get current session ID
provider.current_session # Get current SessionId object
provider.data # Access session data (type-safe)
provider.flash # Access flash messages
provider.load_from(cookies) # Load session from HTTP cookies
provider.set_cookies(cookies) # Set session cookie in response
provider.create_session_cookie # Generate a session cookie
provider[session_id] # Get session by ID (raises if not found)
provider[session_id]? # Get session by ID (returns nil if not found)
provider.clear # Remove all sessions from storesession = provider.current_session
session.session_id # Unique session identifier
session.data # Your SessionData struct
session.created_at # When the session was created
session.expires_at # When the session expires
session.valid? # Check if not expired
session.expired? # Check if expired
session.time_until_expiry # Time remaining until expiration
session.touch # Extend expiration (sliding expiration)- PostgreSQL session store
- MySQL session store
- MongoDB session store
- Session clustering and synchronization
- Cookie-based storage with encryption
- Redis storage with SCAN optimization
- Circuit breaker pattern
- Flash messages
- Session regeneration
- Client binding (IP/User-Agent)
- Data compression
- Metrics integration
We welcome contributions! Here's how to get started:
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Write tests for your changes
- Ensure all tests pass (
crystal spec) - Commit your changes (
git commit -am 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Please ensure your code follows the existing style and includes appropriate test coverage.
This project is licensed under the MIT License - see the LICENSE file for details.
- Elias J. Perez - Creator and maintainer
Give a ⭐️ if Session helps your project!