DEV Community

Cover image for SvelteKit with Supabase SSR Auth Helpers
kvetoslavnovak
kvetoslavnovak

Posted on • Edited on

SvelteKit with Supabase SSR Auth Helpers

TL;DR

https://github.com/kvetoslavnovak/SvelteKitSupabaseAuthApp

EDIT: Supabase released new ssr package replacing Auth Helpers. You can follow respective tutorial here.

What are Auth Helpers?

Supabase has recently announced the revamped auth helpers for Supabase with SvelteKit support.

According to Supabase one of the challenges has been creating a simple experience for server-side rendering (SSR) environments. Auth Helpers are a collection of framework-specific utilities for Supabase Auth. They allow you to implement secure applications with little effort. These libraries include functions for protecting API routes and pages in your applications.

How to use Supabase Auth Helpers in SvelteKit Projet?

Just install these two npm packages in your SvelteKit project:

npm install @supabase/auth-helpers-sveltekit
npm install @supabase/auth-helpers-svelte
Enter fullscreen mode Exit fullscreen mode

Detailed Tutorial

Here is my github repo of the tutorial.

Set Up SvelteKit Project

We will try to have a minimal bare bone project. You are free to add any styling, type checking etc later. Supabase published quite nice examples using TypeScript and the documentation.

Let name the project SvelteKitSupabaseAuthApp.
Just use the Skeleton project, no TypeScript, no ESLint, no Prettier, no Playwright.

npm create svelte@latest SvelteKitSupabaseAuthApp
cd SvelteKitSupabaseAuthApp
npm install
Enter fullscreen mode Exit fullscreen mode

Install Supabase Auth Helpers for SvelteKit

npm install @supabase/auth-helpers-sveltekit
npm install @supabase/auth-helpers-svelte
Enter fullscreen mode Exit fullscreen mode

Set Up Supabase Project

  • Login in Supabase and create a new project on the Supabase dashboard.
  • Go to Authentication > Settings section and change User Sessions - Site URL from http://localhost:3000 to http://localhost:5173 and save. This is a localhost address where SvelteKit serves the project in development mode. This change was introduced with Vite 3.
  • Go to Settings > API section and get the URL and anon key of your project.
  • Go back to your SvelteKitSupabaseAuthApp project and in its root create a new .env file. Replace VITE_SUPABASE_URL with the URL from previous step and VITE_SUPABASE_ANON_KEY with anon key from previous step:
# .env
# Update these with your Supabase details from your project settings > API
VITE_SUPABASE_URL=https://your-project.supabase.co
VITE_SUPABASE_ANON_KEY=your-anon-key
Enter fullscreen mode Exit fullscreen mode

Supabase Client Creation

In src folder create a new folder lib and in this lib folder create a new file sb.js, here we create supabaseClient using our credentials from .env end export the supabaseClient so we can use its auth methods in our application.

We will use login with email in this app. Supabase sends confirmation email automatically when a user signs ups.

// lib/sb.js
import { createSupabaseClient } from '@supabase/auth-helpers-sveltekit';

const { supabaseClient } = createSupabaseClient(
    import.meta.env.VITE_SUPABASE_URL,
    import.meta.env.VITE_SUPABASE_ANON_KEY
);

export { supabaseClient };

Enter fullscreen mode Exit fullscreen mode

Hooks

In SvelteKit hook enables us to use two important functions, a handle function and a getSession function. The handle function runs every time the SvelteKit server receives a request. The getSession function takes the event object and returns a session object that is accessible on the client. This enables us to access data concerning user and cookies for example.

In src folder create a new file hooks.js

// src/hooks.js
import { handleAuth } from '@supabase/auth-helpers-sveltekit';
import { sequence } from '@sveltejs/kit/hooks';

export const handle = sequence(
    ...handleAuth({
        cookieOptions: { lifetime: 1 * 365 * 24 * 60 * 60 }
    })
);

export const getSession = async (event) => {
    const { user, accessToken } = event.locals;
    return {
        user,
        accessToken
    };
};

Enter fullscreen mode Exit fullscreen mode

Layout

In src/routes/ folder create a new layout file __layout.svelte so we can have a simple layout but more importantly to introduce our SupaAuthHelper component.

// src/routes/__layout.svelte
<script>
    import { session } from '$app/stores';
    import { supabaseClient } from '$lib/sb';
    import { SupaAuthHelper } from '@supabase/auth-helpers-svelte';
