DEV Community

Jonathan Gamble
Jonathan Gamble

Posted on

1

Perfect Local SvelteKit Supabase Setup in 2025

The main documentation for Supabase and SvelteKit is flawed.

Here is how you should do it!

Pre-setup

1. Setup Docker

Here is a supabase docker link, but this is really not about docker.

Setup

1. Create SvelteKit App

npx sv create my-app
Enter fullscreen mode Exit fullscreen mode

2. Install Supabase Dependencies

npm i -D supabase@latest @supabase/ssr@latest @supabase/supabase-js@latest
Enter fullscreen mode Exit fullscreen mode

3. Initialize Supabase

npx supabase init
Enter fullscreen mode Exit fullscreen mode

Note: You don't need npx if you have it installed locally on your machine, I just find it easier to keep my package updated.

You may also want to update your supabase URLS in config.toml:

[studio]
api_url = "http://localhost:54321"
...
[auth]
site_url = "http://localhost:5173"
Enter fullscreen mode Exit fullscreen mode

Note: This should match with http://127.0.0.1 if you chose to not use localhost.

4. Setup .env with keys

npx supabase start
Enter fullscreen mode Exit fullscreen mode

Run supabase to get keys.

PUBLIC_SUPABASE_URL=http://localhost:54321
PUBLIC_SUPABASE_ANON_KEY=YOUR_KEY_HERE
Enter fullscreen mode Exit fullscreen mode

Copy API URL and anon key to your .env file.

5. Setup type generatation

Add package.json shortcuts under scripts.

"types": "npx supabase gen types typescript --local > ./src/lib/database.types.ts",
"reset": "npx supabase db reset & npm run types",
Enter fullscreen mode Exit fullscreen mode

Generate types file.

npm run types
Enter fullscreen mode Exit fullscreen mode

Note: You can also reset your database and generate types with:

npm run reset
Enter fullscreen mode Exit fullscreen mode

6. Setup TypeScript

Edit your /src/app.d.ts file.

import type { Database } from '$lib/database.types';
import type { Session, User } from '@supabase/supabase-js';
import { SupabaseClient } from '@supabase/supabase-js';

declare global {
    namespace App {
        interface Locals {
            supabase: SupabaseClient<Database>;
            safeGetSession(): Promise<{
                session: Session | null;
                user: User | null;
            }>;
        }
        interface PageData {
            session?: Session | null;
            user?: User | null;
        }
        // interface Error {}
        // interface Platform {}
    }
}
Enter fullscreen mode Exit fullscreen mode

Note: This is different and includes changes from official docs to import database types, and to allow session and user to be optional.

7. Setup Hooks

Add a /src/hooks.server.ts file.

import {
    PUBLIC_SUPABASE_ANON_KEY,
    PUBLIC_SUPABASE_URL,
} from "$env/static/public";
import { createServerClient } from "@supabase/ssr";
import type { Handle } from "@sveltejs/kit";

export const handle: Handle = async ({ event, resolve }) => {

    event.locals.supabase = createServerClient(
        PUBLIC_SUPABASE_URL,
        PUBLIC_SUPABASE_ANON_KEY,
        {
            cookies: {
                getAll: () => event.cookies.getAll(),
                setAll: (cookiesToSet) => {
                    cookiesToSet.forEach(({ name, value, options }) => {
                        event.cookies.set(name, value, {
                            ...options,
                            path: "/"
                        });
                    });
                }
            }
        }
    );

    event.locals.safeGetSession = async () => {

        /** @ts-expect-error: suppressGetSessionWarning is not officially supported */
        event.locals.supabase.auth.suppressGetSessionWarning = true;

        const {
            data: { session },
        } = await event.locals.supabase.auth.getSession();
        if (!session) {
            return { session: null, user: null };
        }

        const {
            data: { user },
            error,
        } = await event.locals.supabase.auth.getUser();
        if (error) {
            return { session: null, user: null };
        }

        return {
            session,
            user
        };
    };

    return resolve(event, {
        filterSerializedResponseHeaders(name) {
            return name === "content-range" ||
                name === "x-supabase-api-version";
        }
    });
};
Enter fullscreen mode Exit fullscreen mode

