Skip to content

Cookies

@nano_kit/cookie-store 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.

@nano_kit/cookie-store is an optional peer dependency of @nano_kit/ssr. Install it only when your SSR app uses cookies.

pnpm add @nano_kit/cookie-store

Enable request-bound cookie stores with the SSR plugin option:

// vite.config.ts
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',
cookieStore: true
})
]
})

The option makes the renderer create a VirtualCookieStore for each request and provide it through CookieStore$.

Read CookieStore$ inside a store factory when you want to work with the CookieStore API directly.

// src/stores/session.ts
import { action, inject } from '@nano_kit/store'
import { CookieStore$ } from '@nano_kit/cookie-store'
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.

If you want a writable signal backed by cookies, pass the injected CookieStore$ value to cookieStored:

// src/stores/session.ts
import { action, inject } from '@nano_kit/store'
import { CookieStore$ } from '@nano_kit/cookie-store'
import { 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
}
}

Pass the incoming Cookie header to renderer.render(url, cookieHeader), and forward returned Set-Cookie headers to the HTTP response.

// server.js
import { renderer } from './dist/renderer/index.js'
app.get('*', async (req, res) => {
const result = await renderer.render(req.url, 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')
})

Because cookies are available in Stores$, a route can mutate cookies on the server and redirect without rendering UI.

// src/pages/Logout.tsx
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.

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/cookie-store'
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']

See the Session Cookies example for a complete React SSR app using request cookies, cookie-backed signals, server-side logout, and hydration without mismatches.