Skip to content
ExponaBLOG
SVELTE & SVELTEKIT · DAY 058 / 100

Cookies in SvelteKit: a practical Svelte & SvelteKit guide

Set and read cookies safely.

Expona Admin6 min read

Cookies are one of those browser fundamentals that feel deceptively simple until you ship them to production and discover security holes. In SvelteKit, the cookie API is straightforward, but the devil lives in the configuration—one missing flag can leave your authentication tokens exposed to XSS attacks, or your state vulnerable to CSRF.

We'll walk through cookies in SvelteKit: a practical Svelte & SvelteKit guide that covers setting and reading them safely, understanding httpOnly and secure flags, why path must be set explicitly, and how sameSite prevents CSRF attacks.

Setting cookies with cookies.set in actions and loads

In SvelteKit, cookies live behind the cookies object, available in server-side code only. You'll encounter it in two main contexts: inside server-side load functions and within form actions.

Inside a load function, setting a cookie typically happens after some logic completes—perhaps after fetching user preferences or validating a session. Here's a practical example where we set a theme preference:

typescript
// src/routes/+page.server.ts
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ cookies }) => {
  const theme = cookies.get('theme') || 'light';
  
  return {
    theme
  };
};

More commonly, you'll set cookies inside form actions. This is where most authentication and state changes happen. Let's say we have a login flow:

typescript
// src/routes/login/+page.server.ts
import { fail, redirect } from '@sveltejs/kit';
import type { Actions } from './$types';

export const actions: Actions = {
  default: async ({ request, cookies }) => {
    const data = await request.formData();
    const email = data.get('email');
    const password = data.get('password');

    // Validate credentials (your auth logic here)
    const user = await authenticateUser(email, password);
    if (!user) {
      return fail(401, { message: 'Invalid credentials' });
    }

    // Set the session cookie
    cookies.set('sessionId', user.sessionToken, {
      httpOnly: true,
      secure: true,
      sameSite: 'lax',
      path: '/',
      maxAge: 60 * 60 * 24 * 7 // 7 days
    });

    redirect(303, '/dashboard');
  }
};

Notice that cookies.set returns nothing—it's a side effect. The SvelteKit runtime handles sending the Set-Cookie header to the browser. If you call cookies.set multiple times in the same request, all cookies are batched into the response.

Reading cookies is just as straightforward: const token = cookies.get('sessionId'). But the real security comes from how you configure that cookie.

httpOnly and secure flags: protecting against XSS

The httpOnly flag is your first line of defense against cross-site scripting (XSS) attacks. When set to true, JavaScript running in the browser cannot access the cookie. This means if an attacker injects malicious script, they cannot steal your authentication tokens.

Here's what happens without httpOnly:

typescript
// Vulnerable: no httpOnly
cookies.set('sessionId', token, {
  path: '/'
});

// Attacker's injected script can do this:
// const stolen = document.cookie; // Gets sessionId!

With httpOnly: true, that attack fails silently—the browser simply doesn't expose the cookie to JavaScript.

The secure flag ensures the cookie is only sent over HTTPS. In development with localhost, SvelteKit relaxes this requirement, but in production, always set secure: true:

typescript
cookies.set('sessionId', token, {
  httpOnly: true,
  secure: true, // Only sent over HTTPS
  path: '/',
  sameSite: 'lax'
});

Without the secure flag, a man-in-the-middle attacker on unencrypted WiFi can intercept the cookie during transmission. This is especially critical for authentication tokens.

A common pattern in SvelteKit applications is to use environment-based configuration for development:

typescript
const isProduction = process.env.NODE_ENV === 'production';

cookies.set('sessionId', token, {
  httpOnly: true,
  secure: isProduction, // true in prod, false locally
  path: '/',
  sameSite: 'lax'
});

Why path must be set explicitly

The path attribute determines which routes can access a cookie. If you don't set it, the browser defaults to the path where the cookie was set. This can lead to surprising behavior.

Imagine you set a cookie from the /admin/login route without specifying path:

typescript
// Bad: path not set, defaults to /admin/login
cookies.set('sessionId', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'lax'
});

// The cookie only gets sent to:
// /admin/login
// /admin/login/nested
// But NOT to /dashboard or /

Your app breaks because the session cookie isn't available on other routes. The fix is explicit:

typescript
// Good: path set explicitly
cookies.set('sessionId', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'lax',
  path: '/' // Available to entire app
});

In nearly all cases, you want path: '/' for application-wide cookies like sessions. There are rare exceptions—say you have a multi-tenant app and want to isolate cookies per tenant path—but those require deliberate design.

A practical rule: always set path explicitly. Don't rely on defaults. The few seconds it takes prevents debugging sessions later.

sameSite to prevent CSRF attacks

The sameSite attribute protects against Cross-Site Request Forgery (CSRF). It controls whether the browser sends the cookie when a request originates from a different site.

There are three values: strict, lax, and none.

sameSite: 'strict' never sends the cookie in cross-site requests. This is safest but can break legitimate flows. For example, if a user clicks a link from an email to your site, the cookie isn't sent on that initial request:

typescript
cookies.set('sessionId', token, {
  httpOnly: true,
  secure: true,
  path: '/',
  sameSite: 'strict' // Most restrictive
});

sameSite: 'lax' is the sweet spot. It sends the cookie on top-level navigation (like following a link) but not on embedded requests (like form submissions from another site). This prevents CSRF while allowing normal browser flow:

typescript
cookies.set('sessionId', token, {
  httpOnly: true,
  secure: true,
  path: '/',
  sameSite: 'lax' // Good default
});

sameSite: 'none' sends the cookie everywhere, but requires secure: true. Use this only when you intentionally want cross-site access (like embedded widgets). It's rare:

typescript
cookies.set('sessionId', token, {
  httpOnly: true,
  secure: true, // Required when sameSite is 'none'
  path: '/',
  sameSite: 'none'
});

For authentication cookies, sameSite: 'lax' is the standard. It balances security with usability.

Common mistakes

  • Forgetting httpOnly on auth cookies: This is the most common mistake. Without it, any XSS vulnerability exposes your tokens. Always set httpOnly: true for session and auth cookies.
  • Setting secure: false in production: Environment-check this carefully. A cookie with secure: false in production is a security vulnerability waiting to happen.
  • Omitting path: Relying on default behavior creates hard-to-debug issues. Explicit is better than implicit.
  • Using sameSite: 'none' by default: Only use none when you have a specific cross-site requirement. lax is the right default.
  • Setting cookies on the client: Cookies set from browser code (via JavaScript) are automatically httpOnly: false and secure: false in many contexts. Use server-side actions and loads instead.
  • Not handling cookie expiration: Set maxAge or expires to avoid stale sessions. For sensitive tokens, shorter lifespans are better.

Key takeaways

  • Use cookies.set and cookies.get only in server-side code: load functions and form actions.
  • Always set httpOnly: true and secure: true for authentication and session cookies to prevent XSS and man-in-the-middle attacks.
  • Explicitly set path: '/' for application-wide cookies; don't rely on browser defaults.
  • Use sameSite: 'lax' as the default for auth cookies to prevent CSRF while preserving normal navigation.
  • Handle cookie expiration with maxAge or expires to manage session lifetimes.
  • Test your cookie configuration in both development and production environments; behavior differs, especially around the secure flag.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles