Cookies
@nano_kit/platform-web provides a request-bound, CookieStore-compatible implementation for SSR and tests.
Use it when server-rendered code needs to read incoming cookies, write Set-Cookie headers, or provide cookie state through Nano Kit dependency injection. You can use it directly through the CookieStore API, or combine it with cookieStored from @nano_kit/platform-web when you want cookie-backed signals.
Installation
Section titled “Installation”@nano_kit/platform-web is an optional peer dependency of @nano_kit/ssr. Install it only when your SSR app uses cookies.
pnpm add @nano_kit/platform-webyarn add @nano_kit/platform-webnpm install @nano_kit/platform-webVite Plugin
Section titled “Vite Plugin”Enable request-bound cookie stores with the SSR plugin option:
import { defineConfig } from 'vite'import react from '@vitejs/plugin-react'import ssr from '@nano_kit/react-ssr/vite-plugin'
export default defineConfig({ plugins: [ react(), ssr({ index: 'src/index.tsx', inject: { cookieStore: true } }) ]})The option makes the renderer create a VirtualCookieStore for each request and provide it through CookieStore$.
Direct Cookie Store Usage
Section titled “Direct Cookie Store Usage”Read CookieStore$ inside a store factory when you want to work with the CookieStore API directly.
import { action, inject } from '@nano_kit/store'import { CookieStore$ } from '@nano_kit/platform-web'
const SESSION_MAX_AGE = 60 * 60 * 24 * 30
export function Session$() { const cookieStore = inject(CookieStore$) const getUsername = () => cookieStore.get('session') const login = action((username: string) => { const value = username.trim()
if (value) { void cookieStore.set({ name: 'session', value, path: '/', sameSite: 'lax', expires: Date.now() + SESSION_MAX_AGE * 1000 }) } }) const logout = action(() => { void cookieStore.delete({ name: 'session', path: '/' }) })
return { getUsername, login, logout }}In the browser, CookieStore$ resolves to the native browser cookieStore. During SSR, it resolves to a virtual store created from the incoming Cookie header for the current request.
Cookie-Backed Signals
Section titled “Cookie-Backed Signals”If you want a writable signal backed by cookies, pass the injected CookieStore$ value to cookieStored:
import { action, inject } from '@nano_kit/store'import { CookieStore$, cookieStored} from '@nano_kit/platform-web'
const SESSION_MAX_AGE = 60 * 60 * 24 * 30
export function Session$() { const cookieStore = inject(CookieStore$) const $username = cookieStored<string | null>(cookieStore, { name: 'session', path: '/', sameSite: 'lax', maxAge: SESSION_MAX_AGE }, null) const login = action((username: string) => { const value = username.trim()
if (value) { $username(value) } }) const logout = action(() => { $username(null) })
return { $username, login, logout }}Forwarding Cookies to an API
Section titled “Forwarding Cookies to an API”In the browser, fetch attaches the user’s cookies automatically. During SSR the request is server-to-server, so a store that reads a cookie-authenticated backend must forward the incoming cookies itself. serializeCookies reads the named cookies from a CookieStore and serializes them into a Cookie request header value.
A common pattern is a server-only API service that adds the forwarded headers to every request:
import { Injectable$, inject} from '@nano_kit/store'import { CookieStore$, serializeCookies} from '@nano_kit/platform-web'
export class ServerApi$ extends Injectable$ { cookieStore = inject(CookieStore$)
async fetch(path: string, options?: RequestInit) { const cookie = await serializeCookies(this.cookieStore, ['session'])
return fetch(`https://api.example.com/${path}`, { ...options, headers: { ...options?.headers, Cookie: cookie } }) }}Swap it for a browser implementation with import.meta.env.SSR. The client version skips serializeCookies — the browser attaches the cookies for you:
import { ClientApi$ } from './api.client'import { ServerApi$ } from './api.server'
export const Api$ = import.meta.env.SSR ? ServerApi$ : ClientApi$serializeCookies(cookieStore, ['session', 'locale']) resolves to a value like session=abc123; locale=en, and only the requested cookies that exist in the store are included.
Production Server
Section titled “Production Server”Pass the incoming Cookie header to renderer.render(url, { cookie }), and forward returned Set-Cookie headers to the HTTP response.
import { renderer } from './dist/renderer/index.js'
app.get('*', async (req, res) => { const result = await renderer.render(req.url, { cookie: req.headers.cookie })
if (result.setCookieHeaders) { res.setHeader('Set-Cookie', result.setCookieHeaders) }
if (result.redirect) { return res.redirect(result.statusCode, result.redirect) }
if (result.html !== null) { return res.status(result.statusCode).send(result.html) }
res.status(result.statusCode).send('Not Found')})Server-Side Mutations
Section titled “Server-Side Mutations”Because cookies are available in Stores$, a route can mutate cookies on the server and redirect without rendering UI.
import { Navigation$ } from '@nano_kit/router'import { inject } from '@nano_kit/store'import { Session$ } from '../stores/session'
export function Stores$() { const navigation = inject(Navigation$) const { logout } = inject(Session$)
logout() navigation.replace('/')
return []}
export default function Logout() { return <></>}The renderer returns a redirect and a deletion Set-Cookie header. The browser receives both in the same response.
Low-Level Usage
Section titled “Low-Level Usage”You can also use VirtualCookieStore directly in tests or custom render pipelines:
import { InjectionContext, provide } from '@nano_kit/store'import { CookieStore$, VirtualCookieStore} from '@nano_kit/platform-web'
const cookieStore = new VirtualCookieStore( 'theme=dark; session=abc123', '/dashboard')
const context = new InjectionContext([ provide(CookieStore$, cookieStore)])
await cookieStore.set({ name: 'theme', value: 'light', path: '/', sameSite: 'lax'})
cookieStore.peek('theme') // 'light'cookieStore.drainSetCookieHeaders()// ['theme=light; Path=/; SameSite=Lax']Examples
Section titled “Examples”See the Session Cookies example for a complete React SSR app using request cookies, cookie-backed signals, server-side logout, and hydration without mismatches.
See the Event Board example for session authentication with an HttpOnly cookie: a login flow, a route guard with SSR redirects, and an injectable API service that forwards the session cookie to the API during SSR.