Dead simple cookie-based session for Deno Fresh v2.
- 🔐 AES-GCM 256-bit encryption - Secure session data with encrypted cookies
- 💾 Multiple storage backends - Memory, Cookie, Deno KV, Redis, SQL
- ⚡ Flash messages - One-time messages for redirects
- 🔄 Session rotation - Regenerate session ID for security
- 🎯 TypeScript first - Full type safety
deno add jsr:@octo/fresh-sessionimport {
CookieSessionStore,
KvSessionStore,
MemorySessionStore,
RedisSessionStore,
session,
type SessionState,
SqlSessionStore,
} from "@octo/fresh-session";Sample app notes:
sample/session_memory.tsusesMemorySessionStoresample/session_cookie.tsshows the cookie store patternsample/session_kv.tsshows the Deno KV store patternsample/session_redis.tsshows the Redis store patternsample/session_mysql.tsshows the MySQL store patternsample/session_postgres.tsshows the PostgreSQL store pattern
Redis/MySQL/PostgreSQL sample notes:
- Samples read
REDIS_HOST,REDIS_PORT,MYSQL_HOST,MYSQL_PORT,POSTGRES_HOST,POSTGRES_PORT - Defaults (when using
with-resource):- Redis:
127.0.0.1:6380 - MySQL:
127.0.0.1:3307 - PostgreSQL:
127.0.0.1:5433
- Redis:
// routes/_middleware.ts
import { App } from "@fresh/core";
import {
MemorySessionStore,
session,
type SessionState,
} from "@octo/fresh-session";
// Define your app state
interface State extends SessionState {
// your other state properties
}
const app = new App<State>();
// Create a store instance
const store = new MemorySessionStore();
// Add session middleware
// Secret key must be at least 32 characters for AES-256
app.use(session(store, "your-secret-key-at-least-32-characters-long"));// routes/index.tsx
app.get("/", (ctx) => {
const { session } = ctx.state;
// Get value from session
const count = (session.get("count") as number) ?? 0;
// Set value to session
session.set("count", count + 1);
// Check if session is new
const isNew = session.isNew();
// Get session ID
const sessionId = session.sessionId();
return ctx.render(<div>Visit count: {count + 1}</div>);
});const { session } = ctx.state;
// Basic operations
session.get("key"); // Get a value
session.set("key", value); // Set a value
session.isNew(); // Check if session is new
session.sessionId(); // Get session ID
// Flash messages (one-time data)
session.flash.set("message", "Success!"); // Set flash data
session.flash.get("message"); // Get & consume flash data
session.flash.has("message"); // Check if flash exists
// Security
session.destroy(); // Destroy session
session.rotate(); // Rotate session ID (recommended after login)Simple in-memory storage. Data is lost when the server restarts.
import { MemorySessionStore } from "@octo/fresh-session";
const store = new MemorySessionStore();Stores session data in the cookie itself. No server-side storage needed.
⚠️ Cookie size limit is ~4KB. Use for small session data only.
import { CookieSessionStore } from "@octo/fresh-session";
const store = new CookieSessionStore();Persistent storage using Deno KV. Recommended for Deno Deploy.
import { KvSessionStore } from "@octo/fresh-session";
const kv = await Deno.openKv();
const store = new KvSessionStore({ kv, keyPrefix: ["my_sessions"] });For distributed environments with Redis.
import { type RedisClient, RedisSessionStore } from "@octo/fresh-session";
import { connect } from "jsr:@db/redis";
const redis = await connect({
hostname: "127.0.0.1",
port: 6379,
});
// Adapt to RedisClient interface
const client: RedisClient = {
get: (key) => redis.get(key),
set: (key, value, options) =>
redis.set(key, value, options?.ex ? { ex: options.ex } : undefined)
.then(() => {}),
del: (key) => redis.del(key).then(() => {}),
};
const store = new RedisSessionStore({ client, keyPrefix: "session:" });For applications using relational databases.
-- Required table structure (MySQL)
CREATE TABLE sessions (
session_id VARCHAR(36) PRIMARY KEY,
data TEXT NOT NULL,
expires_at DATETIME NULL
);
CREATE INDEX idx_sessions_expires_at ON sessions(expires_at);
-- PostgreSQL example
CREATE TABLE sessions (
session_id VARCHAR(36) PRIMARY KEY,
data TEXT NOT NULL,
expires_at TIMESTAMP NULL
);
CREATE INDEX idx_sessions_expires_at ON sessions(expires_at);import { type SqlClient, SqlSessionStore } from "@octo/fresh-session";
// Adapt your SQL client to SqlClient interface
const client: SqlClient = {
execute: async (sql, params) => {
const result = await yourDbClient.query(sql, params);
return { rows: result.rows };
},
};
const store = new SqlSessionStore({ client, tableName: "sessions" });
// For PostgreSQL:
// const store = new SqlSessionStore({ client, tableName: "sessions", dialect: "postgres" });deno task sample:memory
deno task sample:cookie
deno task sample:kv
deno task sample:redis
deno task sample:mysql
deno task sample:postgresAll Redis/MySQL/PostgreSQL samples use scripts/with-resource.ts, which starts
Docker containers and injects environment variables.
This repo uses named permissions for the resource wrapper:
"permissions": {
"with-resource": {
"run": ["docker", "deno"],
"env": [
"MYSQL_DATABASE",
"MYSQL_USER",
"MYSQL_PASSWORD",
"POSTGRES_DATABASE",
"POSTGRES_USER",
"POSTGRES_PASSWORD"
]
}
}Tasks that start containers use -P=with-resource.
app.use(session(store, secret, {
// Cookie name
cookieName: "fresh_session", // default
// Cookie options
cookieOptions: {
path: "/",
httpOnly: true,
secure: true, // Set to false for local development
sameSite: "Lax", // "Strict" | "Lax" | "None"
maxAge: 60 * 60 * 24, // 1 day in seconds
domain: "",
},
// Session expiration in milliseconds
sessionExpires: 1000 * 60 * 60 * 24, // 1 day (default)
}));Flash messages are one-time data that get cleared after being read. Perfect for success/error messages after redirects.
// In your form handler
app.post("/login", async (ctx) => {
const form = await ctx.req.formData();
// ... validate login
if (success) {
ctx.state.session.flash.set("message", "Login successful!");
ctx.state.session.flash.set("type", "success");
} else {
ctx.state.session.flash.set("message", "Invalid credentials");
ctx.state.session.flash.set("type", "error");
}
return new Response(null, {
status: 302,
headers: { Location: "/" },
});
});
// In your page
app.get("/", (ctx) => {
const message = ctx.state.session.flash.get("message"); // Read & clear
const type = ctx.state.session.flash.get("type");
// message is now cleared and won't appear on next request
return ctx.render(
<div>{message && <Alert type={type}>{message}</Alert>}</div>,
);
});Regenerate session ID while keeping session data. Recommended after authentication to prevent session fixation attacks.
app.post("/login", async (ctx) => {
// ... validate credentials
if (authenticated) {
// Rotate session ID for security
ctx.state.session.rotate();
ctx.state.session.set("userId", user.id);
}
return new Response(null, {
status: 302,
headers: { Location: "/dashboard" },
});
});This occurs when using Response.redirect(). Use this workaround instead:
// ❌ Don't use Response.redirect()
return Response.redirect("/dashboard");
// ✅ Use this instead
return new Response(null, {
status: 302,
headers: { Location: "/dashboard" },
});- Make sure your secret key is at least 32 characters
- Check that
secure: falseis set for local development (non-HTTPS) - Verify the session middleware is added before your routes
MIT
- Initial work by @xstevenyung
- Inspiration from Oak Sessions