Note: Like it or not, the Supabase team has refused to fix the warning issue. There have been numerous closed issues and no improvements. A warning is only good when it shows up when it is supposed to, not when it shows up uncalled for. This is supposed to be fixed in the next version of gotrue, but from everything I have read, we are 2 years away from a production version.

8. Setup Root Layout

Create /src/routes/+layout.server.ts file.

import type { LayoutServerLoad } from "./$types";

export const load = (async ({ locals: { safeGetSession } }) => {

    const { user, session } = await safeGetSession();

    return {
        user,
        session
    };

}) satisfies LayoutServerLoad;
Enter fullscreen mode Exit fullscreen mode

9. Reusable Function (Optional)

I also have a reusable function to stop checking for a user.

export const requireUser = async () => {

    const { url, locals: { safeGetSession } } = getRequestEvent();

    const { user } = await safeGetSession();

    const path = url.pathname || '/home';

    if (!user) {
        redirect(303, '/login?next=' + path);
    }

    return { user };
};
Enter fullscreen mode Exit fullscreen mode

You can run it in any server load function with:

await requireUser();
Enter fullscreen mode Exit fullscreen mode

It is safe since redirect throws an error under the hood. You can also return the user if you need it. It just saves boilerplate.

10. Callback

Create your /auth/callback/+page.server.ts file.

import { error, redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
import { dev } from '$app/environment';

export const load = (async ({ url, locals: { supabase } }) => {

    const errorDescription = url.searchParams.get('error_description');
    if (errorDescription) {
        if (dev) {
            console.error(errorDescription);
        }
        const description = errorDescription;
        error(400, description);
    }

    const code = url.searchParams.get('code');
    const next = url.searchParams.get('next') || '/home';

    if (code) {
        const { error: codeError } = await supabase
            .auth
            .exchangeCodeForSession(code);
        if (!codeError) {
            redirect(303, next);
        }
        if (dev) {
            console.error(codeError);
        }
        error(400, codeError.message);
    }

}) satisfies PageServerLoad;
Enter fullscreen mode Exit fullscreen mode

Notice this is NOT a +server.ts file. This way, if there are errors, you can use an +error.html page to gracefully handle them. The errors are not handled correctly in the source code, so I had to use a hack to catch them.

NOTE: When a user login in from another page, it is automatically handled with next?=. However, you will have to manually handle this in your login page and /login/+page.server.ts.

/* +page.svelte */

const next = page.url.searchParams.get('next');

...

// you must pass it to your form action...

...

/* /login/+page.server.ts */

// pass `next` in to your formData in your form action

const { locals: { supabase }, url } = getRequestEvent();

const { error, data } = await supabase.auth.signInWithOAuth({
    provider,
    options: {
        redirectTo: url.origin + `/auth/callback?next=${next}`
    }
});
Enter fullscreen mode Exit fullscreen mode

I'm not providing the exact login implementation because you might use shadcn, or some other form library, you might use superforms, or you might want to avoid that due to it still using stores. I can create another post on this if there is interest.

Hope this helps!

J

Dev Diairies image

User Feedback & The Pivot That Saved The Project ↪️

We’re following the journey of a dev team building on the Stellar Network as they go from hackathon idea to funded startup, testing their product in the real world and adapting as they go.

Watch full video 🎥

Top comments (1)

Collapse
 
cwrite profile image
Christopher Wright

Really helpful guide, thanks for sharing the steps. The extra context around .env and the Hooks setup is much appreciated!

Feature flag article image

Create a feature flag in your IDE in 5 minutes with LaunchDarkly’s MCP server ⏰

How to create, evaluate, and modify flags from within your IDE or AI client using natural language with LaunchDarkly's new MCP server. Follow along with this tutorial for step by step instructions.

Read full post

👋 Kindness is contagious

Explore this insightful write-up embraced by the inclusive DEV Community. Tech enthusiasts of all skill levels can contribute insights and expand our shared knowledge.

Spreading a simple "thank you" uplifts creators—let them know your thoughts in the discussion below!

At DEV, collaborative learning fuels growth and forges stronger connections. If this piece resonated with you, a brief note of thanks goes a long way.

Okay