Sessions And Cookies
Flint has server-side sessions, HTTP cookie helpers, flash-message cookies, and browser-side Flint UI session helpers. They solve different problems, so do not treat them as one system.
Use this guide before adding login sessions, remember-me cookies, flash messages, browser auth state, or middleware that reads/writes cookies.
Framework Source References
When behavior is unclear, inspect these files in the installed package:
lib/session.dartlib/src/session/session.dartlib/src/session/session_service.dartlib/src/session/cookie.dartlib/src/session/cookie_service.dartlib/src/middleware/cookiesessionmiddleware.dartlib/src/request.dartlib/src/response.dartlib/src/templateengine/allexpression/session.dartlib/src/ui/auth/auth_session.dartlib/src/ui/storage/cookies.dart
Read Templates before changing session(...), hasSession(...), flash rendering, or template syntax.
Imports
Most server app code can use:
import 'package:flint_dart/flint_dart.dart';
Focused session/cookie imports are also available:
import 'package:flint_dart/session.dart';
Frontend Flint UI code should use:
import 'package:flint_dart/ui.dart';
The Mental Model
There are three common cookie/session shapes in Flint:
- Server sessions: the browser receives a
FLINTSESSIDcookie, and the session data lives on the server throughSessionManager. - Plain HTTP cookies: the app writes a specific cookie value with
res.setCookie(...)orCookieService.set(...). - Browser UI auth session: Flint UI stores a token and user payload in browser storage through
AuthSessionManager.
Server sessions are good for server-rendered pages, cookie login flows, and request-side auth fallback.
Plain cookies are good for small preferences, flash messages, non-secret browser state, or controlled auth cookies.
Browser auth sessions are good for client-side UI state, but values in localStorage, sessionStorage, or JavaScript-readable cookies are visible to browser JavaScript. Do not store secrets there unless the app accepts that risk.
Default Middleware
CookieSessionMiddleware initializes the static cookie/session services for each HTTP request:
CookieService.init(ctx.req, res);
SessionService.init(ctx.req, res);
Flint installs this middleware by default. Even when withDefaultMiddleware: false, the constructor still adds CookieSessionMiddleware, so HTTP routes can initialize cookies and sessions.
The middleware checks ctx.res first. WebSocket contexts pass through without initializing CookieService or SessionService, because WebSocket handlers do not have an HTTP response after upgrade.
For new route and controller code, prefer the instance helpers on req and res. Use SessionService and CookieService when app code intentionally wants the static request-local convenience layer.
Environment
Server sessions read configuration from environment values through FlintEnv. Set these before the session manager is first used.
SESSION_DRIVER=memory
SESSION_TTL=7d
SESSION_FILE=sessions.json
SESSION_DB_TABLE=sessions
SESSION_COOKIE_SECURE=false
SESSION_COOKIE_HTTP_ONLY=true
SESSION_COOKIE_SAMESITE=Lax
SESSION_COOKIE_PATH=/
APP_ENV=development
Supported session drivers:
memory: stores sessions in the current Dart process.file: stores sessions in a JSON file.db: stores sessions in a database table.
TTL values currently support days and hours:
SESSION_TTL=7d
SESSION_TTL=12h
Any other shape falls back to seven days.
Cookie defaults:
SESSIONCOOKIESECUREdefaults totruewhenAPP_ENV=production; otherwise it defaults tofalse.SESSIONCOOKIEHTTP_ONLYdefaults totrue.SESSIONCOOKIESAMESITEdefaults toLax.SESSIONCOOKIEPATHdefaults to/.
For cross-site cookies, browsers usually require SameSite=None and Secure. Use that only when the app is actually served over HTTPS.
Session Drivers
Memory
The memory driver is the default:
SESSION_DRIVER=memory
It stores session data in a process-local map. This is fine for development, tests, and simple local apps. It is not durable. Sessions disappear when the process restarts and are not shared across multiple server processes.
File
The file driver stores session data in a JSON file:
SESSION_DRIVER=file
SESSION_FILE=storage/sessions.json
If the file does not exist, Flint creates it and writes {}. Use a path that your server process can read and write. Do not put the session file under public/.
Database
The database driver stores sessions in a table:
SESSION_DRIVER=db
SESSION_DB_TABLE=sessions
The session manager creates the table if it does not exist. The table contains:
iduser_iddatacreated_atexpires_at
The data column uses JSONB on PostgreSQL and JSON on MySQL. The database connection must be available before database-backed sessions are used.
Keep SESSIONDBTABLE as a safe table identifier such as sessions or app_sessions.
Reading Cookies
Read request cookies from req.cookies:
final theme = req.cookies['theme'];
final sessionId = req.sessionId;
req.cookies parses the Cookie header into a Map<String, String>. req.sessionId is a shortcut for:
req.cookies['FLINTSESSID'];
Use cookies for small values. Do not trust cookie values just because the server can read them. Validate them, sign them, or store only opaque ids that point to server-side data.
Writing Cookies
Use response instance methods to write cookies:
return res
.setCookie(
'theme',
'dark',
maxAge: 60 * 60 * 24 * 30,
path: '/',
httpOnly: false,
secure: true,
sameSite: 'Lax',
)
.json({'ok': true});
res.setCookie(...) accepts:
namevalueexpiresmaxAgein secondspathhttpOnlysecuresameSite
Remove a cookie with:
return res.clearCookie('theme').json({'ok': true});
clearCookie(...) must use the same path that was used when the cookie was created. If the app set path: '/admin', clear it with:
res.clearCookie('theme', path: '/admin');
CookieService
CookieService is a static convenience API initialized per HTTP request by CookieSessionMiddleware.
CookieService.set(
'theme',
'dark',
httpOnly: false,
maxAge: const Duration(days: 30),
);
final theme = CookieService.get('theme');
CookieService.delete('theme');
CookieService.set(...) accepts:
namevaluehttpOnlysecurepathmaxAgeas aDurationexpiressameSite
When secure is not supplied, CookieService uses the same secure-cookie default as sessions: secure in production, not secure in development.
Do not call CookieService from startup code, queue jobs, isolates, or WebSocket event callbacks. It depends on the current HTTP request and response.
Creating A Server Session
Use req.startSession(...) in a route or controller after the user has passed the app's checks:
class LoginController extends Controller {
Future<Response> store() async {
final input = await req.validate({
'email': 'required|email',
'password': 'required|string',
});
final user = await User().where('email', input['email']).first();
if (user == null || !Hashing().verify(input['password'], user.password!)) {
return res.status(401).json({'message': 'Invalid credentials'});
}
await req.startSession({
'id': user.id,
'email': user.email,
'role': user.role,
});
return res.json({'user': user});
}
}
startSession(...):
- creates a random session id
- stores the data through the configured session driver
- adds
expires_atto the stored data - writes the
FLINTSESSIDcookie - caches the same data as
userin request-scoped storage - returns the new session id
You can pass a custom TTL:
await req.startSession(
{'id': user.id, 'role': user.role},
ttl: const Duration(hours: 8),
);
Keep session data small. Store ids, roles, and flags. Do not put entire model graphs, large payloads, passwords, password hashes, OTPs, or private tokens in the session.
Reading A Server Session
Use await req.session:
final session = await req.session;
if (session == null) {
return res.status(401).json({'message': 'Login required'});
}
return res.json({
'user': {
'id': session['id'],
'email': session['email'],
},
});
req.session reads FLINTSESSID, loads the session from the configured driver, checks expiration, and returns null when the session is missing or expired.
Expired sessions are removed from memory, file, or database storage when read.
Updating A Server Session
Use req.updateSession(...) to merge values into the current session:
await req.updateSession({
'role': 'admin',
'last_seen_at': DateTime.now().toIso8601String(),
});
updateSession(...):
- reads the current session
- returns
nullwhen no current session exists - merges the new values into existing data
- destroys the old session
- creates a new session
- writes a new
FLINTSESSIDcookie - caches the merged data as
userin request storage
Because the session id changes, call it before the response is sent.
Destroying A Server Session
Use req.destroySession() for logout:
class LogoutController extends Controller {
Future<Response> destroy() async {
await req.destroySession();
return res.json({'message': 'Logged out'});
}
}
destroySession() removes the stored session, clears the FLINTSESSID cookie, and removes the cached request user.
SessionService
SessionService is a static convenience API for key-by-key session access.
await SessionService.set('cart_id', cart.id);
final cartId = await SessionService.get('cart_id');
await SessionService.remove('cart_id');
await SessionService.destroy();
It is initialized by CookieSessionMiddleware and depends on the current HTTP request/response. Use it only inside HTTP request handling.
SessionService.set(...) reads the current FLINTSESSID, loads existing session data or starts with {}, changes one key, and creates a session with the updated data.
SessionService.remove(...) removes a key from the current session data and creates a session with the remaining data.
For login, logout, and sensitive auth changes, prefer req.startSession(...), req.updateSession(...), and req.destroySession() so the flow is explicit in the controller or action.
Auth And Sessions
req.user checks authentication in this order:
- Read a token from
req.authToken. - Verify the token with
Auth.verifyToken. - If a server session also exists, merge the session data with the token payload.
- If no valid token exists, fall back to server session data.
- Cache the user payload in request-scoped storage.
req.authToken reads:
Authorization: Bearer <token>- the cookie named by
FLINTAUTHCOOKIE, defaulting toauth.token flint.auth.token
Important: req.isAuthenticated only checks whether req.user has already cached a user on the request. It does not perform the lookup by itself.
Use:
final user = await req.user;
if (user == null) {
return res.status(401).json({'message': 'Unauthorized'});
}
or load the user in middleware and write it to the context:
class AuthMiddleware extends Middleware {
@override
Handler handle(Handler next) {
return (Context ctx) async {
final res = ctx.res;
if (res == null) return await next(ctx);
final user = await ctx.req.user;
if (user == null) {
return res.status(401).json({'message': 'Unauthorized'});
}
ctx.write<Map<String, dynamic>>(user);
return await next(ctx);
};
}
}
Then controllers can read:
final user = read<Map<String, dynamic>>();
JWT Cookie Login
Some apps use a JWT in an HTTP-only cookie instead of returning the token only in JSON.
final token = Auth.generateToken(user.toMap());
return res
.setCookie(
'auth.token',
token,
httpOnly: true,
secure: true,
sameSite: 'Lax',
path: '/',
maxAge: 60 * 60 * 24 * 7,
)
.json({'user': user});
Then req.authToken can read that cookie on later requests.
For browser clients that need to call APIs with Authorization: Bearer, return the token in JSON and let the frontend store it intentionally. Do not store the same sensitive token in many places without a reason.
Flash Messages
Flash messages are one-request messages usually used after redirects.
return res.withSuccess('Course saved').redirect('/courses');
or:
return res.withError('Unable to save course').back(fallback: '/courses/new');
withSuccess(...) and withError(...) use withFlash(...) internally. withFlash(...) writes an HTTP-only cookie named:
FLINT_FLASH_<key>
By default, flash cookies live for 120 seconds.
When res.view(...) renders the next template, Flint:
- Reads
FLINTFLASH*cookies from the request. - Adds them to the template data under
flash. - Adds each key directly to the template data.
- Adds them to
TemplateEngine().sessions. - Clears the flash cookies after rendering.
Template usage:
{{ if hasSession('success') }}
<p>{{ session('success') }}</p>
{{ endif }}
{{ if hasSession('error') }}
<p>{{ session('error') }}</p>
{{ endif }}
{{ session('key') }} reads from TemplateEngine().sessions. hasSession('key') becomes true or false during template rendering.
Flash cookies are for short messages, not private data.
Templates And Sessions
Template session helpers are not the same as req.session.
req.session reads server session data through SessionManager.
{{ session('key') }} reads TemplateEngine().sessions, which is populated by flash messages during res.view(...) or by code that intentionally writes to the template engine before rendering.
For normal server session values, pass the data explicitly:
final session = await req.session;
return res.view('dashboard.index', data: {
'userName': session?['name'] ?? 'Guest',
});
Template:
<h1>Hello {{ userName }}</h1>
This keeps templates clear and avoids assuming all server session values are automatically exposed to HTML.
Browser Cookies In Flint UI
Frontend code can use the shared cookies helper:
cookies.write(
'theme',
'dark',
maxAge: const Duration(days: 30),
sameSite: CookieSameSite.lax,
);
final theme = cookies.read('theme');
cookies.remove('theme');
Browser cookies written from JavaScript cannot be HttpOnly. Use them for preferences and non-secret state.
cookies.readAll() returns all JavaScript-visible cookies. It cannot read HTTP-only cookies created by the server.
Browser Auth Session In Flint UI
Flint UI has AuthSessionManager for simple browser-side auth state:
authSession.save(
token: token,
user: {
'id': user['id'],
'role': user['role'],
},
);
final loggedIn = authSession.isLoggedIn;
final current = authSession.current;
final role = authSession.role;
authSession.clear();
By default, authSession stores:
auth.token
auth.user
in localStorage.
Use custom keys when needed:
const adminSession = AuthSessionManager(
tokenKey: 'admin.auth.token',
userKey: 'admin.auth.user',
);
Use sessionStorage instead of localStorage when the login should be tied to the browser tab/session:
const tabSession = AuthSessionManager(
tokenKey: 'auth.session.token',
userKey: 'auth.session.user',
storage: sessionStorage,
);
Browser auth session is not a server session. Saving a token in AuthSessionManager does not create FLINTSESSID, and destroying FLINTSESSID does not automatically clear browser storage. Clear both when the app uses both.
WebSockets
WebSocket handlers receive a Context with ctx.req and ctx.socket. ctx.res is null after the upgrade.
The WebSocket handshake request can read cookies and bearer tokens:
app.websocket('/chat', (Context ctx) {
final token = ctx.req.authToken;
final sessionId = ctx.req.sessionId;
final socket = ctx.socket;
if (socket == null) return;
socket.emit('connected', {'sessionId': sessionId});
});
Do not use CookieService, SessionService, res.setCookie(...), or req.startSession(...) inside WebSocket event callbacks. Those operations need an HTTP response.
Authenticate sockets during the handshake or with a token sent by the client, then store any verified identity on the socket or in your own connection state.
Security Guidance
For session and cookie work:
- Keep server session data small and non-sensitive.
- Keep passwords, password hashes, OTPs, reset tokens, API keys, and private provider tokens out of sessions and JavaScript-visible storage.
- Use
httpOnly: truefor server-written auth/session cookies. - Use
secure: truein production HTTPS. - Use
SameSite=Laxfor most app cookies. - Use
SameSite=Strictfor highly sensitive same-site-only flows. - Use
SameSite=Noneonly withSecurefor cross-site use cases. - Rotate the session id after login, privilege changes, or sensitive account changes.
- Clear cookies on logout.
- Validate every cookie value before using it.
- Prefer server-side sessions for secrets and browser storage only for data the frontend is allowed to read.
Controller Examples
Login with server session:
class LoginController extends Controller {
Future<Response> store() async {
final input = await req.validate({
'email': 'required|email',
'password': 'required|string',
});
final user = await User().where('email', input['email']).first();
if (user == null || !Hashing().verify(input['password'], user.password!)) {
return res.status(401).json({'message': 'Invalid credentials'});
}
await req.startSession({
'id': user.id,
'email': user.email,
'role': user.role,
});
return res.json({'user': user});
}
}
Current user from token or session:
class CurrentUserController extends Controller {
Future<Response> show() async {
final user = await req.user;
if (user == null) {
return res.status(401).json({'message': 'Unauthorized'});
}
return res.json({'user': user});
}
}
Logout:
class LogoutController extends Controller {
Future<Response> destroy() async {
await req.destroySession();
return res
.clearCookie('auth.token')
.json({'message': 'Logged out'});
}
}
Flash redirect:
class CourseController extends Controller {
Future<Response> store() async {
final data = await req.validate({
'title': 'required|string|min:3',
});
await Course().create(data);
return res.withSuccess('Course created').redirect('/courses');
}
}
File Organization
Keep session/cookie behavior in named files:
lib/controllers/login_controller.dart
lib/controllers/logout_controller.dart
lib/middlewares/auth_middleware.dart
lib/services/auth/create_login_session_action.dart
lib/services/auth/clear_login_session_action.dart
lib/ui/state/auth_session_state.dart
Do not hide login/session behavior inside private helper methods on a route file. Follow the Flint class-per-file pattern.
Common Mistakes
- Treating browser
authSessionstorage as the same thing as serverFLINTSESSID. - Calling
req.isAuthenticatedbeforeawait req.user. - Calling
CookieServiceorSessionServiceoutside an HTTP request. - Trying to set or clear cookies after
res.json(...),res.send(...),res.view(...), or another response-writing method has closed the response. - Putting session files under
public/. - Using
SESSION_DRIVER=memoryfor a multi-process production app. - Storing passwords, OTPs, or private tokens in sessions.
- Forgetting
secure: truefor production auth cookies. - Using
SameSite=Nonewithout HTTPS. - Forgetting to clear both server cookies and browser storage when the app uses both.
- Expecting template
{{ session('key') }}to automatically expose everyreq.sessionvalue. - Using WebSocket callbacks to create server sessions or set cookies.
Review Checklist
Before finishing session/cookie work:
- The route or controller uses
Context/Controllercorrectly. - Cookies are written before the response is sent.
- Auth cookies are
HttpOnlyandSecurein production. SameSitematches the app's same-site or cross-site behavior.- Server sessions store only small allowed values.
- Logout clears the server session and any auth cookie.
- Frontend logout also clears
authSessionwhen browser storage is used. - Code calls
await req.userbefore usingreq.isAuthenticatedorreq.requireUser(). - Templates receive normal session data explicitly, while flash uses
withSuccess(...),withError(...), orwithFlash(...). - Middleware checks
ctx.resbefore writing HTTP cookies or responses.
One language powering Full-Stack Web, Cross-Platform Clients, Native AI, and Connected Robotics.