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.
How the cookie is protected¶
The middleware stores the session as a compact JSON payload with a timestamp, then signs it with HMAC-SHA256. On each request it:
- Reads the cookie from the request.
- Verifies the signature with constant-time comparison.
- Checks the timestamp against
max_age. - 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.pyfor a runnable session login/logout demo.- Middleware for how
add_middlewarestacks middleware layers. - Config for loading
secret_keyfrom environment variables in a typed way.