Skip to main content
zerotal

Cookies

Most state you'd reach for cookies for is better handled by the session — it stores data in a signed, HttpOnly cookie for you. When you need to read or set a raw cookie directly, you work with the standard Request/Response headers on the HTTP context.

Which should I use?

  • Session — the default. User state, flash messages, anything sensitive or tamper-prone. The session driver signs and encrypts it for you.
  • Raw cookie — small, non-sensitive client preferences the browser must read too (a theme toggle, a dismissed-banner flag). Reach for the manual approach below only here.

Danger — A plain Set-Cookie value is fully client-visible and editable. Zerotal signs the session cookie so it can't be tampered with; for a tamper-proof value of your own, store it in the session or sign it yourself with Url.sign or encryption rather than trusting a raw cookie.

Cookies arrive in the request's Cookie header. Read and parse it from the context:

// in a controller
import type { HttpContext } from "zerotal";

function readCookie(ctx: HttpContext, name: string): string | undefined {
  const header = ctx.request.headers.get("Cookie") ?? "";
  for (const part of header.split(";")) {
    const [k, ...v] = part.trim().split("=");
    if (k === name) return decodeURIComponent(v.join("="));
  }
  return undefined;
}

const theme = readCookie(ctx, "theme") ?? "light";

Set the response with a ctx helper, then append a Set-Cookie header to it. Use append (not set) so multiple cookies can be sent in one response:

// in a controller
ctx.json({ ok: true }); // assigns ctx.response

ctx.response!.headers.append(
  "Set-Cookie",
  `theme=dark; Path=/; Max-Age=${60 * 60 * 24 * 365}; SameSite=Lax`,
);

Notectx.json() returns void — it sets ctx.response for you. Reach for the response object via ctx.response after calling a response helper; don't assign its return value.

AttributeWhy
Path=/Make the cookie apply site-wide.
HttpOnlyHide it from JavaScript — use for anything sensitive.
SecureOnly send over HTTPS. Enable in production.
SameSite=LaxSensible CSRF-resistant default; use Strict for extra isolation.
Max-Age=<seconds>Lifetime. Omit for a session cookie that clears on browser close.

To delete a cookie, set it again with Max-Age=0:

// in a controller
ctx.response!.headers.append("Set-Cookie", "theme=; Path=/; Max-Age=0");

Framework-managed cookies

You rarely set cookies by hand — two parts of the framework manage their own:

  • Session cookie — the session driver stores the whole session in a single cookie, signed with HMAC-SHA256 and flagged HttpOnly, SameSite=Lax (and Secure in production). Its name (cookie) and lifetime (lifetime) come from config/session.ts. Put user state in the session rather than rolling your own signed cookie.
  • XSRF-TOKEN cookieCsrfMiddleware sets this readable (non-HttpOnly) cookie after every request so Axios/Inertia can echo it back as the X-XSRF-TOKEN header.

Next steps

  • Session — signed, HttpOnly cookie-backed state (the usual choice).
  • CSRF Protection — the XSRF-TOKEN cookie.
  • HTTP Context — the request/response objects you read and write.
  • Encryption — sign or encrypt your own cookie values.