Captain Codeman Captain Codeman

Re-creating the SvelteKit Session Store

Not identical but close enough

Contents

LayoutData + invalidateAll()

The suggested replacement to the original session store relies on the new LayoutData and invalidateAll() mechanism.

Basically you add the session information to your /+layout.server.ts load function, so it’s then available in the root LayoutData type (and will also be in the page app store which can be typed as App.PageData in /src/app.d.ts).

/src/routes/+layout.server.ts

import type { LayoutServerLoad } from './$types'
import { getSession } from '$lib/session'

export const load: LayoutServerLoad = async ({ locals }) => {
  const { user } = locals
  const session = getSession(user)

  // load could also return additional data
  // other than the session, such as site config
  return { session }
}

Again, the getSession function can be as simple or complex as you want - for example, selecting a subset of the data to return:

// session is a subset of the user object in this example
function getSession(user: User | null) {
  if (user) {
    const { id, name, email, roles } = user
    return { user: { id, name, email, roles } }
  }
  return { user: null }
}

We can now reference this data in our /+layout.svelte template for a rudimentary auth status system:

<script lang="ts">
  import { invalidateAll } from '$app/navigation'
  import type { LayoutData } from './$types'

  export let data: LayoutData
  
  function signOut() {
    // DELETE /session endpoint to clear session cookie
    invalidateAll()
  }
  
  function signIn() {
    // POST /session endpoint to set session cookie
    invalidateAll()
  }
</script>

{#if data.session.user}
  Welcome {data.session.user.name}
  <button on:click={signOut}>Sign out</button>
{:else}
  Welcome visitor
  <button on:click={signIn}>Sign in</button>
{/if}

<slot />

The actual authentication mechanism would exist in the /session endpoint, which would also set or clear the cookie we read in our hooks handle method earlier.

So this works, but there are a couple of downsides.

Because we’re invalidating everything we re-load everything, not just our session. This may be appropriate for what you need, or it may not. I often find I have a lot of site-related data that is otherwise static and I know it’s not going to change - I’d rather it not reload. There are some more elaborate dependency controls for data loading which I’ve not been able to use yet which may lessen the impact of this.

Also, we are now making two requests. One to actually update the session, which we need to wait for, and then the invalidate call which effectively tells it to re-load the data we just sent.

All together it’s a little clumsy and slower and less efficient than it could be.

But we do have session data that updates on the client, and we can subscribe to the page store for the parts of our app that need to be reactive, but be careful with the page store updates on navigation gotcha which can lead to re-calcs and re-renders.

Custom session store

Personally, I like the original approach where the session store alone could be updated in a more surgical manner. I felt it fit better with the whole ethos of Svelte itself.

So how can we achieve that?

I’ve settled on creating my own store that I can then import wherever I need it. The trick is that we’re only going to use the LayoutData for the initial setting of the store data but with a way for us to override it on the client with the result of the call to the /session enpoint without having to invalidate() and re-load the LayoutData

Here’s the store part of it:

import { derived, writable } from 'svelte/store'
import { browser } from '$app/environment'
import { page } from '$app/stores'
import { dedupe } from './dedupe'

// internal store allows us to override the page data session without having to invalidate LayoutData
const internal = writable()

// derived store from page data for initial session data
export const external = dedupe(derived(page, $page => $page.data.session))

// derived store to handle "if overridden, otherwise default"
export const session = derived([internal, external], ([$internal, $external]) => $internal || $external)

It looks a little funky but what it’s doing is quite simple - on initial render, the internal store will be undefined so the session store will return the $page.data.session property from the page store. Anytime we sign-in or sign-out and call the /session endpoint, we set the internal store to the result. From then on, anything using the $session store will see that data.

The nice thing about this is that the client-part of the session handling is totally self-contained and encapsulated in one place. We don’t need to add anything to the root layout and anything that needs to use session will be importing session, which is cleaner and communicates the intent better IMO than importing page from $app/stores and then just happening to use a session property of it.

I’m using this with Firebase Authentication where the sign-in and sign-out are handled by the client-side firebase sdk, and the onAuthStateChanged or onIdTokenChanged listeners can handle the call to sync things up and update the store.

The session cookie is validated and set using the firebase-admin SDK and we then have an easy way to check calls on the server and can also server-side render pages, even if they include firebase-user specific information for that “fast first page load” experience.

firebase auth with server-side cookies and custom session store

Checkout the sveltekit-example for a more complete version of the code and let me know what you think of this approach in the comments below.