How to setup Supabase Auth with Sapper SSR
Tags: svelte, webdev, supabase, tutorial
Published: 2021-02-16 01:19:45

**Hey! You! This tutorial uses an old version of Supabase and also uses Sapper which is basically dead by now. If you are considering using Svelte and Supabase, look at the [SvelteKit adder](https://github.com/supabase-community/svelte-supabase) instead.**

As a developer who quite recently discovered [Svelte](https://svelte.dev) (and now likes it a bunch), I decided to make my portfolio and a new (secret as of now) side-project using [Sapper](https://sapper.svelte.dev), its little framework. Both of these project require authentication and I thought to myself, why not do it all with server-side instead of client-side like many tutorials show with [Firebase](https://firebase.google.com).

However, I will be using [Supabase](https://supabase.io) instead, as I prefer supporting open-source software, and honestly, it's also super nice and easy to work with! As a matter of fact, [my previous post](https://dev.to/jakobbouchard/setting-up-umami-with-vercel-and-supabase-3a73) featured it as the database for Umami analytics.

I'll be honest here, I hit quite a few roadblocks during the process, but in the end, all was well and I actually learned a bunch of new stuff!

I will be including explanations to both the functions and reasons why I decided to do it in a certain way in collapsibles as I am not sure everyone will need them, and they also might get quite long.

The project's code is also [available here](https://github.com/jakobbouchard/sapper-supabase-auth).

# Step 1: Setting up Sapper
We'll be basing ourselves off the Sapper template, as it will make our job easier. To setup Sapper, just run these commands:

```zsh
npx degit "sveltejs/sapper-template#rollup" sapper-supabase-auth
npm i
```

Now that the required packages are installed, let's install the other packages we'll need, since there's quite a few of them. We can also uninstall `polka`, since we'll replace it with `express`. I'll explain why later in the tutorial.

```zsh
npm r polka
npm i express@latest cookie-parser body-parser @supabase/supabase-js
```

Now open `server.js`, where we'll change every mention of `polka` by `express`:

```javascript
// server.js

import sirv from 'sirv';
import express from 'express'; // HERE
import compression from 'compression';
import * as sapper from '@sapper/server';

const { PORT, NODE_ENV } = process.env;
const dev = NODE_ENV === 'development';

express() // AND HERE
	.use(
		compression({ threshold: 0 }),
		sirv('static', { dev }),
		sapper.middleware()
	)
	.listen(PORT, err => {
		if (err) console.log('error', err);
	});

```

You can now run `npm run dev` and open `localhost:3000` in your browser. You should be greeted by this:

![Sapper template homepage](https://dev-to-uploads.s3.amazonaws.com/i/u8f5kkpik8usf5dy9q2r.png)

{% details Explanation %}
There isn't much explanation required here, however, we did change `polka` for `express` for a reason. `express`' response isn't the same and since we use cookies, I preferred using it since it has an included `res.cookie()` function, that `polka` doesn't have. If you decide to not swap them, you will either need to include a new package to create cookies or create your own cookie file.
{% enddetails %}

# Step 2: Creating the dashboard page and the authentication page

Now that we confirmed that Sapper still works, we can go ahead and create the files we'll be needing for this tutorial. First off, we can go ahead and delete the `blog` folder as well as rename the `about.svelte` file to `dashboard.svelte`. The `index.svelte` file will act as a authentication page for simplicity.

In `components/Nav.svelte`, we can replace the existing links with our new links:

```html
<nav>
	<ul>
		<li><a aria-current="{segment === undefined ? 'page' : undefined}" href=".">home</a></li>
		<li><a aria-current="{segment === 'dashboard' ? 'page' : undefined}" href="dashboard">dashboard</a></li>
	</ul>
</nav>
```

## Authentication and dashboard layout

For the authentication page, we will simply create 2 forms: one for the sign in and another for the sign up.

```html
<!-- index.svelte -->

<script></script>

<style>
	* {
		margin: 0 auto;
	}

	section:not(:last-child) {
		margin-bottom: 4em;
	}

	h1 {
		margin: 0.5em 0;
		font-size: 2.8em;
		font-weight: 700;
		text-align: center;
		text-transform: uppercase;
	}

	form {
		width: 50%;
	}

	input:not([type="checkbox"]) {
		display: block;
		width: 100%;
		margin-bottom: 0.25em;
		padding: 0.5em;
		border-radius: 0.5em;
		border: 1px #999 solid;
	}

	label {
		display: block;
	}

	button {
		width: 100%;
		margin-top: 1em;
		padding: 0.5em;
		border-radius: 0.25em;
		border: 2px rgb(255,62,0) solid;
		background-color: rgba(255,62,0,0.1);
		color: rgb(255,62,0);
		font-size: medium;
		transition: background-color 0.1s ease-in-out;
	}

	button:hover {
		background-color: rgba(255,62,0,0.2);
		cursor: pointer;
	}

</style>

<svelte:head>
	<title>Sapper Supabase Auth</title>
</svelte:head>

<section>
	<h1>Sign in</h1>
	
	<form>
		<input id="email" type="email" placeholder="Email address">
		<input id="password" type="password" placeholder="Password">
		<label for="remember">
			<input id="remember" type="checkbox">
			Remember me
		</label>
		<button type="submit">Sign in</button>
	</form>
</section>

<hr>

<section>
	<h1>Sign up</h1>
	
	<form>
		<input id="email" type="email" placeholder="Email address">
		<input id="password" type="password" placeholder="Password">
		<label for="remember">
			<input id="remember" type="checkbox">
			Remember me
		</label>
		<button type="submit">Sign up</button>
	</form>
</section>
```

As for the dashboard, we will simply have a title and a sign out button.

```html
<!-- index.svelte -->

<script context="module"></script>

<script></script>

<style>
	* {
		margin: 0 auto;
	}

	h1 {
		margin: 0.5em 0;
		font-size: 2.8em;
		font-weight: 700;
		text-align: center;
		text-transform: uppercase;
	}

	button {
		width: 100%;
		margin-top: 1em;
		padding: 0.5em;
		border-radius: 0.25em;
		border: 2px rgb(255,62,0) solid;
		background-color: rgba(255,62,0,0.1);
		color: rgb(255,62,0);
		font-size: medium;
		transition: background-color 0.1s ease-in-out;
	}

	button:hover {
		background-color: rgba(255,62,0,0.2);
		cursor: pointer;
	}
</style>

<h1>Our very cool dashboard</h1>

<button>Sign out</button>
```

Our pages should now look like this:
![Authentication page](https://dev-to-uploads.s3.amazonaws.com/i/9ra3oxaj9myd6uwfduki.png)
![Dashboard](https://dev-to-uploads.s3.amazonaws.com/i/9wj30nok85ngyy47312h.png)

# Step 3: Securing the dashboard

To secure the dashboard, we will be using Sapper's session store, which we can access with the Sapper middleware in the `server.js` file. For now, we'll only return false, but later we will populate it with the user's access token.

```javascript
// server.js

express()
	.use(
		compression({ threshold: 0 }),
		sirv('static', { dev }),
		sapper.middleware({
			session: async (req, res) => {
				return { userToken: false };
			}
		})
	)
	.listen(PORT, err => {
		if (err) console.log('error', err);
	});
```

In `dashboard.svelte`, we can make use of Sapper's `preload` function, which we can access in the `module` context. We can simply check if the userToken exists in the session, and if it doesn't, we redirect to the authentication page.

```html
<!-- dashboard.svelte -->

<script context="module">
	export async function preload(page, session) {
		let { userToken } = session;
		if (!userToken) return this.redirect(302, '/');
		return userToken;
	}
</script>
```

If you now try to access the dashboard page, either by the navbar or by directly entering the link, it should redirect you to the authentication page.

{% details Explanation %}
Sapper has an included session/store ([learn more about stores](https://svelte.dev/tutorial/writable-stores)) that is easily accessible via the middleware in the server. For now, we simply make it an object with a `userToken` property that we set to `false`. In our dashboard, we use Sapper's included [`preload`](https://sapper.svelte.dev/docs#Preloading) function, where we check the session's `userToken`. If it doesn't exist/is equal to `false`, we redirect the user to the authentication page. If it does exist, we return the token.
{% enddetails %}

# Step 4: Creating the authentication functions

## Supabase setup

Head over to [Supabase](https://app.supabase.io) to create your project. Once your project is created, go in the settings, then in in the API section, where you'll be able to find your project URL and key:

![Supabase API Settings](https://dev-to-uploads.s3.amazonaws.com/i/6ov61d4rvkttce4djyy6.png)

Before creating our functions, we should start by creating our Supabase client. In the `src` folder, we will create a simple file called `supabase.js` that we will import every time we need to use Supabase. I highly suggest putting the Supabase URL and anonymous key in environment variables, for better security.

```javascript
// supabase.js

import { createClient } from '@supabase/supabase-js';

const supabase = createClient('https://xyzcompany.supabase.co', 'public-anon-key');

export default supabase;
```

## API endpoints and authentication function

We will now create our internal API endpoint, where we will be able to sign in and sign up. Later, we will also create one where we sign out.

In the `src` folder, create a folder named `api`, and inside it, create a file called `auth.js`. This file will not be accessible via the browser, and only with our server, by using, for example, the `fetch` functions.

```javascript
// api/auth.js

import supabase from '../../supabase';

export async function post(req, res) {
	if (req.method == 'POST') {
		const { email, password, remember, authType } = req.body;
		let userData;

		switch (authType) {
			case 'signin':
				userData = await supabase.auth.api.signInWithEmail(email, password);
				break;

			case 'signup':
				userData = await supabase.auth.api.signUpWithEmail(email, password);
				break;
		}

		const { data, error } = userData;

		if (error) {
			console.log('Error signing in: ', error.name, error.message);
			return res.end(JSON.stringify({ success: false }));
		}

		if (data) {
			const tokenExpires = remember ? new Date(Date.now() + data.expires_in * 1000) : 0;
			const refreshExpires = remember ? new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) : 0;
			const cookieOptions = { httpOnly: true, secure: false, sameSite: 'strict' };

			res.cookie('supaToken', data.access_token, { expires: tokenExpires, ...cookieOptions });
			res.cookie('supaRefresh', data.refresh_token, { expires: refreshExpires, path: '/api/refresh', ...cookieOptions });
			if (remember) {
				res.cookie('supaRemember', 1, { expires: refreshExpires, ...cookieOptions });
			}

			return res.end(JSON.stringify({ success: true }));
		}

		return res.end(JSON.stringify({ success: false }));

	}
}
```

In our `index.svelte`, we can then hook up our forms to a function that will call this API endpoint and redirect us to the dashboard if it works.

```html
<script>
async function authenticate(event, authType) {
	const { email, password, remember } = event.target.elements;

	const authAPI = await fetch('/api/auth', {
		method: 'POST',
		credentials: 'same-origin',
		body: JSON.stringify({
			email: email.value,
			password: password.value,
			remember: remember.checked,
			authType: authType
		}),
		headers: {
			'Content-Type': 'application/json'
		}
	});
	const { success } = await authAPI.json();
	if (success) {
		window.location.href = '/dashboard';
	}
}
</script>

<!-- Everything here stays the same -->

<section>
	<h1>Sign in</h1>
	
	<form on:submit|preventDefault={ (e) => authenticate(e, 'signin') } >
		<input id="email" type="email" placeholder="Email address">
		<input id="password" type="password" placeholder="Password">
		<label for="remember">
			<input id="remember" type="checkbox">
			Remember me
		</label>
		<button type="submit">Sign in</button>
	</form>
</section>

<hr>

<section>
	<h1>Sign up</h1>
	
	<form on:submit|preventDefault={ (e) => authenticate(e, 'signup') } >
		<input id="email" type="email" placeholder="Email address">
		<input id="password" type="password" placeholder="Password">
		<label for="remember">
			<input id="remember" type="checkbox">
			Remember me
		</label>
		<button type="submit">Sign up</button>
	</form>
</section>
```

We use `on:submit|preventDefault` as it, well, prevents the page from refreshing on submit of the file.

The last part here is to implement a way for the session to know when the user is authenticated and also a way for the server to understand JSON, as that is what we send when we call the API. We can do that in the `server.js` file.

```javascript
// server.js

import sirv from 'sirv';
import express from 'express';
import compression from 'compression';
import * as sapper from '@sapper/server';

import cookieParser from 'cookie-parser';
import { json } from 'body-parser';
import supabase from './supabase';

const { PORT, NODE_ENV } = process.env;
const dev = NODE_ENV === 'development';

express()
	.use(cookieParser())
	.use(json())
	.use(
		compression({ threshold: 0 }),
		sirv('static', { dev }),
		sapper.middleware({
			session: async (req, res) => {
				// If there's a JWT, check if it's valid
				const results = await supabase.auth.api.getUser(req.cookies['supaToken']);
				if (results.user) {
					return { userToken: req.cookies['supaToken'] };
				}
				return { userToken: false };
			}
		})
	)
	.listen(PORT, err => {
		if (err) console.log('error', err);
	});
```

{% details Explanation %}
For the Supabase client, it's really simple. Using our project settings, we use Supabase's client library to create ourselves a client, that we can then use anywhere in our code to connect to our project.

For the `auth.js` file, things get a bit more busy. First of all, we check if the request was really made using `POST`. If it was, we extract the values we need to authenticate from `req.body`, which we can use to check what type of authentication we are processing. Since the sign up and sign in functions both return the same data, we can use the same code to process them! Once we get the data, we check if there's any error, and if there is, we send the response back to the file that requested it. If there isn't any error, we check for data, which we then process a bit. First, we need to set the expiry times for the cookies we'll create. Using a ternary operator, we verify if the user checked the *Remember me* box. If they did, we set the expiry time of the access token to 1 hour, and the refresh token's to 30 days. If they didn't we set it to zero, meaning it'll get erased when the browser closes. The cookie options we created basically mean that our cookie can only be accessed by the web server (`httpOnly`) and not JavaScript, that it can be accessed on non-HTTPS websites (be sure to change `secure` to `true` on production websites) and that it can only be accessed if the site for the cookie matches the site currently shown in the browser's URL bar. We then send the response back to the page, with the new cookies and the `success` set to `true`.

The `index.svelte` page executes the `authenticate()` function when either of the form buttons are pressed. It gets the `event` from the form and sets a custom parameter called `authType` that gets used by our API. It then fetches the result, with a JSON payload containing the entered email and password, the *Remember me* preference and the `authType`. If the response is a success, it redirects the user to the dashboard.

The `server.js` file got some additions too. First of all, we needed to import our new Supabase client, as well as the `json()` function from `body-parser`, and `cookie-parser`. The `json()` function enables us to read the `req.body` correctly. Instead of automatically setting the `userToken` to `false`, we can now check if the token in the cookie (thanks to `cookie-parser`) is valid using Supabase's `auth.api.getUser()` function. This function returns a user property if the passed token is valid. If the cookie is valid, we put the token in the session.
{% enddetails %}

You can now try authenticating and you should be redirected to the dashboard. You should also be able to see the cookies in your developer tools.

# Step 5: Sign out function

We will now create our the `signout` API endpoint. In the `api` folder, create a file named `signout.js`. This file, like the others, will not be accessible via the browser, and only with our server. The contents of this file will be similar to the other one.

```javascript
// api/signout.js

import supabase from '../../supabase';

export async function post(req, res) {
	if (req.method == 'POST') {
		const { supaToken } = req.cookies;
		const { error } = await supabase.auth.api.signOut(supaToken);

		if (error) {
			console.log('Error signing out: ', error.name, error.message);
			return res.end(JSON.stringify({ success: false }));
		}

		res.clearCookie('supaToken');
		res.clearCookie('supaRefresh', { path: '/api/refresh' });
		res.clearCookie('supaRemember');
		return res.end(JSON.stringify({ success: true }));
	}
}
```

And now for our dashboard page.

```html
<script context="module">
	export async function preload(page, session) {
		let { userToken } = session;
		if (!userToken) return this.redirect(302, '/');
		return userToken;
	}
</script>

<script>
async function signOut() {
	const authAPI = await fetch('/api/signout', {
		method: 'POST',
		credentials: 'same-origin',
	});
	const { success } = await authAPI.json();
	if (success) {
		window.location.href = '/';
	}
}
</script>

<!-- Everything here stays the same -->

<h1>Our very cool dashboard</h1>

<button on:click="{ signOut }">Sign out</button>
```

{% details Explanation %}
For the `signout` endpoint, we get the `supaToken` from the request's cookies. Then, we sign out using Supabase's included methods. If all goes well, we clear all of the cookies (specifying the path is needed in order to clear the cookie) and we return `success` as `true`.

In our dashboard, the `signOut()` function is quite similar to the `authenticate()` in our login page. The main difference is that it uses our `signout` endpoint, and it doesn't require a body, as it gets the token from the cookies.
{% enddetails %}

# Step 6: Keeping the user signed in

Now our users can log in, which is great, but the access token (which is what we use to detect if the user is logged in) expires after 1 hour, which is gonna be a problem if our users either stay on the website for more than 1 hour or if they leave the website and then come back.

This step is a bit more complex than the others, or at least it felt like it when I was figuring it out. This method will refresh the access token every 55 minutes, making sure it never expires, and if the user accesses the page with an expired token, it will also try to refresh it if possible. However, the only issue that I haven't figured out yet is how to make it work on protected pages correctly. So if somebody figures it out, I'd like to know how you did it!

To begin this, we will create yet another API endpoint, called `refresh.js`. This file will contain this code:

```javascript
// api/refresh.js

import supabase from '../../supabase';

export async function post(req, res) {
	if (req.method == 'POST' && req.cookies['supaRefresh']) {
		const refreshToken = req.cookies['supaRefresh'];
		const remember = req.cookies['supaRemember'];
		const { data, error } = await supabase.auth.api.refreshAccessToken(refreshToken);

		if (error) {
			console.log('Error refreshing token: ', error.name, error.message);
			return res.end(JSON.stringify({ supaToken: false }));
		}

		if (data) {
			const tokenExpires = remember ? new Date(Date.now() + data.expires_in * 1000) : 0;
			const refreshExpires = remember ? new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) : 0;
			const cookieOptions = { httpOnly: true, secure: false, sameSite: 'strict' };

			res.cookie('supaToken', data.access_token, { expires: tokenExpires, ...cookieOptions });
			res.cookie('supaRefresh', data.refresh_token, { expires: refreshExpires, path: '/api/refresh', ...cookieOptions });
			if (remember) {
				res.cookie('supaRemember', 1, { expires: refreshExpires, ...cookieOptions });
			}

			return res.end(JSON.stringify({ supaToken: data.access_token }));
		}

		return res.end(JSON.stringify({ supaToken: false }));
	}
}
```

Now, since the code is extremely similar to our authentication code, we could just use the same endpoint, but I prefer keeping it separated due to the refresh token being a bit more sensitive info.

This code will be called in `_layout.svelte` as the functions in this file are executed on every page.

```html
<!-- _layout.svelte -->

<script>
	import { onMount } from 'svelte';
	import { stores } from '@sapper/app'
	const { session } = stores()
	import Nav from '../components/Nav.svelte';

	export let segment;

	onMount(async () => {
		if (!$session.userToken) {
			const refresh = await fetch(`/api/refresh`, {
				method: 'POST',
				credentials: 'same-origin'
			});
			const { supaToken } = await refresh.json();
			if (supaToken) {
				return session.set({ userToken: supaToken });
			}
			return;
		}

		// refreshes token every 55 minutes to also sync with server-side.
		setInterval(async () => {
			const refresh = await fetch(`/api/refresh`, {
				method: 'POST',
				credentials: 'same-origin'
			});
			const { supaToken } = await refresh.json();
			if (supaToken) {
				return session.set({ userToken: supaToken });
			}
			console.log('No user, timeout will be killed eventually')
		}, 1000 * 60 * 55);
	});
</script>
```

{% details Explanation %}
The `refresh` endpoint is basically identical to our `auth` endpoint, so I will not be explaining it in details. The main difference here is that we return the `userToken` instead of the `success`.

Our `_layout.svelte` code is a bit more complex however. It uses Svelte's [`onMount()`](https://svelte.dev/tutorial/onmount) method to execute the code once the components appears in the DOM. It also gets the session from the Svelte's `stores`. In the `onMount()`, we check if the userToken exists. If it doesn't we try to refresh it using our new endpoint. We then use the returned token to update our session with the new, valid, access token. The cookies have already been set by the `refresh` endpoint, so they're in sync. The interval at the bottom makes sure that a new access token gets generated every 55 minutes, so that it never invalidates, preventing our users from getting errors.
{% enddetails %}

# We're done 🎉

And that's it! We are officially done with this and everything should work well! We now have a secure authentication flow that doesn't use client-side code using Sapper and Supabase. Stay tuned, as I will definitely make more tutorials using these 2 technologies soon(™).

If you want to check out my projects, you can go to my [portfolio](https://jakobbouchard.dev) or my [GitHub](https://github.com/jakobbouchard). *At the time of writing, my portfolio is getting remade from scratch as I don't like my current one.*

\* Supabase is free for now, but it will become paid in the future. However, if you sign up during the beta you get free credits!

If you have any questions or just want to say hello, leave a comment and I’ll be happy to reply! And obviously, if I missed something, tell me, I will credit you for the fix!