Table of Contents
Installation
# Supabase Auth Helpers
npm install @supabase/auth-helpers-sveltekit @supabase/auth-helpers-svelte
# TRPC
npm install @trpc/server @trpc/client trpc/sveltekit
The following version of Node is required:
- Node.js:
^16.15.0
Getting Started
Configuration
Set up the fillowing env vars. For local development you can set them in a .env
file. See an example here.
# Find these in your Supabase project settings > API
VITE_SUPABASE_URL=https://your-project.supabase.co
VITE_SUPABASE_ANON_KEY=your-anon-key
SupabaseClient and SupaAuthHelper component setup
Start off by creating a db.ts
file inside of your src/lib
directory. Then instantiate your supabaseClient
by using the createSupabaseClient
function from the @supabase/auth-helpers-sveltekit
library. The supabaseClient
is safe to be imported to the client-side code, because it only uses the anon key. RLS (row-level security) policies will ensure that the user is authenticated before accessing data they would otherwise not be supposed to.
// $lib/supabase/db.ts
import { createSupabaseClient } from '@supabase/auth-helpers-sveltekit';
const { supabaseClient } = createSupabaseClient(
import.meta.env.VITE_SUPABASE_URL as string,
import.meta.env.VITE_SUPABASE_ANON_KEY as string
);
export { supabaseClient };
Edit your __layout.svelte
file and add import the SupaAuthHelper
component, the supabaseClient
we just instantiated and the session
store. In addition, add the QueryClientProvider
provided by the @sveltestack/svelte-query
library.
<!-- @/routes/__layout.svelte -->
<script>
import { session } from '$app/stores';
import { supabaseClient } from '$lib/supabase/db';
import { SupaAuthHelper } from '@supabase/auth-helpers-svelte';
</script>
<SupaAuthHelper supabaseClient={supabaseClient} session={session}>
<slot />
</SupaAuthHelper>
Typings
In order to get the most out of TypeScript and its intellisense, you should import our types into the app.d.ts
type definition file that comes with your SvelteKit project.
// @/app.d.ts
/// <reference types="@sveltejs/kit" />
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare namespace App {
interface UserSession {
user: import('@supabase/supabase-js').User;
accessToken?: string;
}
interface Locals extends UserSession {
serverClient: import('@supabase/supabase-js').SupabaseClient;
error: import('@supabase/supabase-js').ApiError;
}
interface Session extends UserSession {}
// interface Platform {}
// interface Stuff {}
}
tRPC Setup
The index.ts
file is where you’ll create the tRPC context and router.
// $lib/trpc/index.ts
import * as trpc from "@trpc/server";
import type { RequestEvent } from "@sveltejs/kit";
export const createContext = async ({ request, locals }: RequestEvent) => {
return {
req: request,
locals
};
};
export type Context = trpc.inferAsyncReturnType<typeof createContext>;
export const createRouter = () => trpc.router<Context>();
The server.ts
file is where you’ll merge the individual routers that handle your API endpoints. The example router will be explained later in the tutorial.
// $lib/trpc/server.ts
import { createRouter } from "./context";
import { exampleRouter } from "./routers/example";
export const router = createRouter()
.merge("example:", exampleRouter)
// export type definition of API
export type Router = typeof router;
The client.ts
file is where you’ll create the client-side tRPC client. This is the only code you’ll import to your client-side code.
There are some additional helper types that can be used to assign types to variables prior to calling the query. You’ll see an example of these types used in the server-side fetching example below.
// $lib/trpc/client.ts
import { createTRPCClient } from "@trpc/client";
import type { Router } from "./server"; // 👈 only the types are imported from the server
export default createTRPCClient<Router>({
url: "/trpc"
});
type Query = keyof Router['_def']['queries'];
type Mutation = keyof Router['_def']['mutations'];
// Useful types 👇👇👇
export type InferQueryOutput<RouteKey extends Query> = inferProcedureOutput<Router['_def']['queries'][RouteKey]>;
export type InferQueryInput<RouteKey extends Query> = inferProcedureInput<Router['_def']['queries'][RouteKey]>;
export type InferMutationOutput<RouteKey extends Mutation> = inferProcedureOutput<
Router['_def']['mutations'][RouteKey]
>;
export type InferMutationInput<RouteKey extends Mutation> = inferProcedureInput<Router['_def']['mutations'][RouteKey]>;
So far, so good. The last piece of the puzzle is a tRPC middleware for verifying that the user is authenticated. As we defined in the app.d.ts
declaration file, the App.Locals
includes the user information if the user is authenticated.
The MiddlewareFunction
type takes 3 properties: the InputContext
type, the tRPC Context
type, and the tRPC ResponseMeta
type. For more information about the ResponseMeta
type, see the trpc-sveltekit
package documentation. It is not required for this tutorial.
// $lib/trpc/middlewares.ts
import type { MiddlewareFunction } from "@trpc/server/dist/declarations/src/internals/middlewares";
import { TRPCError } from "@trpc/server";
import { Context } from "$lib/trpc";
export const authMiddleware: MiddlewareFunction<{ req: Request; locals: App.Locals }, Context, unknown> = async ({
ctx: { locals },
next
}) => {
if (!locals.user) throw new TRPCError({ message: `Unauthorized`, code: "UNAUTHORIZED" });
return next();
};
Hooks setup
The hooks.ts
file is where the heavy lifting of this library happens, you need to import the handleAuth
function to handle the signing in, signing out, and cookie creation phase. You can import all the hooks using the function and destructure its returned data.
After the hooks are imported, you can set up a custom hook for implementing the createTRPCHandle
function. These handles can be combined with the built-in sequence
function provided by SvelteKit.
The supabaseServerClient
is assigned to the app locals, which are only accessed on the server. You will see an example of this in use in the example router later in the tutorial. For RLS to work in a server environment, you need to inject the user’s access token into the server client.
// @/hooks.ts
import type { Handle, GetSession } from "@sveltejs/kit";
import { sequence } from "@sveltejs/kit/hooks";
import { handleAuth, supabaseServerClient } from "@supabase/auth-helpers-sveltekit";
import { router } from "$lib/trpc/server";
import { createContext } from "$lib/trpc/context";
import { createTRPCHandle } from "trpc-sveltekit";
export const handle: Handle = sequence(
...handleAuth({
// the logout handler
logout: { returnTo: "/" }
}),
async ({ event, resolve }) => {
event.locals.serverClient = supabaseServerClient(event.locals.accessToken || "");
const response = await createTRPCHandle({
url: "/trpc",
router,
createContext,
event,
resolve
});
return response;
}
);
// The session information returned here will be available to the client via the `session` store.
export const getSession: GetSession = async event => {
const { user, accessToken, error } = event.locals;
return {
user,
accessToken,
error
};
};
These will create the handlers under the hood that perform different parts of the authentication flow:
/api/auth/callback
: TheUserHelper
forwards the session details here every timeonAuthStateChange
fires on the client side. This is needed to set up the cookies for your application so that SSR works seamlessly./api/auth/user
: You can fetch user profile information in JSON format./api/auth/logout
: You can logout the user./trpc/[endpoints]
: The endpoints handled by the tRPC router.
Signing in
<script lang="ts">
import { supabase } from "$lib/supabase/client";
import { page, session } from "$app/stores";
let user = $session.user;
async function signin() {
if (!user && supabase) {
return await supabase.auth.signIn(
{
provider: "github"
},
{
redirectTo: `${$page.url.origin}/redirect?to=${encodeURIComponent($page.url.pathname)}`
}
);
}
};
</script>
Signing out
This library has provided a dedicated endpoint for you to use to sign a user out. This endpoint will sign the user out of the Gotrue server, clear the cookies that were set when the user logged in and redirect the user to a configurable path.
The logout handler endpoint is /api/auth/logout
, this will take a GET
request which means it can be used as the href for a normal a
tag in your html.
<a href="/api/auth/logout">Sign out</a>
Basic Setup
You can now determine if a user is authenticated on the client-side by checking that the user
object returned by the $session
store is defined.
<script>
import { session } from '$app/stores';
</script>
{#if !$session.user}
<h1>I am not logged in</h1>
{:else}
<h1>Welcome {$session.user.email}</h1>
<p>I am logged in!</p>
{/if}
Data Fetching
Client-side data fetching with RLS
For RLS to work properly when fetching data client-side, you need to make sure to import the { supabaseClient }
from @supabase/auth-helpers-sveltekit
and only run your query once the user is defined client-side in the $session
:
<script>
import Auth from 'supabase-ui-svelte';
import { error, isLoading } from '@supabase/auth-helpers-svelte';
import { supabaseClient } from '$lib/supabase/db';
import { session } from '$app/stores';
let loadedData = [];
async function loadData() {
const { data } = await supabaseClient.from('test').select('*');
loadedData = data
}
$: {
if ($session.user && $session.user.id) {
loadData();
}
}
</script>
{#if !$session.user}
{#if $error}
<p>{$error.message}</p>
{/if}
<h1>{$isLoading ? `Loading...` : `Loaded!`}</h1>
<Auth {supabaseClient} providers={["github"]} />
{:else}
<a href=="/api/auth/logout">Sign out</a>
<p>user:</p>
<pre>{JSON.stringify($session.user, null, 2)}</pre>
<p>client-side data fetching with RLS</p>
<pre>{JSON.stringify(loadedData, null, 2)}</pre>
{/if}
Server-side data fetching with RLS and tRPC
This is the same as the client-side example above, but this time we are using tRPC to fetch the data from supabase on the server.
// src/trpc/routers/example.ts
import Auth from 'supabase-ui-svelte';
import { authMiddleware, createRouter } from "$lib/trpc";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
interface ExampleData {
...
}
export const exampleRouter = createRouter()
.query("greeting", {
input: z.object({
text: z.string().nullish(),
}).nullish(),
async resolve({ input: { text } }) {
return `Hello ${text || 'world'}!`;
}
});
// anything after this line is a protected endpoint and should use the Supabase server client
.middleware(authMiddleware)
.query("protected", {
async resolve({ ctx: { locals } }) {
const { data /* any[] */, error /* PostgresError */ } = await locals.serverClient.from('test').select('*');
if (error) throw new TRPCError({ message: error, code: "INTERNAL_SERVER_ERROR" });
// the ExampleData type will be returned to the client.
const result: ExampleData[] = data;
return result;
}
});
<script lang="ts">
import { session } from "$app/stores";
import { browser } from "$app/env";
import { error, isLoading } from "@supabase/auth-helpers-svelte";
import trpc, { InferQueryOutput } from "$lib/trpc/client";
let errorMessage: string = "";
// greeter is automatically type-safe
$: greeter = trpc.query("example:greeting", { text: $session.user?.name });
// exampleData infers the type of the data returned from the specified endpoint
let exampleData: InferQueryOutput<"example:protected"> = [];
async function loadData() {
data = await trpc.query("example:protected");
}
$: { if ($session.user) loadData(); }
$: { if ($error) errorMessage = $error.message; }
</script>
{#await greeter then greeting}
<p>{greeting}</p>
{/await}
{#if errorMessage}
<p>{errorMessage}</p>
{/if}
{#if !$session.user}
<h1>{$isLoading ? `Loading...` : `Loaded!`}</h1>
<Auth {supabaseClient} providers={["github"]} />
{:else}
<a href="/api/auth/logout">Sign out</a>
<p>user:</p>
<pre>{JSON.stringify($session.user, null, 2)}</pre>
<p>client-side data fetching with RLS</p>
<pre>{JSON.stringify(exampleData, null, 2)}</pre>
{/if}