</script>

<svelte:head>
    <title>Email and Password Demo - Supabase Auth Helpers</title>
</svelte:head>

<SupaAuthHelper {supabaseClient} {session}>
    <main>
        <div>
            <h2>
                <a href="/">Supabase Auth Helpers Demo</a>
            </h2>

            <slot />

            <div>
                {#if $session?.user?.id}
                    <a href="/api/auth/logout">Sign out</a>
                {/if}
            </div>
        </div>
    </main>
</SupaAuthHelper>

Enter fullscreen mode Exit fullscreen mode

Sign up Page and Route

In src/routes/ folder create a new file signup,svelte Just a simple sing up form.

// src/routes/signup,svelte
<script>
    export let errors = null;
    export let values = null;
    export let message = null;
</script>

<section>
    <div>
        <h1>Sign up</h1>
        {#if errors}
            <div>{errors.form}</div>
        {/if}
        {#if message}
            <div>{message}</div>
        {/if}
        <form method="post">
            <div>
                <label for="email">Email</label>
                <p>
                    <input
                        id="email"
                        name="email"
                        value={values?.email ?? ''}
                        type="email"
                        placeholder="Email"
                        required
                    />
                </p>
            </div>
            <div>
                <label for="password">Password</label>
                <p>
                    <input
                        id="password"
                        name="password"
                        value={values?.password ?? ''}
                        type="password"
                        placeholder="Password"
                        required
                    />
                </p>
            </div>
            <div>
                <p>
                    <button>Sign up</button>
                </p>
            </div>
        </form>

        <div>
            <p>
                Already have an account? <a href="/">Sign in</a>
            </p>
        </div>
    </div>
</section>

Enter fullscreen mode Exit fullscreen mode

In src/routes/ folder create a new signup.js file. This endpoint points the existing user to /dashboard which is protected user only page or provides sing up data to supabaseClient. We are also adding object specifying redirect when client clicks confirmation link in a confirmation email that Supabase will send him/her.

// src/routes/signup.js
import { supabaseClient } from '$lib/sb';

export const GET = async ({ locals }) => {
    // if the user is already logged in, then redirect to the dashboard
    if (locals.user) {
        return {
            status: 303,
            headers: {
                location: '/dashboard'
            }
        };
    }
    return {
        status: 200
    };
};

export const POST = async ({ request, url }) => {
    const data = await request.formData();

    const email = data.get('email');
    const password = data.get('password');

    const errors = {};
    const values = { email, password };

    const { error } = await supabaseClient.auth.signUp(
        { email, password }, 
        { redirectTo: `${url.origin}/logging-in`}
    );

    if (error) {
        errors.form = error.message;
        return {
            status: 400,
            body: {
                errors,
                values
            }
        };
    }

    return {
        status: 200,
        body: {
            message: 'Please check your email for a confirmation email.'
        }
    };
};

Enter fullscreen mode Exit fullscreen mode

Log in (index) Page and Route

In src/routes/ folder replace index.svelte with this new one.Just a simple login form.

// src/routes/index.svelte
<script>
    export let errors;
    export let values;
</script>

<section>
    <div>
        <h1>Sign in</h1>
        {#if errors}
            <div>{errors.form}</div>
        {/if}
        <form method="post">
            <div>
                <label for="email">Email</label>
                <p>
                    <input
                        id="email"
                        name="email"
                        value={values?.email ?? ''}
                        type="email"
                        placeholder="Email"
                        required
                    />
                </p>
            </div>
            <div>
                <label for="password">Password</label>
                <p>
                    <input
                        id="password"
                        name="password"
                        value={values?.password ?? ''}
                        type="password"
                        placeholder="Password"
                        required
                    />
                </p>
            </div>
            <div>
                <p>
                    <button>Sign in</button>
                </p>
            </div>
        </form>

        <div>
            <p>
                Don't have an account? <a href="/signup">Sign up</a>
            </p>
        </div>
    </div>
</section>

Enter fullscreen mode Exit fullscreen mode

In src/routes/ folder create a new index.js file. This endpoint points existing user to /dashboard which is protected user only page or provides login data to supabaseClient, returned session is used for a cookie creation.

//src/routes/index.js
import { supabaseClient } from '$lib/sb';

export const GET = async ({ locals }) => {
    if (locals.user) {
        return {
            status: 303,
            headers: {
                location: '/dashboard'
            }
        };
    }
    return {
        status: 200
    };
};

export const POST = async ({ request, url }) => {
    const data = await request.formData();

    const email = data.get('email');
    const password = data.get('password');

    const headers = { location: '/dashboard' };
    const errors = {};
    const values = { email, password };

    const { session, error } = await supabaseClient.auth.signIn({ email, password });

    if (error) {
        errors.form = error.message;
        return {
            status: 400,
            body: {
                errors,
                values
            }
        };
    }

    if (session) {
        const response = await fetch(`${url.origin}/api/auth/callback`, {
            method: 'POST',
            headers: new Headers({ 'Content-Type': 'application/json' }),
            credentials: 'same-origin',
            body: JSON.stringify({ event: 'SIGNED_IN', session })
        });

        // TODO: Add helper inside of auth-helpers-sveltekit library to manage this better
        const cookies = response.headers
            .get('set-cookie')
            .split('SameSite=Lax, ')
            .map((cookie) => {
                if (!cookie.includes('SameSite=Lax')) {
                    cookie += 'SameSite=Lax';
                }
                return cookie;
            });
        headers['Set-Cookie'] = cookies;
    }
    return {
        status: 303,
        headers
    };
};


Enter fullscreen mode Exit fullscreen mode

Email confirmation redirect page

In src/routes/ folder create a new file logging-in@blank.svelte so we can check if the user redirected after email confirmation has been already set in session store. We will also provide a special layout for this case later on. During my tests the redirect and session store were quite fast so this page may be visible rather rarely.

src/routes/logging-in@blank.svelte
<script>
    import { session } from '$app/stores';
    import { goto } from '$app/navigation';
    import { page } from '$app/stores';

    let redirectPath = '/dashboard';

    $: {
        const redirectTo = $page.url.searchParams.get('redirect');
        if (redirectTo) {
            redirectPath = redirectTo;
        }
        // check if user has been set in session store then redirect
        if ($session?.user?.id) {
            goto(redirectPath);
        }
    }
</script>

<section>
    <div>
        <progress class="progress" max="100" />
    </div>
    <div>
        Signing in from the email confirmation link  ...
    </div>
</section>

<style>
    .progress:indeterminate {
        animation-duration: 3.8s;
    }
</style>

Enter fullscreen mode Exit fullscreen mode

Dashboard Page and Route - protected users only page

In src/routes/ folder create a new file dashboard.svelte where we display some data about user stored by Supabase,

src/routes/dashboard.svelte 
<script>
    export let user;
</script>

<div>
    <h3>This is protected route accesible only by logged users</h3>
    <p>Hello user {user.email}</p>
</div>
<div>
    <p>User {user.email} details:</p>
    <pre>{JSON.stringify(user, null, 2)}</pre>
</div>

Enter fullscreen mode Exit fullscreen mode

In src/routes/ folder create a new dashboard.js file.

src/routes/dashboard.js
import { supabaseServerClient, withApiAuth } from '@supabase/auth-helpers-sveltekit';

export const GET = async ({ locals, request }) =>
    withApiAuth(
        {
            redirectTo: '/',
            user: locals.user
        },
        async () => {
            // const { data } = await supabaseServerClient(request).from('test').select('*');
            return {
                body: {
                    // data,
                    user: locals.user
                }
            };
        }
    );

Enter fullscreen mode Exit fullscreen mode

Email Confirmation Redirect Layout

In src/routes/ folder create a new file __layout-blank.svelte where we are having a special named layout for email confirmation redirect as mentioned earlier.

//src/routes/__layout-blank.svelte
<script>
    import { session } from '$app/stores';
    import { supabaseClient } from '$lib/sb';
    import { SupaAuthHelper } from '@supabase/auth-helpers-svelte';
</script>

<svelte:head>
    <title>Email and Password Demo - Supabase Auth Helpers</title>
</svelte:head>

<SupaAuthHelper {supabaseClient} {session}>
    <slot />
</SupaAuthHelper>

Enter fullscreen mode Exit fullscreen mode



I hope this tutorial was useful. Big thank to Supabase guys for SvelteKit Auth Helpers, their examples where true source for this article. Because I am really no auth expert especialy concerning SSR all comments, corrections or enhancements are welcomed.

Top comments (1)

Collapse
 
vhs profile image
vhs

Here are some example projects provided and maintained by the Supabase team: github.com/supabase/auth-helpers#e....