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
2. Install Supabase Dependencies
npm i -D supabase@latest @supabase/ssr@latest @supabase/supabase-js@latest
3. Initialize Supabase
npx supabase init
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"
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
Run supabase to get keys.
PUBLIC_SUPABASE_URL=http://localhost:54321
PUBLIC_SUPABASE_ANON_KEY=YOUR_KEY_HERE
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",
Generate types file.
npm run types
Note: You can also reset your database and generate types with:
npm run reset
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 {}
}
}
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";
}
});
};
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;
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 };
};
You can run it in any server load function with:
await requireUser();
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;
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}`
}
});
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
Top comments (1)
Really helpful guide, thanks for sharing the steps. The extra context around .env and the Hooks setup is much appreciated!