kemal-session
Version, currently 1.6.023 versions
- 1.6.0latestJun 2, 2026
- 1.5.0not indexedJun 2, 2026
- 1.4.0not indexedJun 2, 2026
- 1.3.0not indexedJun 2, 2026
- 1.2.0not indexedJun 2, 2026
- 1.1.0not indexedJun 2, 2026
- 1.0.0not indexedJun 2, 2026
- 0.13.0not indexedJun 2, 2026
- 0.12.1not indexedJun 2, 2026
- 0.12.0not indexedJun 2, 2026
- 0.11.1not indexedJun 2, 2026
- 0.11.0not indexedJun 2, 2026
- 0.10.0not indexedJun 2, 2026
- 0.9.0not indexedJun 2, 2026
- 0.8.0not indexedJun 2, 2026
- 0.7.0not indexedJun 2, 2026
- 0.6.0not indexedJun 2, 2026
- 0.5.0not indexedJun 2, 2026
- 0.4.0not indexedJun 2, 2026
- 0.3.0not indexedJun 2, 2026
- 0.2not indexedJun 2, 2026
- 0.1.1not indexedJun 2, 2026
- 0.1not indexedJun 2, 2026
github.com/kemalcr/kemal-session
Powerful Session Management for Kemal applications
65 stars
7 dependents
License: MIT
Installation
# Add this to your shard.yml
dependencies:
kemal-session:
github: kemalcr/kemal-session
version: ~> 1.6.0Then run:
shards installshard.yml
- Crystal
>= 1.0.0- Author
- Serdar Dogruyol <dogruyolserdar@gmail.com>
Dependencies
Development Dependencies
- kemal~> 1.11.0github: kemalcr/kemaldev
README
# kemal-session
[](https://github.com/kemalcr/kemal-session/actions/workflows/ci.yml)
> π **Powerful session management for Kemal web applications**
Add secure, persistent session support to your [Kemal](https://github.com/sdogruyol/kemal) web applications with just a few lines of code! Perfect for user authentication, shopping carts, temporary data storage, and more.
## β¨ Why kemal-session?
- π― **Simple & Intuitive**: Get started in minutes with a clean, easy-to-use API
- π **Secure by Default**: Built-in CSRF protection and signed session cookies
- ποΈ **Fast & Flexible**: Multiple storage engines (Memory, File, Redis, PostgreSQL, etc.)
- π§© **Type-Safe**: Support for all Crystal types plus custom objects
- π‘οΈ **Production Ready**: Automatic session cleanup and security best practices
## π¦ Installation
Add kemal-session to your `shard.yml`:
```yaml
dependencies:
kemal-session:
github: kemalcr/kemal-session
```
Then run:
```bash
shards install
```
## π Quick Start
### 1. Basic Session Usage (User Login / Logout)
```crystal
require "kemal"
require "kemal-session"
# Session Configuration
Kemal::Session.config.secret = "my-secret-key"
# User login (create session)
post "/login" do |env|
username = env.params.body["username"]?.to_s
# In a real app you would authenticate here
env.session.string("username", username)
env.session.bool("logged_in", true)
"Welcome #{username}, you're now logged in."
end
# Protected route using session
get "/profile" do |env|
unless env.session.bool?("logged_in")
env.response.status_code = 401
next "Please log in first"
end
username = env.session.string("username")
"Hello #{username}!"
end
# User logout (destroy session)
post "/logout" do |env|
env.session.destroy
"You have been logged out."
end
Kemal.run
```
### 2. Real-World Example: Shopping Cart
```crystal
require "kemal"
require "kemal-session"
# Session Configuration
Kemal::Session.config.secret = "my-secret-key"
# Add item to cart
post "/cart/add" do |env|
product_id = env.params.body["product_id"].as(String)
# Get existing cart or create new one
cart = env.session.object?("cart") || [] of String
cart << product_id
env.session.object("cart", cart)
"Item added to cart! Total items: #{cart.size}"
end
# View cart
get "/cart" do |env|
cart = env.session.object?("cart") || [] of String
if cart.empty?
"Your cart is empty"
else
"Your cart: #{cart.join(", ")} (#{cart.size} items)"
end
end
Kemal.run
```
## π‘οΈ CSRF Protection
Protect your application from Cross-Site Request Forgery attacks with built-in CSRF middleware.
### Basic CSRF Setup
```crystal
require "kemal"
require "kemal-session"
# Session Configuration
Kemal::Session.config.secret = "my-secret-key"
# Add CSRF protection
add_handler Kemal::Session::CSRF.new
get "/form" do |env|
csrf_token = env.session.string("csrf")
<<-HTML
<form method="POST" action="/submit">
<input type="hidden" name="authenticity_token" value="#{csrf_token}">
<input type="text" name="message" placeholder="Enter message">
<button type="submit">Submit</button>
</form>
HTML
end
post "/submit" do |env|
message = env.params.body["message"]
"Message received: #{message}"
end
Kemal.run
```
### Advanced CSRF Configuration
```crystal
# Customize CSRF behavior
add_handler Kemal::Session::CSRF.new(
header: "X-CSRF-TOKEN", # Custom header for AJAX requests
allowed_methods: ["GET", "HEAD", "OPTIONS"], # Methods that skip CSRF check
allowed_routes: ["/api/public"], # Public routes that skip CSRF
parameter_name: "_token", # Custom form field name
error: "Invalid or missing CSRF token", # Custom error message
per_session: false # see below
)
```
#### CSRF Token per request or per session?
If `per_session` is `false` (the default), the token changes after one single submission; this is
ideal for "classic" web pages that are fully loaded for each request.
If `per_session` is `true` the token is not automatically updated during a session's lifetime;
manual changes via `env.session.string("csrf","new_token")` are still possible.
This is useful for partial page updates with AJAX-based approaches like [HTMX](https://htmx.org),
as you can change the CSRF token with each full page load while keeping it the same for partial operations.
### CSRF for API Endpoints
```crystal
# Custom error handler for JSON APIs
csrf_handler = Kemal::Session::CSRF.new(
error: ->(env : HTTP::Server::Context) {
env.response.content_type = "application/json"
env.response.status_code = 403
{"error" => "CSRF token required"}.to_json
}
)
add_handler csrf_handler
```
## π¬ Flash Messages
Flash messages are one-time messages that persist across a single redirectβideal for success notices, errors, or warnings after form submissions.
- **Set** a flash message in one request (e.g. after an action).
- **Read** it in the next request (e.g. the redirected page); the value is then removed automatically.
### Basic Usage
```crystal
require "kemal"
require "kemal-session"
Kemal::Session.config.secret = "my-secret-key"
# Set a flash message (e.g. after login or form submit)
post "/login" do |env|
# ... authenticate user ...
env.flash["notice"] = "Welcome back!"
env.redirect "/dashboard"
end
# Read the flash in the next request (e.g. layout or target page)
get "/dashboard" do |env|
notice = env.flash["notice"]? # returns value and clears it
error = env.flash["error"]? # optional: nil if key missing
message = notice ? "<p class=\"notice\">#{notice}</p>" : ""
message += error ? "<p class=\"error\">#{error}</p>" : ""
"Dashboard\n#{message}"
end
```
### API
| Operation | Method | Behavior |
|-----------|--------|----------|
| Set | `env.flash["key"] = "value"` | Stores the message for the next request |
| Read (optional) | `env.flash["key"]?` | Returns the value and removes it; returns `nil` if key is missing |
| Read (required) | `env.flash["key"]` | Returns the value and removes it; raises `KeyError` if key is missing |
Use `env.flash["key"]?` when the key might not be set (e.g. optional notices). Use `env.flash["key"]` when you expect the key to exist; it will raise if the message was already consumed or never set.
### Example: Form with Success/Error
```crystal
post "/contact" do |env|
if send_email(env.params.body)
env.flash["notice"] = "Message sent successfully."
else
env.flash["error"] = "Failed to send message."
end
env.redirect "/contact"
end
get "/contact" do |env|
notice = env.flash["notice"]?
error = env.flash["error"]?
# Render form and show notice/error above it
end
```
## π Supported Data Types
Kemal Session supports all common Crystal types with intuitive method names:
| Crystal Type | Session Method | Example |
|--------------|----------------|---------|
| `Int32` | `session.int` | `env.session.int("count", 42)` |
| `Int64` | `session.bigint` | `env.session.bigint("timestamp", 1234567890_i64)` |
| `String` | `session.string` | `env.session.string("name", "Alice")` |
| `Float64` | `session.float` | `env.session.float("price", 19.99)` |
| `Bool` | `session.bool` | `env.session.bool("logged_in", true)` |
| Custom Objects | `session.object` | `env.session.object("user", user_obj)` |
### π Reading Values
```crystal
# Get values (raises if not found)
count = env.session.int("count")
name = env.session.string("username")
# Get optional values (returns nil if not found)
count = env.session.int?("count") # returns Int32 or nil
name = env.session.string?("username") # returns String or nil
# Provide default values
count = env.session.int?("count") || 0
theme = env.session.string?("theme") || "light"
```
### ποΈ Working with Collections
Access the underlying hash for advanced operations (read-only):
```crystal
# Iterate through all integer values
env.session.ints.each do |key, value|
puts "#{key}: #{value}"
end
# Check what string keys exist
if env.session.strings.has_key?("username")
puts "User is logged in"
end
# Get all session data
puts "Total sessions: #{env.session.strings.size}"
```
β οΈ **Important**: Only use hash access for reading. Never modify values directly through these hashes, as changes won't be persisted!
## π― Custom Objects (StorableObject)
Store complex objects in sessions by implementing the `StorableObject` module. Perfect for user profiles, preferences, or any custom data structures.
### Creating a Storable Object
```crystal
# Define your class with JSON serialization
class User
include JSON::Serializable
include Kemal::Session::StorableObject # Add this after JSON::Serializable
property id : Int32
property name : String
property email : String
property preferences : Hash(String, String)
def initialize(@id : Int32, @name : String, @email : String)
@preferences = {} of String => String
end
end
```
### Using Storable Objects
```crystal
require "kemal"
require "kemal-session"
# Session Configuration
Kemal::Session.config.secret = "my-secret-key"
# Store user in session
post "/login" do |env|
user = User.new(123, "Alice", "alice@example.com")
user.preferences["theme"] = "dark"
user.preferences["language"] = "en"
env.session.object("current_user", user)
"Login successful!"
end
# Retrieve user from session
get "/profile" do |env|
user = env.session.object("current_user").as(User)
<<-HTML
<h1>Welcome, #{user.name}!</h1>
<p>Email: #{user.email}</p>
<p>Theme: #{user.preferences["theme"]?}</p>
HTML
end
# Update user preferences
post "/preferences" do |env|
user = env.session.object("current_user").as(User)
user.preferences["theme"] = env.params.body["theme"].as(String)
# Save updated user back to session
env.session.object("current_user", user)
"Preferences updated!"
end
```
### Complex Example: Shopping Cart with Items
```crystal
class CartItem
include JSON::Serializable
include Kemal::Session::StorableObject
property id : String
property name : String
property price : Float64
property quantity : Int32
def initialize(@id : String, @name : String, @price : Float64, @quantity : Int32 = 1)
end
def total
price * quantity
end
end
class ShoppingCart
include JSON::Serializable
include Kemal::Session::StorableObject
property items : Array(CartItem)
def initialize
@items = [] of CartItem
end
def add_item(item : CartItem)
existing = items.find { |i| i.id == item.id }
if existing
existing.quantity += item.quantity
else
items << item
end
end
def total
items.sum(&.total)
end
def item_count
items.sum(&.quantity)
end
end
# Usage in routes
post "/cart/add" do |env|
cart = env.session.object?("cart").try(&.as(ShoppingCart)) || ShoppingCart.new
item = CartItem.new(
id: env.params.body["id"].as(String),
name: env.params.body["name"].as(String),
price: env.params.body["price"].to_f
)
cart.add_item(item)
env.session.object("cart", cart)
"Added to cart! Total: $#{cart.total} (#{cart.item_count} items)"
end
```
## βοΈ Configuration
Customize session behavior to fit your application's needs:
### Quick Configuration
```crystal
Kemal::Session.config do |config|
config.cookie_name = "my_app_session" # Custom cookie name
config.secret = "your-super-secret-key" # π Always set this in production!
config.timeout = 2.hours # Session expires after 2 hours
config.gc_interval = 5.minutes # Clean expired sessions every 5 minutes
config.secure = true # Only send over HTTPS
config.domain = "example.com" # Scope to specific domain
end
```
### One-line Configuration
```crystal
Kemal::Session.config.cookie_name = "session_id"
Kemal::Session.config.secret = "my-secret-key"
Kemal::Session.config.timeout = 30.minutes
```
### π Configuration Options
| Option | Description | Default | Example |
|--------|-------------|---------|---------|
| `timeout` | Session expires after this time since last activity | `1.hour` | `2.hours`, `30.minutes` |
| `cookie_name` | Name of the session cookie | `"kemal_sessid"` | `"my_app_session"` |
| `engine` | Storage backend for sessions | `MemoryEngine` | `FileEngine`, `RedisEngine` |
| `gc_interval` | How often to clean expired sessions | `4.minutes` | `10.minutes`, `1.hour` |
| `secret` | Secret key for signing session cookies | `""` β οΈ | Generated secure string |
| `secure` | Send cookie only over HTTPS | `false` | `true` for production |
| `domain` | Scope cookie to specific domain | `nil` | `"example.com"` |
| `path` | Scope cookie to specific path | `"/"` | `"/app"` |
| `samesite` | SameSite cookie policy | `HTTP::Cookie::SameSite::Lax` | `HTTP::Cookie::SameSite::Strict` |
### π Security Best Practices
#### 1. Generate a Secure Secret
```bash
# Generate a random secret key
crystal eval 'require "random/secure"; puts Random::Secure.hex(64)'
```
```crystal
# Use environment variables in production
Kemal::Session.config.secret = ENV["SESSION_SECRET"]? || "fallback-for-development"
```
#### 2. Production Security Settings
```crystal
Kemal::Session.config do |config|
config.secret = ENV["SESSION_SECRET"] # From environment
config.secure = true # HTTPS only
config.samesite = HTTP::Cookie::SameSite::Strict # CSRF protection
config.domain = "yourdomain.com" # Scope to your domain
config.timeout = 1.hour # Reasonable timeout
end
```
#### 3. Cookie Security
```crystal
Kemal::Session.config do |config|
config.samesite = HTTP::Cookie::SameSite::Strict # Prevents CSRF attacks
config.secure = true # HTTPS only
config.domain = "example.com" # Limit to your domain
end
```
## ποΈ Storage Engines
Choose the right storage engine for your application's needs:
### Memory Engine (Default)
Perfect for development and single-server applications:
```crystal
# Already the default, but you can configure it explicitly
Kemal::Session.config.engine = Kemal::Session::MemoryEngine.new
```
**Pros**: Fast, no setup required
**Cons**: Sessions lost on server restart, not suitable for multiple servers
### File Engine
Store sessions on disk for persistence across restarts:
```crystal
Kemal::Session.config.engine = Kemal::Session::FileEngine.new({
:sessions_dir => "/var/lib/my_app/sessions/"
})
```
**Pros**: Persists across restarts, simple setup
**Cons**: File I/O overhead, not suitable for multiple servers
### Production-Ready Engines
For production applications, consider these external engines:
| Engine | Use Case | shard.yml |
|--------|----------|-------|
| **[Redis](https://github.com/crystal-garage/kemal-session-redis-engine)** | High performance, multiple servers | `crystal-garage/kemal-session-redis-engine` |
| **[PostgreSQL](https://github.com/mang/kemal-session-postgres)** | Existing PostgreSQL infrastructure | `mang/kemal-session-postgres` |
| **[MySQL](https://github.com/crisward/kemal-session-mysql)** | Existing MySQL infrastructure | `crisward/kemal-session-mysql` |
| **[RethinkDB](https://github.com/kingsleyh/kemal-session-rethinkdb)** | Real-time applications | `kingsleyh/kemal-session-rethinkdb` |
### Redis Engine Example
```yaml
# shard.yml
dependencies:
kemal-session:
github: kemalcr/kemal-session
kemal-session-redis-engine:
github: crystal-garage/kemal-session-redis-engin
```
```crystal
require "kemal"
require "kemal-session"
require "kemal-session-redis-engine"
Kemal::Session.config do |config|
config.cookie_name = "redis_test"
config.secret = "a_secret"
config.engine = Kemal::Session::RedisEngine.new(
"redis://localhost:6379/0?initial_pool_size=1&max_pool_size=10&checkout_timeout=10&retry_attempts=2&retry_delay=0.5&max_idle_pool_size=50",
key_prefix: "my_app:session:"
)
config.timeout = Time::Span.new(1, 0, 0)
end
get "/" do
puts "Hello World"
end
post "/sign_in" do |context|
context.session.int("see-it-works", 1)
end
Kemal.run
```
### Custom Engine
Create your own storage engine by implementing the required interface. Check the [wiki](https://github.com/kemalcr/kemal-session/wiki/Creating-your-session-engine) for detailed instructions.
## πͺ Session Management
### πͺ User Logout
```crystal
get "/logout" do |env|
env.session.destroy
redirect "/login"
end
```
### π¨βπΌ Administrative Session Management
For building admin interfaces, you can manage other users' sessions:
```crystal
# Get specific session by ID
admin_session = Kemal::Session.get("session_id_here")
# Iterate through all active sessions
Kemal::Session.each do |session|
puts "Session: #{session.id}, Last Activity: #{session.last_access_time}"
end
# Get all sessions as an array
all_sessions = Kemal::Session.all
puts "Total active sessions: #{all_sessions.size}"
# Force logout a specific user
Kemal::Session.destroy("problematic_session_id")
# Emergency: Log out all users
Kemal::Session.destroy_all
```
β οΈ **Security Warning**: Administrative session functions access ALL user sessions. Use with extreme caution and proper authorization checks:
```crystal
get "/admin/sessions" do |env|
# Always verify admin permissions first!
admin_user = env.session.object?("current_user").try(&.as(User))
halt env, status_code: 403, response: "Forbidden" unless admin_user.try(&.admin?)
sessions = Kemal::Session.all
# ... render admin interface
end
```
### ποΈ Memory Considerations
- `Kemal::Session.all` and `Kemal::Session.each` load all sessions into memory
- For high-traffic applications, consider pagination or streaming approaches
- The memory impact depends on your storage engine implementation
## π Production Examples
### Complete Authentication System
```crystal
require "kemal"
require "kemal-session"
# Configure session for production
Kemal::Session.config do |config|
config.secret = ENV["SESSION_SECRET"]
config.secure = true if ENV["KEMAL_ENV"]? == "production"
config.timeout = 2.hours
config.samesite = HTTP::Cookie::SameSite::Strict
end
# Add CSRF protection
add_handler Kemal::Session::CSRF.new
# User model
class User
include JSON::Serializable
include Kemal::Session::StorableObject
property id : Int32
property username : String
property email : String
property admin : Bool
def initialize(@id : Int32, @username : String, @email : String, @admin : Bool = false)
end
end
# Login route
post "/login" do |env|
username = env.params.body["username"].as(String)
password = env.params.body["password"].as(String)
# Authenticate user (implement your logic)
if user = authenticate_user(username, password)
env.session.object("current_user", user)
env.session.string("login_time", Time.utc.to_s)
redirect "/dashboard"
else
env.session.string("error", "Invalid credentials")
redirect "/login"
end
end
# Protected route
get "/dashboard" do |env|
user = env.session.object?("current_user").try(&.as(User))
halt env, status_code: 401, response: "Please log in" unless user
"Welcome #{user.username}! You logged in at #{env.session.string?("login_time")}"
end
# Admin-only route
get "/admin" do |env|
user = env.session.object?("current_user").try(&.as(User))
halt env, status_code: 401, response: "Please log in" unless user
halt env, status_code: 403, response: "Admin required" unless user.admin
"Admin panel - manage users here"
end
Kemal.run
```
### API with Session-based Auth
```crystal
# API endpoints with session authentication
get "/api/profile" do |env|
env.response.content_type = "application/json"
user = env.session.object?("current_user").try(&.as(User))
if user
user.to_json
else
env.response.status_code = 401
{"error" => "Authentication required"}.to_json
end
end
```
## π Helpful Resources
- π [Crystal Language Documentation](https://crystal-lang.org/docs/)
- π [Kemal Framework](https://kemalcr.com/)
- π§ [Creating Custom Engines](https://github.com/kemalcr/kemal-session/wiki/Creating-your-session-engine)
- π‘ [Crystal Security Best Practices](https://crystal-lang.org/reference/guides/security.html)
## π€ Contributing
We love contributions! Here's how you can help:
1. π΄ Fork the repository
2. π Create a feature branch (`git checkout -b my-new-feature`)
3. βοΈ Make your changes and add tests
4. β
Ensure all tests pass (`crystal spec`)
5. β
Ensure consistent code formatting (`crystal tool format`)
6. π Commit your changes (`git commit -am 'Add some feature'`)
7. π Push to the branch (`git push origin my-new-feature`)
8. π― Create a Pull Request
## π Acknowledgments
Special thanks to:
- [Thyra](https://github.com/Thyra) for the initial implementation
- The Crystal and Kemal communities for their support
Documentation
Built from the current release. The first visit to a release nobody has asked for starts its build.
Links
This release
- Version
1.6.0- Tagged
- Jun 2, 2026
- Commit
bd032f965a87- Crystal
>= 1.0.0- Indexed
- yes
Dependents
Repository
github.com/kemalcr/kemal-session
Metadata
- Created
- Aug 12, 2026
- Updated
- Aug 12, 2026
- Synced
- Aug 12, 2026
- Versions
- 23