Not identical but close enough
If you’ve been working with the pre-1.0 version of SvelteKit you may have used the session
store.
This provided a framework-sanctioned way to populate and consume session data in your app, with defined methods to populate the data and some neat auto-refetching of any data that relied on the session.
Unfortunatelty, this inbuilt SvelteKit session mechanism has been removed. I don’t agree with all the reasoning behind it, especially anything based on “some people who didn’t read the docs became confused” as that can be applied to any and every feature (routing - I’m looking at you!), but it’s gone. When it comes to session handling, it’s now a “build your own adventure” … so how?
There are a few different approaches. Before we look at them, let’s establish that we want the session to be cookie based so requests to the server can be authenticated and permissions applied, plus we also want the session data to be available on the client so we can adjust the UI accordingly (even if it’s just to show the user’s authentication status). It makes sense for the client-side session to be a Svelte Store so our UI can be reactive to it changing.
We’ll hook into the requests on the server using the “hooks” feature:
import type { Handle } from '@sveltejs/kit'
import { getUser } from '$lib/session'
export const handle: Handle = async ({ event, resolve }) => {
const { cookies, locals } = event
locals.user = null // default if session cookie fails
// decode the cookie and get the session property
const session = cookies.get('session')
locals.user = session ? getUser(session) : null
return resolve(event)
}
The getUser
function could decode the session data, or fetch the user using the session as an id.
The locals.user
property can be the full user data on the server, and we may return just a subset of it to the client.
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.
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.
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.