kemal-session
Version, currently 0.4.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
13 dependents
License: MIT
Nothing has been indexed for 0.4.0 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:
kemal-session:
github: kemalcr/kemal-session
version: ~> 0.4.0Then run:
shards installshard.yml
No shard.yml has been indexed for 0.4.0. 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.
# 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
0.4.0- Tagged
- Jun 2, 2026
- Commit
0e1844272662- Indexed
- not yet
Dependents
and 1 more
Repository
github.com/kemalcr/kemal-session
Metadata
- Created
- Aug 12, 2026
- Updated
- Aug 12, 2026
- Synced
- Aug 12, 2026
- Versions
- 23