Nothing has been indexed for 1.0.27 yet. The tag is recorded, its shard.yml has not been read, so the manifest and dependency list below are empty because they are unknown rather than because they are absent.

Installation

# Add this to your shard.yml
dependencies:
  session:
    github: azutoolkit/session
    version: ~> 1.0.27

Then run:

shards install

shard.yml

No shard.yml has been indexed for 1.0.27. You can read it on the repository.

Dependencies

Unknown: the shard.yml for this version has not been read yet.

README

This README is the one indexed from the repository at its latest ref, not from the tag for this version.

Session

Type-safe, production-ready session management for Crystal.

Crystal CI Codacy Badge License: MIT


Why Session?

Type-SafeDefine sessions as Crystal classes with compile-time guarantees
Multiple BackendsCookie, Memory, Redis, or Clustered Redis—pick what fits
Battle-Tested SecurityAES-256 encryption, HMAC-SHA256, PBKDF2, client binding
Production ResilienceCircuit breakers, retry logic, graceful degradation
346 TestsComprehensive coverage you can rely on

Quick Start

Install (add to shard.yml):

dependencies:
  session:
    github: azutoolkit/session

Use (< 30 seconds to your first session):

require "session"

# Define your session data
class UserSession < Session::Base
  property? authenticated : Bool = false
  property username : String? = nil
end

# Configure once
Session.configure do |config|
  config.secret = ENV["SESSION_SECRET"]
  config.store = Session::MemoryStore(UserSession).new
end

# Create and use sessions
store = Session.config.store.not_nil!
session = store.create
session.username = "alice"

That's it. You're ready to build.


Key Features

Storage Backends

BackendBest ForPersistenceMulti-Node
CookieStoreStateless apps, serverlessClient-sideYes
MemoryStoreDevelopment, single-serverNoneNo
RedisStoreProduction deploymentsRedisYes
ClusteredRedisStoreHigh-scale, multi-nodeRedis + local cacheYes
# Cookie (stateless, client-side)
config.store = Session::CookieStore(UserSession).new

# Memory (development)
config.store = Session::MemoryStore(UserSession).new

# Redis (production)
config.store = Session::RedisStore(UserSession).new(client: Redis.new)

# Clustered Redis (high-scale production)
config.cluster.enabled = true
config.store = Session::ClusteredRedisStore(UserSession).new(client: Redis.new)

Security

  • Encryption — AES-256-CBC with random IV per operation
  • Signing — HMAC-SHA256 to detect tampering
  • Key Derivation — Optional PBKDF2 with configurable iterations
  • Client Binding — Lock sessions to IP and/or User-Agent
  • Size Protection — Automatic cookie size validation (4KB limit)
Session.configure do |config|
  config.secret = ENV["SESSION_SECRET"]       # 32+ chars recommended
  config.use_kdf = true                       # Enable PBKDF2
  config.kdf_iterations = 100_000             # OWASP recommended
  config.bind_to_ip = true                    # Prevent session hijacking
  config.bind_to_user_agent = true
end

Resilience

  • Circuit Breaker — Fail fast when backends are down
  • Retry Logic — Exponential backoff with jitter
  • Compression — Gzip for large session payloads
Session.configure do |config|
  config.circuit_breaker_enabled = true
  config.circuit_breaker_config = Session::CircuitBreakerConfig.new(
    failure_threshold: 5,
    reset_timeout: 30.seconds
  )

  config.enable_retry = true
  config.retry_config = Session::RetryConfig.new(
    max_attempts: 3,
    base_delay: 100.milliseconds,
    backoff_multiplier: 2.0
  )
end

Clustering

Multi-node session management with Redis Pub/Sub invalidation and local caching.

flowchart LR
    subgraph Node_A["Node A"]
        A_Cache["Local Cache"]
    end
    subgraph Node_B["Node B"]
        B_Cache["Local Cache"]
    end
    Redis[("Redis")]
    PubSub{{"Pub/Sub"}}

    A_Cache <--> Redis
    B_Cache <--> Redis
    A_Cache -.->|invalidate| PubSub -.->|evict| B_Cache
Session.configure do |config|
  config.cluster.enabled = true
  config.cluster.node_id = ENV["POD_NAME"]? || UUID.random.to_s
  config.cluster.local_cache_ttl = 30.seconds
  config.cluster.local_cache_max_size = 10_000

  config.store = Session::ClusteredRedisStore(UserSession).new(
    client: Redis.new(url: ENV["REDIS_URL"])
  )
end

API Essentials

Store Operations

store = Session.config.store.not_nil!

store.create              # New session
store.delete              # Destroy session
store.regenerate_id       # New ID, keep data (post-login security)
store.valid?              # Check validity
store.current_session     # Access your typed session data
store.flash               # One-request flash messages

Session Object

session = store.current_session

session.session_id           # Unique ID
session.username             # Your session properties directly
session.valid?               # Not expired?
session.expired?             # Past expiration?
session.time_until_expiry    # Remaining lifetime
session.touch                # Extend expiration

Flash Messages

# Set (available next request)
store.flash["notice"] = "Saved successfully"
store.flash["error"] = "Something went wrong"

# Read (clears after access)
store.flash.now["notice"]  # => "Saved successfully"

Query & Bulk Operations

store = Session::MemoryStore(UserSession).new

# Iterate sessions
store.each_session { |s| puts s.username }

# Find by criteria
admins = store.find_by { |s| s.roles.includes?("admin") }

# Bulk delete (e.g., revoke compromised user)
store.bulk_delete { |s| s.user_id == compromised_id }

HTTP Integration

require "http/server"

Session.configure do |config|
  config.secret = ENV["SESSION_SECRET"]
  config.store = Session::MemoryStore(UserSession).new
end

store = Session.config.store.not_nil!

server = HTTP::Server.new([
  Session::SessionHandler.new(store),
  YourAppHandler.new,
])

server.listen(8080)

The handler automatically loads sessions from cookies, validates bindings, handles corruption gracefully, and sets response cookies.


Configuration Reference

Session.configure do |config|
  # Core
  config.secret = ENV["SESSION_SECRET"]   # Required
  config.timeout = 1.hour                 # Session lifetime
  config.session_key = "_session"         # Cookie name
  config.sliding_expiration = true        # Extend on access

  # Security
  config.use_kdf = true                   # PBKDF2 key derivation
  config.kdf_iterations = 100_000
  config.bind_to_ip = true
  config.bind_to_user_agent = true
  config.encrypt_redis_data = true

  # Performance
  config.compress_data = true
  config.compression_threshold = 256

  # Resilience
  config.enable_retry = true
  config.circuit_breaker_enabled = true

  # Clustering
  config.cluster.enabled = true
  config.cluster.local_cache_ttl = 30.seconds
  config.cluster.local_cache_max_size = 10_000

  # Callbacks
  config.on_started = ->(id : String, session : Session::Base) { Log.info { "Session #{id} created" } }
  config.on_deleted = ->(id : String, session : Session::Base) { Log.info { "Session #{id} destroyed" } }

  # Metrics
  config.metrics_backend = Session::Metrics::LogBackend.new
end

Documentation

Full documentation available at GitBook (or see the docs/ directory).


Contributing

  1. Fork it
  2. Create your branch (git checkout -b feature/awesome)
  3. Write tests
  4. Make sure crystal spec passes
  5. Commit and push
  6. Open a PR

License

MIT — see LICENSE


Built with Crystal. Maintained by @eliasjpr.