Search code examples
sessioncachingsveltecloudflaresveltekit

How to manage caching pages when they contain user specific data (with a CDN)


I am attempting to manage cache on my sveltekit website. The Env:

  • Sveltekit 1.20.4
  • Svelte 4.0.5
  • adapter-node
  • Nginx reverse proxy
  • Cloudflare in front of it all

I manage sessions with a session id cookie that fetches user session data from redis on every external request. On internal requests (ie svelte's special fetch()), session information is forwarded to locals inside the hooks.server.js's handleFetch(). Note that no data the client requests comes from anywhere else but the site itself, it's a monolith.

All pages on the site have user specific information in them, for example on the nav bar there's a user card if they are logged in, and most pages have admin moderation tools. Pure data is fetched from api endpoints, which I set cache-control headers for.

So how do I use user specific data in my home page and still be able to let Cloudflare cache most of it?

And if that's not possible, then how do I tell upstream (CF and the client) that the page can be cached when no session ID cookie is set?

And lastly, a third option might be to create various api endpoints for user specific data and request it after the page loads in onMount(), which I guess would work but adds an extra request. Maybe someone can elaborate on how this would work better.


Solution

  • For anyone wondering how to do this given a setup similar to mine, which is:

    • sveltekit backend
    • httpOnly cookie sessions with only session id as value
    • fetching sessions server side using said session id
    • cloudflare in front of the site with caching enabled
    • need for public cache of most pages on the site
    • need for user specific data on most pages

    Considering these constraints, high traffic pages need to be user-agnostic to be cached by cloudflare, which forces us to fetch() user data from an endpoint after the page has loaded.

    So, first create a session store for front-end only, this will contain all information the client might need for rendering user cards etc. Do not include sensitive session data like password and session id itself because it defeats the purpose of having httpOnly cookies!

    session.js

    import { writable } from 'svelte/store';
    import { invalidate } from '$app/navigation';
    import { browser } from '$app/environment';
    
    export const refetch = writable(true);
    export const session = writable({}); // This default user object can be different
    
    if (browser) {
        refetch.subscribe(b => {
            if (!b) return;
            
            session.set({});
            invalidate('data:session'); // Can be invalidateAll() also
        });
    }
    
    export default session;
    

    Now in your root +layout.svelte

    import { session, refetch } from '$lib/stores/session';
    import { afterNavigate } from '$app/navigation';
    
    afterNavigate(async () => { // Add your error handling...
        if (!$refetch) return;
        $refetch = false;
    
        const req = await fetch('/api/session'); // Put your own session endpoint and headers
        const res = await req.json();
        console.log('Session', res);
    
        $session = res;
    });
    

    I won't provide any specifics for the session/+server.js endpoint, you can implement it however you like.

    Whenever $refetch is set to true it will clear the current session store and fetch it from your api after the page finishes navigating. When the page first loads, $refetch is true and it will get the user without a trigger. Any load functions you want to rerun, like pages that require login, add depends('data:session') inside them or, instead of using invalidate('data:session') a simpler more heavy handed approach would be to invalidateAll().

    In your login and register pages, be sure to set $refetch = true when submitting the form in your use:enhance function body.

    This also works with oauth2 redirects since the client will load the page once (only) after the backend has returned a final location/route.

    For added control, you may add a secondary cookie such as cf-nocache for admin users and set a cache rule in your CF panel that bypasses cache when the cookie exists. This will prevent admin versions of pages from being public cached without having to set complicated headers in your routes.

    Final thoughts - Keep in mind that the session store will always be empty on the server side when SSR runs. This will return a user-less page and the client is the one hydrating this user store. If you need to use session data on the server, add your user session to the locals object.