Supabase with Svelte/SvelteKit: Full-Stack Tutorial

SvelteKit and Supabase integration enables building full-stack applications with server-side rendering, form actions, load functions, and Supabase's backend services creating blazing-fast web applications with minimal JavaScript and optimal SEO. Unlike traditional SPAs requiring client-side data fetching and authentication, SvelteKit's load functions fetch data on the server eliminating loading states, form actions handle mutations without JavaScript, and SSR ensures content is indexed by search engines while maintaining progressive enhancement. This comprehensive guide covers setting up Supabase in SvelteKit projects, implementing server-side authentication with cookies, using load functions for server-side data fetching, creating form actions for mutations, handling real-time subscriptions in Svelte stores, protecting routes with hooks, building type-safe applications with TypeScript, and deploying SvelteKit + Supabase to production. SvelteKit integration becomes ideal for developers wanting minimal JavaScript bundles, server-side rendering for SEO, progressive enhancement, or building high-performance applications with excellent Core Web Vitals scores. Before proceeding, understand JavaScript client basics, authentication, and real-time features.
SvelteKit Project Setup
# Create SvelteKit project
npm create svelte@latest my-sveltekit-app
cd my-sveltekit-app
# Choose:
# - SvelteKit demo app or Skeleton project
# - TypeScript (recommended)
# - ESLint, Prettier
# Install dependencies
npm install
npm install @supabase/supabase-js @supabase/ssr
# Project structure:
# my-sveltekit-app/
# src/
# lib/
# supabase.js # Supabase helpers
# supabaseClient.js # Client instance
# routes/
# +page.svelte # Home page
# +page.server.js # Server-side load
# auth/
# +page.svelte # Auth page
# +page.server.js # Auth actions
# dashboard/
# +page.svelte
# +page.server.js
# +layout.server.js # Root layout load
# hooks.server.js # Server hooks
# .env # Environment variablesSupabase SSR Helpers
// src/lib/supabase.js
import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public'
import { createServerClient } from '@supabase/ssr'
export const createSupabaseServerClient = (event) => {
return createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
cookies: {
get: (key) => event.cookies.get(key),
set: (key, value, options) => {
event.cookies.set(key, value, options)
},
remove: (key, options) => {
event.cookies.delete(key, options)
}
}
})
}
// src/lib/supabaseClient.js - Browser client
import { createBrowserClient } from '@supabase/ssr'
import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public'
export const supabase = createBrowserClient(
PUBLIC_SUPABASE_URL,
PUBLIC_SUPABASE_ANON_KEY
)
// .env file
// PUBLIC_SUPABASE_URL=your-project-url
// PUBLIC_SUPABASE_ANON_KEY=your-anon-keyServer Hooks for Authentication
// src/hooks.server.js
import { createSupabaseServerClient } from '$lib/supabase'
export const handle = async ({ event, resolve }) => {
event.locals.supabase = createSupabaseServerClient(event)
// Get session for every request
event.locals.getSession = async () => {
const {
data: { session }
} = await event.locals.supabase.auth.getSession()
return session
}
return resolve(event, {
filterSerializedResponseHeaders(name) {
return name === 'content-range'
}
})
}Root Layout with Session
// src/routes/+layout.server.js
export const load = async ({ locals: { getSession } }) => {
return {
session: await getSession()
}
}
<!-- src/routes/+layout.svelte -->
<script>
import { invalidate } from '$app/navigation'
import { onMount } from 'svelte'
import { supabase } from '$lib/supabaseClient'
export let data
$: ({ session } = data)
onMount(() => {
const {
data: { subscription }
} = supabase.auth.onAuthStateChange((event, _session) => {
if (_session?.expires_at !== session?.expires_at) {
invalidate('supabase:auth')
}
})
return () => subscription.unsubscribe()
})
</script>
<nav>
<a href="/">Home</a>
{#if session}
<a href="/dashboard">Dashboard</a>
<span>Logged in as {session.user.email}</span>
{:else}
<a href="/auth">Login</a>
{/if}
</nav>
<slot />Authentication with Form Actions
// src/routes/auth/+page.server.js
import { redirect, fail } from '@sveltejs/kit'
export const actions = {
signup: async ({ request, locals: { supabase } }) => {
const formData = await request.formData()
const email = formData.get('email')
const password = formData.get('password')
const { error } = await supabase.auth.signUp({
email,
password
})
if (error) {
return fail(400, { error: error.message, email })
}
return { success: true }
},
signin: async ({ request, locals: { supabase } }) => {
const formData = await request.formData()
const email = formData.get('email')
const password = formData.get('password')
const { error } = await supabase.auth.signInWithPassword({
email,
password
})
if (error) {
return fail(400, { error: error.message, email })
}
throw redirect(303, '/dashboard')
},
signout: async ({ locals: { supabase } }) => {
await supabase.auth.signOut()
throw redirect(303, '/')
}
}
<!-- src/routes/auth/+page.svelte -->
<script>
import { enhance } from '$app/forms'
export let form
</script>
<div class="auth-container">
<h1>Authentication</h1>
<!-- Sign Up Form -->
<form method="POST" action="?/signup" use:enhance>
<h2>Sign Up</h2>
<input
name="email"
type="email"
placeholder="Email"
value={form?.email ?? ''}
required
/>
<input
name="password"
type="password"
placeholder="Password"
required
/>
<button type="submit">Sign Up</button>
{#if form?.error}
<p class="error">{form.error}</p>
{/if}
{#if form?.success}
<p class="success">Check your email for confirmation!</p>
{/if}
</form>
<!-- Sign In Form -->
<form method="POST" action="?/signin" use:enhance>
<h2>Sign In</h2>
<input
name="email"
type="email"
placeholder="Email"
required
/>
<input
name="password"
type="password"
placeholder="Password"
required
/>
<button type="submit">Sign In</button>
</form>
</div>
<style>
.auth-container {
max-width: 400px;
margin: 2rem auto;
}
form {
margin-bottom: 2rem;
padding: 1rem;
border: 1px solid #ddd;
}
input {
display: block;
width: 100%;
margin: 0.5rem 0;
padding: 0.5rem;
}
.error { color: red; }
.success { color: green; }
</style>Server-Side Data Loading
// src/routes/dashboard/+page.server.js
import { redirect } from '@sveltejs/kit'
export const load = async ({ locals: { supabase, getSession } }) => {
const session = await getSession()
if (!session) {
throw redirect(303, '/auth')
}
// Fetch posts server-side
const { data: posts } = await supabase
.from('posts')
.select('*')
.order('created_at', { ascending: false })
return {
posts: posts ?? []
}
}
export const actions = {
create: async ({ request, locals: { supabase } }) => {
const formData = await request.formData()
const title = formData.get('title')
const content = formData.get('content')
const { error } = await supabase
.from('posts')
.insert({ title, content })
if (error) {
return fail(400, { error: error.message })
}
return { success: true }
},
delete: async ({ request, locals: { supabase } }) => {
const formData = await request.formData()
const id = formData.get('id')
const { error } = await supabase
.from('posts')
.delete()
.eq('id', id)
if (error) {
return fail(400, { error: error.message })
}
return { success: true }
}
}
<!-- src/routes/dashboard/+page.svelte -->
<script>
import { enhance } from '$app/forms'
export let data
</script>
<div class="dashboard">
<h1>Dashboard</h1>
<!-- Create Post Form -->
<form method="POST" action="?/create" use:enhance>
<input name="title" placeholder="Title" required />
<textarea name="content" placeholder="Content" required></textarea>
<button type="submit">Create Post</button>
</form>
<!-- Posts List -->
<div class="posts">
{#each data.posts as post}
<article class="post">
<h3>{post.title}</h3>
<p>{post.content}</p>
<form method="POST" action="?/delete" use:enhance>
<input type="hidden" name="id" value={post.id} />
<button type="submit">Delete</button>
</form>
</article>
{:else}
<p>No posts yet.</p>
{/each}
</div>
</div>
<style>
.dashboard {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
}
form {
margin-bottom: 2rem;
}
.posts {
display: flex;
flex-direction: column;
gap: 1rem;
}
.post {
border: 1px solid #ddd;
padding: 1rem;
border-radius: 4px;
}
</style>Real-time Svelte Store
// src/lib/stores/posts.js
import { writable } from 'svelte/store'
import { supabase } from '$lib/supabaseClient'
function createPostsStore() {
const { subscribe, set, update } = writable([])
let subscription
return {
subscribe,
init: async () => {
// Fetch initial data
const { data } = await supabase
.from('posts')
.select('*')
.order('created_at', { ascending: false })
set(data || [])
// Setup real-time subscription
subscription = supabase
.channel('posts-channel')
.on(
'postgres_changes',
{ event: 'INSERT', schema: 'public', table: 'posts' },
(payload) => {
update(posts => [payload.new, ...posts])
}
)
.on(
'postgres_changes',
{ event: 'UPDATE', schema: 'public', table: 'posts' },
(payload) => {
update(posts =>
posts.map(p => p.id === payload.new.id ? payload.new : p)
)
}
)
.on(
'postgres_changes',
{ event: 'DELETE', schema: 'public', table: 'posts' },
(payload) => {
update(posts => posts.filter(p => p.id !== payload.old.id))
}
)
.subscribe()
},
destroy: () => {
if (subscription) {
supabase.removeChannel(subscription)
}
}
}
}
export const posts = createPostsStore()
<!-- Usage in component -->
<script>
import { onMount, onDestroy } from 'svelte'
import { posts } from '$lib/stores/posts'
onMount(() => {
posts.init()
})
onDestroy(() => {
posts.destroy()
})
</script>
<div>
{#each $posts as post}
<div>{post.title}</div>
{/each}
</div>SvelteKit + Supabase Best Practices
- Use Load Functions: Fetch data server-side in +page.server.js for better SEO and no loading states
- Form Actions for Mutations: Use SvelteKit form actions for progressive enhancement without JavaScript
- Server-Side Auth: Handle authentication in server hooks and load functions for security
- Protect Routes: Check session in load functions and redirect unauthorized users
- Cleanup Subscriptions: Use onDestroy to cleanup real-time subscriptions in Svelte stores
- TypeScript Support: Use TypeScript for type safety throughout SvelteKit and Supabase
- Progressive Enhancement: Ensure forms work without JavaScript using enhance directive
Next Steps
- Add TypeScript: Implement type-safe database queries
- Compare Frameworks: Explore React, Next.js, or Vue.js
- Learn Authentication: Deep dive into email/password auth
- Secure Access: Implement Row Level Security policies
Conclusion
SvelteKit and Supabase integration enables building full-stack applications with server-side rendering, progressive enhancement, and minimal JavaScript bundles while leveraging Supabase's authentication, database, and real-time features. By using server hooks for authentication with cookies, load functions for server-side data fetching eliminating loading states, form actions for mutations working without JavaScript, and Svelte stores for real-time subscriptions, you create fast, SEO-friendly applications. SvelteKit's approach provides excellent Core Web Vitals with content rendered server-side, forms enhanced progressively, and minimal client JavaScript for optimal performance. Always fetch data in load functions for SSR benefits, use form actions with enhance directive for progressive enhancement, handle auth in server hooks maintaining security, protect routes by checking sessions, and cleanup real-time subscriptions in onDestroy. SvelteKit + Supabase combination offers best developer experience for building modern full-stack applications prioritizing performance, SEO, and user experience. Continue building with TypeScript types, security policies, and real-time features.
$ share --platform
$ cat /comments/ (0)
$ cat /comments/
// No comments found. Be the first!


