Skip to content

Sessions

A velociraptor doesn't drag a heavy pack across the territory. It keeps what it needs close to the body, light and secure. VelociPy sessions follow that rule: all session data lives in a signed browser cookie, so there is no server-side store to configure, no Redis to warm up, and no stale rows to sweep.

SessionMiddleware signs every cookie with HMAC-SHA256 using only the Python standard library. The client can see the data, but any tampering invalidates the signature and the session starts fresh.

No extra dependencies

Session signing uses hmac, json, base64, and time from the standard library. You don't need to install any crypto package to use sessions.

Setting up the middleware

Attach SessionMiddleware with app.add_middleware() and a strong secret key:

from velocipy import Request, VelociPy
from velocipy.middleware import SessionMiddleware

app = VelociPy()
app.add_middleware(
    SessionMiddleware,
    secret_key="change-me-in-production",
    max_age=3600,
    http_only=True,
    secure=False,
    same_site="lax",
)


@app.post("/login")
async def login(request: Request) -> dict[str, bool]:
    request.session["user_id"] = "42"
    request.session["username"] = "alice"
    return {"ok": True}


@app.get("/me")
async def me(request: Request) -> dict[str, str | None]:
    return {
        "user_id": request.session.get("user_id"),
        "username": request.session.get("username"),
    }


@app.post("/logout")
async def logout(request: Request) -> dict[str, bool]:
    request.session.clear()
    return {"ok": True}

request.session behaves like a normal dictionary. The middleware notices when you change it and writes a fresh signed cookie to the response automatically.

Protect your secret key

Anyone who knows secret_key can forge valid session cookies. In production, load it from an environment variable and keep it out of source control.

Reading and writing session data

You can use the full dictionary API on request.session:

@app.get("/cart")
async def cart(request: Request):
    items = request.session.get("cart", [])
    return {"items": items}


@app.post("/cart/add")
async def add_to_cart(request: Request):
    cart = request.session.setdefault("cart", [])
    cart.append(request.query_params.get("item", "egg"))
    request.session["cart"] = cart  # marks the session as changed
    return {"items": cart}

The middleware only sends a Set-Cookie header when the session has actually changed. Read-only requests don't rewrite the cookie, which keeps responses small.

Session options

SessionMiddleware accepts the usual cookie controls:

Option Default What it controls
secret_key required Key used to sign the cookie payload.
session_cookie "session" Name of the cookie.
max_age 3600 Cookie lifetime in seconds; None for a browser-session cookie.
path "/" Cookie path.
domain None Optional cookie domain.
secure False Send only over HTTPS.
http_only True Hide the cookie from JavaScript.
same_site "lax" SameSite policy ("lax", "strict", "none").

Production-hardened settings

import os

app.add_middleware(
    SessionMiddleware,
    secret_key=os.environ["SESSION_SECRET_KEY"],
    max_age=7 * 24 * 60 * 60,  # one week
    secure=True,
    http_only=True,
    same_site="strict",
)

Use secure=True behind HTTPS

Without HTTPS, a Secure cookie is never sent back by the browser. Enable it only when your site runs over TLS.

The middleware stores the session as a compact JSON payload with a timestamp, then signs it with HMAC-SHA256. On each request it:

  1. Reads the cookie from the request.
  2. Verifies the signature with constant-time comparison.
  3. Checks the timestamp against max_age.
  4. Rejects anything malformed or expired by starting a fresh empty session.

No state is kept on the server, so scaling out is trivial: every instance only needs the same secret_key.

Testing sessions

The built-in TestClient handles cookies automatically, so session flows are easy to verify in-process:

from velocipy.testing import TestClient

with TestClient(app) as client:
    response = client.get("/me")
    assert response.json() == {"user_id": None, "username": None}

    response = client.post("/login")
    assert response.status_code == 200

    response = client.get("/me")
    assert response.json() == {"user_id": "42", "username": "alice"}

    response = client.post("/logout")
    assert response.status_code == 200

    response = client.get("/me")
    assert response.json() == {"user_id": None, "username": None}

What to store in a session

Because the cookie travels with every request, keep sessions small. Good candidates:

  • A user ID or opaque session ID.
  • Lightweight preferences like theme or language.
  • A small list of IDs that point to server-side data.

Avoid storing large objects, secret values, or permission lists that grow with the user. If you need richer server-side state, store a session ID in the cookie and keep the rest in a database or cache.

At a glance

Feature Behavior
Storage Signed client-side cookie; no server-side state.
Signing HMAC-SHA256, stdlib only.
Mutation tracking Auto-detected; cookie is rewritten only when changed.
Expiry Enforced via cookie Max-Age and signed timestamp.
Tamper resistance Invalid signature or payload → fresh empty session.
Scaling Share secret_key across instances; nothing else needed.

See also

  • examples/sessions.py for a runnable session login/logout demo.
  • Middleware for how add_middleware stacks middleware layers.
  • Config for loading secret_key from environment variables in a typed way.