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.set on a name state: does not declare, or one the door cannot carry, naming what the door keeps.
  • state.set outside a request at a door — a call made in process, or a runtime with no async context. On a Worker, nodejs_compat is that context; fougere check names a wrangler config declaring state: 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.
Built with Fougere and Nuxt, hosted on GitHub Pages.