Supercharge Your Startup Journey with SvelteKit and Supabase
Build quicker. Learn quicker. Startups are all about how fast you can get your idea in front of potential customers that will pay for a solution. Sveltekit + Supabase is the best combination I've seen since I've started full stack development almost a decade ago.
Build quicker. Learn quicker. Startups are all about how fast you can get your idea in front of potential customers that will pay for a solution. Sveltekit + Supabase is the best combination I've seen since I've started full stack development almost a decade ago.
SvelteKit, a cutting-edge JavaScript framework, is revolutionizing the way web applications are built. It offers a refreshing approach to frontend development by shifting the heavy lifting from the browser to the build step, resulting in leaner and faster applications. SvelteKit's key principle is compiling components at build time, enabling highly efficient and performant web experiences. With its intuitive syntax, reactive programming model, and seamless integration with popular frontend libraries and tools, SvelteKit provides developers with a powerful toolkit for creating dynamic and interactive user interfaces. In this article, we'll delve into the features and advantages of SvelteKit that make it a compelling choice for modern web development projects.
Supabase, an open-source alternative to Firebase, is revolutionizing the way startups handle their backend infrastructure. It serves as a powerful combination of a Postgres database and a set of authentication and real-time features, offering developers a full-stack platform that simplifies and streamlines the development process. With Supabase, startups can leverage the reliability and scalability of PostgreSQL, a battle-tested and widely adopted database technology. Supabase provides real-time capabilities, enabling instant updates and collaboration between users. The built-in authentication system ensures secure user management, allowing startups to focus on building their core product without compromising on security. Supabase gives you all the backend features you need to build a product: Database, Authentication, Storage, Realtime API, Edge Functions, Serverless APIs, and more.
In this series, we will walk you through the entire process of building a startup from scratch using these two technologies. The reader is expected to have a decent understanding of Javascript/Typescript, CSS, and HTML. Some SQL understanding can come in handy. Assuming you have those skills in hand, jump aboard. Let's build your next startup.
Create the app
npm create svelte@latest deff-app
Then cd deff-app && pnpm i && npx svelte-add@latest tailwindcss
to install the dependencies & tailwindcss.
Start Supabase locally for development
Most of your development will happen locally. In order to test your app, you want to have a local supabase database + studio running to run tests rather than leveraging a remote supabase instance. We'll walk you through how to set one up for your local development needs.
npx supabase init && npx supabase start
I then changed .env
with my prod variables into .env.preview
and added the following to .env
:
deff-app git:(main) ✗ npx supabase start
Seeding data supabase/seed.sql...
Started supabase local development setup.
API URL: http://localhost:54321
DB URL: postgresql://postgres:postgres@localhost:54322/postgres
Studio URL: http://localhost:54323
Inbucket URL: http://localhost:54324
JWT secret: super-secret-jwt-token-with-at-least-32-characters-long anon key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0
service_role key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU
I took the output from my local supabase and filled into .env
PUBLIC_SUPABASE_URL=http://localhost:54321
PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0
We're now all setup to run locally supabase. You can access the database via the studio at http://localhost:54323
Connect to Supabase w/ auth helpers
Install supabase js & the sveltekit auth package:
npm install @supabase/supabase-js @supabase/auth-helpers-sveltekit
Add the code below to your src/hooks.server.ts
to initialize the client on the server:
// src/hooks.server.ts
import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public'
import { createSupabaseServerClient } from '@supabase/auth-helpers-sveltekit'
import type { Handle } from '@sveltejs/kit'
export const handle: Handle = async ({ event, resolve }) => {
event.locals.supabase = createSupabaseServerClient({
supabaseUrl: PUBLIC_SUPABASE_URL,
supabaseKey: PUBLIC_SUPABASE_ANON_KEY,
event,
})
/**
* A convenience helper so we can just call await getSession() instead const { data: { session } } = await supabase.auth.getSession()
*/
event.locals.getSession = async () => {
const {
data: { session },
} = await event.locals.supabase.auth.getSession()
return session
}
return resolve(event, {
filterSerializedResponseHeaders(name) {
return name === 'content-range'
},
})
}
For typescript, update your src/app.d.ts
with the content below:
// src/app.d.ts
import { SupabaseClient, Session } from '@supabase/supabase-js'
declare global {
namespace App {
interface Locals {
supabase: SupabaseClient
getSession(): Promise<Session | null>
}
interface PageData {
session: Session | null
}
// interface Error {}
// interface Platform {}
}
}
Create a new src/routes/+layout.server.ts
file to handle the session on the server-side.
// src/routes/+layout.server.ts
import type { LayoutServerLoad } from './$types'
export const load: LayoutServerLoad = async ({ locals: { getSession } }) => {
return {
session: await getSession(),
}
}
Start your dev server (npm run dev
) in order to generate the./$types
files we are referencing in our project.
Create a new src/routes/+layout.ts
file to handle the session and the supabase object on the client-side.
// src/routes/+layout.ts
import { invalidate } from '$app/navigation'
import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public'
import { createSupabaseLoadClient } from '@supabase/auth-helpers-sveltekit'
import type { LayoutLoad } from './$types'
export const load: LayoutLoad = async ({ fetch, data, depends }) => {
depends('supabase:auth')
const supabase = createSupabaseLoadClient({
supabaseUrl: PUBLIC_SUPABASE_URL,
supabaseKey: PUBLIC_SUPABASE_ANON_KEY,
event: { fetch },
serverSession: data.session,
})
const {
data: { session },
} = await supabase.auth.getSession()
return { supabase, session }
}
Update your src/routes/+layout.svelte
:
<!-- src/routes/+layout.svelte -->
<script lang="ts">
import '../app.postcss';
import { invalidate } from '$app/navigation';
import { onMount } from 'svelte';
export let data;
$: ({ supabase, session } = data);
onMount(() => {
const { data } = supabase.auth.onAuthStateChange((event, _session) => {
if (_session?.expires_at !== session?.expires_at) {
invalidate('supabase:auth');
}
});
return () => data.subscription.unsubscribe();
});
</script>
<svelte:head>
<title>deff</title>
</svelte:head>
<slot />
Now all the pieces are in place for supabase. We'll come back to this later to implement user authentication in a subsequent article.
Add svelte inspector
This allows me to shift-click within chome to go directly to vscode component.
Add the following to `svelte.config.js`
const config = {
// ...
vitePlugin: {
inspector: true
}
};
Set up github & supabase & vercel
In order to make our development supercharged, we want to leverage automated devops techniques so we're only focused on product + marketing. Github + Github Actions + Supabase + Vercel ensures we aren't worrying about any servers.
First let's get the code into a github repo. I will be sharing code snippets that work for my specific github repo so you'll need to replace the github repo link accordingly if you're copying & pasting.
git init
git add .
git commit -m "first commit"
git branch -M main
git remote add origin git@github.com:leerobert/sveltekit-supabase-startup.git
git push -u origin main
The code is now live at https://github.com/leerobert/sveltekit-supabase-startup
Next I create a new project on supabase (https://app.supabase.com/new) and get the relevant credentials:
Then I go to Project Settings > API tab to view the API credentials. From there I retrieve the following two variables that are required to deploy the app within vercel:
PUBLIC_SUPABASE_URL=https://your-project.supabase.co
PUBLIC_SUPABASE_ANON_KEY=ey-your-anon-key
Then create a new project on Vercel and connect it to the github repo. And I add the above supabase environment variables to the vercel environment variables.
It should look something like this within the environment variables component of creating the Vercel web app. Click deploy and you should have vercel linked to your github repo. This is crucial for the final step which is initializing a proper CI/CD workflow so you can optimally develop locally & push up migrations / functions with any devops work.
Setup CI/CD
<paragraph about how to do ci/cd properly. local dev. make migrations. push code + migrations to a staging/develop branch. deploy check change in a staging env. then push to production>
Let's create a second supabase instance for our staging environment.
Next let's create a staging
branch and connect that branch to Vercel. To do that, first we must create a staging branch and push it up to github:
git checkout -b staging
git push -u origin staging
Then I go to the domain settings for my project (the link looks like this for me https://vercel.com/leerobert/sveltekit-supabase-startup/settings/domains). I enter a new domain with -staging appended to the name. Something like: sveltekit-supabase-startup-staging.vercel.app
I click edit and change the branch to staging
You should have two domains linked to two different branches now and your domain list should look like this:
Database Migrations
Now let's create a migration and go through the deployment process. First I open up my local supabase studio at http://localhost:54323/project/default/editor. Next I'll manually create a table called "deffs". I turned off Row Level Security (RLS) for now. We'll get to that in a later article.
Next, let's create a migration for the new table:
npx supabase db diff --use-migra -f initial_deffs_table
This will create a file in your supabase/migrations
folder that has the following contents:
create table "public"."deffs" (
"id" bigint generated by default as identity not null,
"created_at" timestamp with time zone default now(),
"name" text not null
);
alter table "public"."deffs" enable row level security;
CREATE UNIQUE INDEX deffs_pkey ON public.deffs USING btree (id);
alter table "public"."deffs" add constraint "deffs_pkey" PRIMARY KEY using index "deffs_pkey";
We then want to keep track of the types (for importing purposes) in a supabase.schema.ts
file. You can generate that by running:
npx supabase gen types typescript --local > src/supabase.schema.ts
Next, we want to create the Github CI deploy scripts necessary to run migrations in our staging and prod environments.
The first staging script will ensure that our supabase.schema.ts
file is always in sync with our migrations. This ensure that our types used in our code stay in sync with the database state. Create a file in .github/workflows/ci.yaml
with the following info:
# .github/workflows/ci.yaml
name: CI
on:
pull_request:
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: supabase/setup-cli@v1
with:
version: 1.61.1
- name: Start Supabase local development setup
run: supabase start
- name: Verify generated types are up-to-date
run: |
supabase gen types typescript --local > src/supabase.schema.ts
if [ "$(git diff --ignore-space-at-eol types.ts | wc -l)" -gt "0" ]; then
echo "Detected uncommitted changes after build. See status below:"
git diff
exit 1
fi
Next create our staging deployment workflow in .github/workflows/staging.yaml
:
# .github/workflows/staging.yaml
name: Deploy Migrations to Staging
on:
push:
branches:
- staging
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-22.04
env:
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
SUPABASE_DB_PASSWORD: ${{ secrets.STAGING_DB_PASSWORD }}
STAGING_PROJECT_ID: STAGING_PROJECT_ID
steps:
- uses: actions/checkout@v3
- uses: supabase/setup-cli@v1
with:
version: 1.61.1
- run: supabase link --project-ref $STAGING_PROJECT_ID
- run: supabase db push
Note, in this file I am hardcoding my STAGING_PROJECT_ID
and not using a Github secrets to ensure these fiels stay separate.
And lastly, the production workflow file .github/workflows/production.yaml
:
# .github/workflows/production.yaml
name: Deploy Migrations to Production
on:
push:
branches:
- main
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-22.04
env:
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
SUPABASE_DB_PASSWORD: ${{ secrets.PRODUCTION_DB_PASSWORD }}
PRODUCTION_PROJECT_ID: dfgfggoaaxmrmoggpdmi
steps:
- uses: actions/checkout@v3
- uses: supabase/setup-cli@v1
with:
version: latest
- run: supabase link --project-ref $PRODUCTION_PROJECT_ID
- run: supabase db push
In order to have these properly deploy, we need to set up the github secrets. Go to the github repo > settings > and click secrets & variables in the left sidebar > actions. We're going to create three repository secrets for the actions.
SUPABASE_ACCESS_TOKEN -> create an access token at https://app.supabase.com/account/tokens
PRODUCTION_DB_PASSWORD -> the password you provided when creating the production database in supabase
STAGING_DB_PASSWORD -> the password you provided when creaitng the staging database in supabase
After saving these tokens, you should be able to push your commit up to the staging
branch and you will see a Github Action run the supabase commit that pushes your migration changes to the staging supabase instance. If your action fails, make sure you have the right password / project id / access token variables all properly typed in.
As you can see in the pull request (https://github.com/leerobert/sveltekit-supabase-startup/pull/1), both the CI & staging checks pass.
After merging the pull request into main
, we see the production
workflow action run at https://github.com/leerobert/sveltekit-supabase-startup/actions/runs/5006404642 which then pushes the migration onto the production database.
The key to this flow is keeping staging & production identical in workflows so as you work on code locally, you can push to staging for a "sanity" check before a production push.
Let's do this for a function next.
Function deployments
Let's create a basic hello world function:
npx supabase functions new hello_world
This will create a function within supabase/functions
. The best way to do edits on functions is by opening up the supabase
functions within a separate VSCode window and modifying the .vscode
settings to handle deno functions. Otherwise, Sveltekit and Deno don't really get along.
You can open up VSCode manually within that folder or run cd supabase && code .
if you have code
installed. Then create within the supabase folder window a file .vscode/settings.json
. Put the following into the file:
{
"deno.enable": true,
"deno.unstable": true,
}
This should make all of your typescript errors go away for the deno code.
You can now run npx supabase functions serve
to see the logging for the function. At the bottom of the hello_world/index.ts
file exists a curl
command you can run to test the function. You might need to add hello_world
to the end of the url like:
curl -i --location --request POST 'http://localhost:54321/functions/v1/hello_world' \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \
--header 'Content-Type: application/json' \
--data '{"name":"Functions"}'
Ok now let's add this function to our CI/CD flow. Modify .github/workflows/staging.yaml
to add a new line at the end:
- run: supabase functions deploy hello_world --project-ref $STAGING_PROJECT_ID
And also add to .github/workflows/production.yaml
:
- run: supabase functions deploy hello_world --project-ref $PRODUCTION_PROJECT_ID
After creating a PR (https://github.com/leerobert/sveltekit-supabase-startup/pull/2), you should see the function being deployed in the github action.
Merging into main will also deploy the function to your production environment as well. You will find the edge function living within the functions tab of the supabase studio.
Next steps
You have a full CI/CD process of being able to introduce code form a development environment (across multiple coders) into a PR which then can be merged into a staging environment for testing. The staging environment has the exact same setup as production so merges from staging to prod should be seemless. Database migrations & function deployments are all handled for code changes to the functions as well. Devops is a minimum.
The next article will discuss how to do authentication and multi-tenancy within supabase. Stay tuned.