Authentication
Keryx uses cookie-based sessions stored in Redis. Authentication is handled through actions and middleware — there's no separate auth plugin or magic. You write a login action, apply SessionMiddleware to protected routes, and the framework handles the rest.
Sessions
When a user logs in, a session is created in Redis and a cookie is set on the response. The session stores arbitrary typed data (like userId) and has a configurable TTL (default: 24 hours).
// Define your session data shape
export type SessionImpl = { userId?: number };The session initializer manages the full lifecycle:
// Create or update session data
await connection.updateSession({ userId: user.id });
// Access session data
const userId = connection.session?.data.userId;
// Destroy the session (logout)
await api.session.destroy(connection);Sessions are typed via the generic Connection<T> parameter, giving you type-safe access to session data throughout your middleware and actions.
Login Action
A login action validates credentials and creates a session:
import { SessionMiddleware } from "../middleware/session";
import type { SessionImpl } from "./session";
export class SessionCreate implements Action {
name = "session:create";
description = "Sign in with email and password";
web = { route: "/session", method: HTTP_METHOD.PUT };
mcp = { enabled: false, isLoginAction: true };
middleware = [RateLimitMiddleware];
inputs = z.object({
email: z
.string()
.email()
.transform((val) => val.toLowerCase()),
password: secret(z.string().min(8)),
});
run = async (
params: ActionParams<SessionCreate>,
connection: Connection<SessionImpl>,
) => {
const [user] = await api.db.db
.select()
.from(users)
.where(eq(users.email, params.email));
if (!user || !(await checkPassword(user, params.password))) {
throw new TypedError({
message: "Invalid email or password",
type: ErrorType.CONNECTION_ACTION_RUN,
});
}
await connection.updateSession({ userId: user.id });
return { user: serializeUser(user), session: connection.session! };
};
}The mcp = { isLoginAction: true } marker tells the OAuth system to use this action during the MCP authorization flow. See MCP for details.
SessionMiddleware
Apply SessionMiddleware to any action that requires authentication:
import { SessionMiddleware } from "../middleware/session";
export class UserEdit implements Action {
name = "user:edit";
middleware = [RateLimitMiddleware, SessionMiddleware];
web = { route: "/user", method: HTTP_METHOD.POST };
// ...
}If no valid session exists, SessionMiddleware throws a TypedError with type CONNECTION_SESSION_NOT_FOUND (HTTP 401). The action's run() method is never called.
The middleware checks both that connection.session exists and that connection.session.data.userId is set — so even if a session cookie is present, the user must have actually logged in.
Logout Action
export class SessionDestroy implements Action {
name = "session:destroy";
web = { route: "/session", method: HTTP_METHOD.DELETE };
middleware = [RateLimitMiddleware, SessionMiddleware];
async run(_params, connection: Connection<SessionImpl>) {
await api.session.destroy(connection);
return { success: true };
}
}OAuth for MCP Clients
MCP clients authenticate through OAuth 2.1 with PKCE. The OAuth flow renders a browser-based login/signup page that invokes your login and signup actions. Actions tagged with mcp.isLoginAction and mcp.isSignupAction must return an OAuthActionResponse:
// Must return this shape from login/signup actions used in OAuth
type OAuthActionResponse = { user: { id: number } };The OAuth page dynamically generates form fields from your action's Zod inputs schema — if you change the inputs on your login or signup action, the form updates automatically. Fields wrapped in secret() render as password inputs, and .describe() text is used for labels.
See the MCP guide for the full OAuth flow and endpoint documentation.
Session Configuration
| Key | Env Var | Default | Description |
|---|---|---|---|
cookieName | SESSION_COOKIE_NAME | "session_id" | Cookie name for session tracking |
ttl | SESSION_TTL | 86400 | Session TTL in seconds (1 day) |
Cookie security settings (Secure, SameSite, HttpOnly) are configured in the web server config. See the Security guide for production recommendations.
Testing with Sessions
The pattern for testing authenticated endpoints:
// 1. Create a user
await fetch(url + "/api/user", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: "Test User",
email: "test@example.com",
password: "password123",
}),
});
// 2. Log in to get a session
const sessionRes = await fetch(url + "/api/session", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: "test@example.com",
password: "password123",
}),
});
const { session } = await sessionRes.json();
const sessionId = session.id;
// 3. Use the session cookie on authenticated requests
const res = await fetch(url + "/api/user", {
method: "POST",
headers: {
"Content-Type": "application/json",
Cookie: `${config.session.cookieName}=${sessionId}`,
},
body: JSON.stringify({ name: "New Name" }),
});See the Testing guide for more patterns.