Back to articles

From Zero to Production: Building a Full-Stack App with SvelteKit and Supabase

AuthorMajd Muhtaseb05/10/202515 minutes
From Zero to Production: Building a Full-Stack App with SvelteKit and Supabase

Introduction

This guide walks you through building a basic full-stack application using SvelteKit and Supabase. SvelteKit provides a fantastic developer experience for building fast and SEO-friendly web applications, while Supabase offers a powerful and easy-to-use backend-as-a-service.

Prerequisites

  • Node.js (v16 or higher)
  • npm or pnpm
  • Supabase Account

Setting up your SvelteKit Project

First, create a new SvelteKit project:

npm create svelte@latest my-sveltekit-app
cd my-sveltekit-app
npm install

Initializing Supabase

  1. Create a new project on Supabase (https://supabase.com/).
  2. Once your project is created, navigate to the project settings and find your project URL and public API key. We'll need these later.
  3. Create a table in your Supabase database, name it for example todos, and add columns like id (UUID, primary key), text (text), and completed (boolean).

Configuring Supabase in SvelteKit

Install the Supabase JavaScript client:

npm install @supabase/supabase-js

Create a .env file in the root of your project and add your Supabase credentials:

VITE_SUPABASE_URL="YOUR_SUPABASE_URL"
VITE_SUPABASE_ANON_KEY="YOUR_SUPABASE_ANON_KEY"

Create a supabaseClient.js file in your src/lib directory:

// src/lib/supabaseClient.js
import { createClient } from '@supabase/supabase-js'

const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY

export const supabase = createClient(supabaseUrl, supabaseAnonKey)

Implementing Authentication

Let's add authentication using Supabase Auth UI. Install the necessary package:

npm install @supabase/auth-ui-svelte @supabase/auth-ui-shared

Create an +page.svelte file inside src/routes folder

// src/routes/+page.svelte
<script>
  import { Auth } from '@supabase/auth-ui-svelte'
  import '@supabase/auth-ui-shared/dist/index.css'
	import { supabase } from '$lib/supabaseClient'
	import { onMount } from 'svelte';
  import { goto } from '$app/navigation';

  let session = null

	onMount(() => {
		supabase.auth.getSession().then(({ data: { session } }) => {
			if(session){
				goto('/todos');
			}
		})

		supabase.auth.onAuthStateChange((_event, _session) => {
			session = _session
			if(session){
				goto('/todos');
			}
		})
	})

</script>

{#if !session}
  <Auth supabaseClient={supabase} appearance={{ theme: 'dark' }} providers={[]} />
{/if}

Now create a /todos route and a +page.svelte file inside src/routes/todos folder

// src/routes/todos/+page.svelte
<script>
    import { supabase } from '$lib/supabaseClient';
    import { onMount } from 'svelte';
    import { goto } from '$app/navigation';

    let todos = [];
    let newTodo = '';

    onMount(async () => {
        const { data, error } = await supabase
            .from('todos')
            .select('*')
            .order('id', { ascending: false });

        if (error) {
            console.error('Error fetching todos:', error);
            return;
        }

        todos = data;
    });

    async function addTodo() {
        const { data, error } = await supabase
            .from('todos')
            .insert([{ text: newTodo }])
            .select();

        if (error) {
            console.error('Error adding todo:', error);
            return;
        }

        todos = [...data, ...todos];
        newTodo = '';
    }

    async function toggleComplete(todo) {
        const { error } = await supabase
            .from('todos')
            .update({ completed: !todo.completed })
            .eq('id', todo.id);

        if (error) {
            console.error('Error updating todo:', error);
            return;
        }

		todos = todos.map((t) => {
			if(t.id === todo.id) {
				return {...t, completed: !t.completed}
			}
			return t;
		})
    }

    async function deleteTodo(todo) {
        const { error } = await supabase
            .from('todos')
            .delete()
            .eq('id', todo.id);

        if (error) {
            console.error('Error deleting todo:', error);
            return;
        }

        todos = todos.filter((t) => t.id !== todo.id);
    }

	async function signOut() {
		await supabase.auth.signOut()
		goto('/')
	}
</script>

<h1>My Todos</h1>

<input type="text" bind:value={newTodo} placeholder="Add a new todo" />
<button on:click={addTodo}>Add</button>

<ul>
    {#each todos as todo (todo.id)}
        <li>
            <input type="checkbox" checked={todo.completed} on:change={() => toggleComplete(todo)} />
            <span class:completed={todo.completed}>{todo.text}</span>
            <button on:click={() => deleteTodo(todo)}>Delete</button>
        </li>
    {/each}
</ul>

<button on:click={signOut}>Sign out</button>

<style>
    .completed {
        text-decoration: line-through;
        color: gray;
    }
</style>

Running Your Application

npm run dev -- --open

Your application should now be running at http://localhost:5173.

Conclusion

You've successfully built a full-stack application using SvelteKit and Supabase. This is just the beginning! Explore SvelteKit and Supabase further to build more complex and feature-rich applications. Remember to secure your application, handle errors gracefully, and optimize performance for production.