On This Page
After our last guide on the Upstash blog scored a spot on the Bytes newsletter, I thought we'd keep the SvelteKit party going.
As a Svelte super fan, I'm seeing more and more people jumping on board every day — and it makes me incredibly excited for the future.
One of the tools that’s still flying under the radar is Lucia.
In this guide I’m going to show you how to get authentication up and running with Lucia. Where we will be using PlanetScale for our database needs, and Upstash Redis to handle sessions.
Below is a screenshot of our end goal for this guide. You can find the example repository here.
This guide will be in SvelteKit, but since Lucia supports any framework, most of this guide can easily be applied to any popular framework out there.
But first things first.
What is Lucia?
Simply put, Lucia is a helpful library for TypeScript that makes handling users and sessions a piece of cake.
Originally, this library was created for SvelteKit, but it has continually evolved and is now versatile enough to play well with just about any framework.
https://twitter.com/pilcrowonpaper/status/1689334346782748674
What's so awesome about Lucia is how it equips you with everything you need to manage the complexity of authentication without sacrificing user experience.
Think of Lucia as a set of primitives — it’s up to you how you want to structure your code and handle the user experience.
Lucia has a few key parts that are important to understand:
Middleware: allows Lucia to read the request and response for different frameworks and runtime.
Database adapters: allow Lucia to store and retrieve users and sessions. By providing an adapter Lucia knows how to query for these types. There are 2 types of adapters; regular adapters, and session adapters. In this particular guide we’re going to use a mySQL database hosted on PlanetScale to store our users, and a Redis instance hosted on Upstash to handle sessions.
With that background info, let’s jump right in.
Prerequisites
To get up and running with the app and following along you need:
- A fundamental understanding of SvelteKit, primarily regarding routes and server-side data loading.
- Basic familiarity with the Drizzle. In this guide we’ll only use Drizzle to manage our MySQL schema.
- An account and database on PlanetScale.
- Access to a Redis instance for example, Upstash Redis.
Getting started
For the sake of efficiency, we won't be creating the entire application from scratch.
Instead, you can clone the sveltekit-lucia-redis
directory from the Upstash examples repo to follow along.
After downloading the repository, navigate into the application using the cd
command and install the dependencies via your preferred package manager and set the .env
variables by duplicating .env.example
.
Understanding the key parts
Here’s a quick rundown of all the important parts.
src/lib/server/auth/index.ts
- Here’s where we configure Lucia.src/lib/server/drizzle
- Drizzle helps us to easily create a mySQL schema which we can conveniently push to PlanetScale using Drizzle Kit. In our last post I used Prisma, so I figured we switch things up and keep things interesting.src/lib/server/planetscale
- Exports the Upstash Client which we use in our Lucia adapter config to manage users.src/lib/server/upstash
- Exports the Upstash Client which we use in our Lucia adapter config to manage sessions.
Alright! Let’s break down the code and see the app in action!
Breaking down the code
Configuring Lucia
The first thing we need to do is configure Lucia. We do this by creating a new file in src/lib/server/auth/index.ts
.
Let’s break down what’s happening here.
We import the lucia
function from the lucia
package to set up the configuration for Lucia.
The first thing we do is configure the adapter
property. This is where we tell Lucia how to handle users and sessions.
Take note of the session
property. We set this to null
because we want to use Redis to handle sessions. If you would use 'session'
here instead, Lucia would use the same adapter for both users and sessions (these strings correspond to the tables in your database).
Don't worry about the session adapter for now, we'll get to that later.
In the middleware
property we can let Lucia know that we're using SvleteKit. This will allow Lucia to read the request and response objects.
Take note of the exported Auth
type. This is the type of our auth
object. We'll need this to set up the SvelteKit locals.
Getting great type inference with Lucia
Lucia is written in TypeScript, so you get great type inference out of the box. Let's make sure SvelteKit knows about the Auth
type we just created.
Open up your app.d.ts
file and add the following:
By adding the Auth
type to the Lucia
namespace, we can now access the auth
object from the locals
object in our SvelteKit routes.
But also anything imported from Lucia will now have the correct types as well.
Now that we have these types, we can set up hooks.server.ts
. This is where we’ll bind the AuthRequest
object to the current request and this will make it easily accessible on the server through locals
. We'll also bind the Session
object to the current session.
We'll also import sequence
which is a helper function that allows us to run multiple hooks in sequence. This will be useful later on when we try to protect our routes.
Creating the user model
Now that we have Lucia configured, we can create our user model.
We'll use Drizzle ORM, since it's all hot and happening right now.
And their memes are on point. Just look at this one.
And we'll only use it to manage our schema, not to query the database as that's handled by Lucia.
Before you can continue, you need to create a database on PlanetScale. And setup the Drizzle config file. This will help the Drizzle CLI to connect to your database.
Because we're using the mySQL adapter, Lucia expects our user model to have a specific structure. You can find more information about this in the docs.
Place the following code in src/lib/server/drizzle/schema/index.ts
.
And run pnpm drizzle-kit push:mysql
to push the schema to PlanetScale.
Voila! You now have a user model that Lucia can use to manage users.
Setting up session management
Now that we have our user model, we can set up session management.
We'll use Upstash Redis to handle sessions. You can sign up for a free account here.
Once you're in the dashboard, all you need to do is create a new database and copy the environment variables.
Now add these variables to your .env
file. And add the following to src/lib/server/upstash/index.ts
.
Now remember when we configured Lucia, we told it to use the upstash
adapter for sessions. This is where we tell Lucia how to handle sessions.
Lucky for us, Lucia has an Upstash Redis adapter out of the box. So all we need to do is import it and pass it the Upstash client.
Now that was easy!
Creating the routes
Now that we have Lucia configured, we can create our routes.
Create the following folders in src/routes
. Don't worry about any files for now, we'll go over them in a bit.
src/routes/auth
src/routes/auth/signin
src/routes/auth/signup
src/routes/app
Tip: Having certain "features" under the same folder name or group makes it easy to manage when doing redirects and protecting routes.
Creating the signin page
Finally, we can do some front-end work! Let's start with the signin page.
Create a new file in src/routes/auth/signin/+page.svelte
. I'm going to omit the styling for now, and focus on the functionality — but you can find the full code in the example repository.
There's not much too it, the interesting part is the use:enhance
action. This will progressively enhance the form, and allow us to show a loading state.
Handling the signin request
SvelteKit makes it incredibly easy to handle POST requests. All we need to do is create a file +page.server.ts
in the same directory as the +page.svelte
file and export a actions
object with at least a default
property.
Let's explore the key elements in this part of the file:
We've imported the PROVIDER_ID
enum and auth
from src/lib/server/auth
, which we created earlier. This auth
object contains all the methods we need to manage users and sessions.
Now let's take a look at the actions
object. We can get the form data from the request object, and do some basic housekeeping.
Next, we'll try to sign in the user. If the user doesn't exist, or the password is incorrect, Lucia will throw an error. We've prepared for this event by catching the error and returning a 400 response along with an error message.
If the user exists and the password is correct, we'll create a new session.
And finally, we'll redirect the user to the dashboard.
As you can probably tell, Lucia abstracts away a lot of the complexity of authentication. All we need to do is call the right methods, and Lucia will handle the rest.
No need to, hash passwords, create sessions, or manage cookies. Lucia does it all for us. And it's all type-safe!
Creating the signup page
Create a new file in src/routes/auth/signup/+page.svelte
.
The signup page is very similar to the signin page, so there's not much to explain here. The only difference is that we're asking for a password confirmation.
Handling the signup request
Create a new file in src/routes/auth/signup/+page.server.ts
.
For the signup we will need to use a similar pattern as with our signup form but there will be few differences starting from data retrieval from the form, field's validation up to the point of creating the user and handling already existing users.
The imports are the same as with the signin page, so we'll skip that part.
We'll start by getting the form data from the request object, and do some basic housekeeping. We'll also check if the passwords match.
Next, we'll try to find the user by email using Drizzle ORM. If the user exists, we'll return a 400 response along with an error message.
If the user doesn't exist, we'll create a new user using Lucia.
And finally, we'll create a new session and redirect the user to the dashboard.
Bonus: Creating the signout page
While we're at it, let's create an endpoint to sign out the user. Create a new file in src/routes/auth/signout/+server.ts
.
And add the following code:
Again Lucia has our back by making it incredibly easy to invalidate sessions. All we need to do now is call this endpoint when the user clicks the sign out button.