Sessions & httpOnly Cookies
The login flow ends with the API setting a cookie in the browser. From that point on, the user is authenticated. This page explains what is in that cookie, what is on the server side, and why each detail matters.
The cookie itself
When the API sets the session cookie, it looks something like this in the response headers:
Set-Cookie: dashify_session=
<long random id>; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=28800
A few things to notice:
- The value is a long random id, not the user id, not the email, not the password. It is just a meaningless token.
- The cookie carries no personal information at all. If someone reads it off the network, they learn nothing about the user.
- The flags after the value are the security settings.
The flags
HttpOnly
This is the most important flag. HttpOnly tells the browser: "JavaScript running on this page is not allowed to read this cookie."
Why does this matter? Because of cross-site scripting (XSS). If an attacker manages to inject malicious JavaScript into the page — through a vulnerable third-party widget, a forgotten input that does not sanitise HTML, anything — that JavaScript runs in the user's browser with the same privileges as the legitimate page.
Without HttpOnly, the malicious script could read the session cookie and send it to the attacker, who could then impersonate the user. With HttpOnly, the script cannot see the cookie at all. The browser still sends the cookie on legitimate API requests, but JavaScript code never gets to touch it.
This single flag makes XSS dramatically less dangerous. An attacker who can run code on your page can still cause damage, but they cannot steal sessions.
Secure
Secure tells the browser: "Only send this cookie over HTTPS connections, never plain HTTP."
In production, the entire site runs on HTTPS, so this is implicit. The flag matters because it prevents accidents — if a developer ever accidentally exposes an HTTP endpoint, the cookie does not leak there. Defence in depth.
In local development the flag is relaxed because localhost typically does not have HTTPS. The configuration knows the difference.
SameSite=Lax
SameSite controls when the browser sends the cookie on requests to your site that originate from other sites.
SameSite=None— send the cookie on every request, even if a different site is making the request. This is what enables CSRF attacks.SameSite=Strict— only send the cookie when the user is already on your site. Blocks CSRF entirely, but breaks legitimate cross-site flows like clicking a link from an email and arriving logged-in.SameSite=Lax— the middle ground. Send the cookie on top-level navigation (clicking a link), don't send it on background requests like forms or iframes initiated by other sites. Modern browsers default to this.
Dashify uses SameSite=Lax, which blocks the cross-site form-post variant of CSRF and still lets users click a link from their email and arrive logged in. We layer the CSRF double-submit pattern on top to handle the cases Lax does not cover. The CSRF page in the Security section has the full story.
Path=/
The cookie is sent on every request to the site, not just one specific path.
Max-Age
The cookie expires from the browser after this many seconds. The session itself also expires from Redis after the same window. They are kept in sync: 8 hours by default.
What the session record looks like in Redis
The cookie value is just a random id. The actual session data lives in Redis under that key:
session:9b3c5...e2d1
{
userId: "65f...",
tenantId: "63a...",
createdAt: 1715000000,
lastSeenAt: 1715000800,
ip: "203.0.113.4",
userAgent: "Mozilla/5.0 ...",
factors: ["password", "totp"]
}
Three things deserve attention:
- No personal data. The session record carries an id and some metadata, never anything secret.
lastSeenAtis updated on every request (rate-limited so we do not write to Redis on every single click). It powers the "active sessions" view in the user's security settings.factorsrecords which authentication factors were used. If 2FA was required, the array contains both "password" and "totp". The platform can require a re-authentication for sensitive actions if the session was authenticated with too few factors.
Why sessions and not just JWTs
Some platforms skip server-side sessions entirely and use JWTs (JSON Web Tokens) — self-contained, signed tokens that carry the user's identity inside them. The next page covers JWTs in detail.
Dashify uses both, deliberately:
- The session cookie is the primary authentication token. It can be revoked instantly by deleting the Redis record. If you log out from one device, every other device using that session is also logged out the moment they make their next request.
- JWTs are used in narrower contexts (mainly Socket.IO handshakes and short-lived API operations) where instant revocation matters less than not having to hit Redis on every connection.
The combination gives us both worlds: server-side control over when a session ends, and stateless lightweight tokens where they are appropriate.
The "active sessions" view
A logged-in user can see every active session for their account on the security page: the IP, the browser, when it was created, when it was last active. They can revoke any session — or all sessions except the current one — with one click. Behind the scenes that is just deleting the Redis records.
This is the same plumbing the user themselves uses — there is no separate "admin terminate" path. Consistency by reuse.
Session rotation
When a user does something sensitive (changes their password, enables 2FA, links an SSO account), the API rotates the session id. The old id is invalidated, a new one is issued. This makes it impossible for an attacker who somehow has a session id to keep using it after the user takes a recovery action.
Session rotation is a quiet feature — users rarely notice it — but it closes a real attack class.
What about cross-tab synchronisation?
If a user has Dashify open in two browser tabs and logs out in one, the other tab's next request fails authentication and redirects to login. This is automatic — the session is in Redis, so once it is deleted, every connection that depended on it sees the same answer.
Real-time updates (a banner saying "you have been logged out elsewhere") are also possible via Socket.IO and are layered on top in some flows.
Key takeaways
- The session cookie holds only a random id; the real session data lives in Redis.
- The cookie is HttpOnly (JavaScript cannot read it), Secure (HTTPS-only), and SameSite=Lax (limits cross-site sending).
- Session revocation is instant — delete the Redis record, the next request fails.
- Sessions rotate on sensitive events (password change, 2FA toggle).
- The user can see and revoke every active session from their security page.