Caller state
A source keeps rows; some things belong to the caller instead — a cart before sign-in, a chosen
locale. An op asks for State and reads or writes it by name:
import type { State } from '@fougere/core';
export default class CartHandler {
async open(state: State) {
state.set('cart', 'c42');
return { opened: true };
}
async current(state: State) {
return { cart: state.get<string>('cart') ?? null };
}
}
The op names no cookie and no header. How each name is kept is the operator's, in
fougere.config.ts, and a package brings the way to keep it:
import '@fougere/state-cookie';
export default defineFougere({
state: { cart: { cookie: { maxAge: 604800 } } },
});
A cookie is HttpOnly, SameSite=Lax and Path=/ unless the entry says otherwise
(sameSite: 'none' adds Secure). state.delete('cart') removes it.
Kept in a session
What is too large for a cookie, or must stay hidden from the caller, is kept server-side:
import '@fougere/state-session';
export default defineFougere({
state: {
locale: { cookie: {} }, // its own cookie
cart: { session: {} }, // in the session row
draft: { session: {} }, // the same row
},
});
The caller holds one cookie, session, with a random reference. Every name kept in session lives in
one stateSession row, an entity like any other: it sits in the default source, and fougere migrate
creates its table. The op does not change — state.set('cart', …) whichever way cart is kept. A
session whose names are all forgotten is deleted, and its cookie expired.
A name state: does not declare is refused rather than kept in a session by default: a session needs a
table, and no table appears because an op wrote a value.
What travels, and what is written
- Signed, not encrypted. The value leaves with a signature made with
FOUGERE_STATE_KEY. The caller can read it and cannot change it: a changed value reads as absent. What must stay hidden is kept in a session. - Written only on an answer. What the calls of a request wrote leaves with the answer; a refused request keeps nothing.
- One request, every call. A GraphQL document making three calls writes once, with the answer.
- Every HTTP door carries it — REST, GraphQL, and the envelope a page calls. A call one process makes to another carries no caller state: it has no caller.
What is refused
state.seton a namestate:does not declare, or one the door cannot carry, naming what the door keeps.state.setoutside a request at a door — a call made in process, or a runtime with no async context. On a Worker,nodejs_compatis that context;fougere checknames awranglerconfig declaringstate:without it (state-without-async-context).- With no
FOUGERE_STATE_KEY, the boot warns: the key dies with the process, so a restart or a second replica reads every value as absent.