← Docs

Sessions

A Session row is created the moment a sign-in completes — one per completed login, carrying auth_method_used, the device fingerprint, IP, and last_active_at. Every access token and ID token this project issues carries that Session's id as its sid claim. List them at GET /api/v1/sessions/, or on the Sessions page.

Revoking is real, not cosmetic

POST /api/v1/sessions/{id}/revoke/ stamps revoked_at — and every subsequent request bearing a token with that Session's sid is rejected immediately, checked on every request via is_session_valid(), not just hidden from a list. This is the one enforcement point in this system that acts on an already-issued token immediately, rather than waiting for the next sign-in — see Roles & permissions for why a Permission change is deliberately not treated the same way.

Live, over a real WebSocket — not polling

A revocation pushes to every affected browser tab within about a second, over a native ASGI WebSocket at /ws/sessions/ (no django-channels — a small routing dispatcher in config/asgi.py). Authentication is the same httpOnly session cookie every other request already carries — no token is ever read by, or exposed to, client-side JS for this; the cookie is attached to the WebSocket handshake automatically because /ws/* is reverse-proxied to the same origin as everything else. On revoke, the affected session_id is published to a per-user Redis channel; every open connection for that user receives it — the exact tab holding the revoked session is told to log out immediately, and the Sessions list itself (any tab showing it) refreshes live, whether the revocation came from this dashboard's own "Revoke" button, "Log out of all devices," or another device entirely.

What's not built yet

Only session revocation pushes live today — no other Organization activity (a new member joining, a Role change, a webhook delivery) streams to this dashboard yet; those still require a page reload or a poll. See Audit log for what's available polled.