Authenticating users in Astro using Neon Postgres and Lucia Auth
Step-by-step guide to building user authentication in Astro application with Lucia Auth and Postgres powered by Neon
This guide covers the step-by-step process of building user authentication APIs and HTML pages in Astro application with Lucia Auth and Postgres, powered by Neon. User authentication provides a way to manage user identities and access control in your application. Upon completing the guide, you would have an understanding of how to perform user authentication using Lucia Auth and protect a page from unauthorized access.
Prerequisites
To follow along this guide, you will need the following:
- Node.js 18 or later
- A Neon account
- A Vercel account
Steps
- Provisioning a Serverless Postgres powered by Neon
- Create a new Astro application
- Add Tailwind CSS to the application
- Enabling Server Side Rendering in Astro with Vercel
- Setting up a Postgres Database Connection and Schema
- Setup Lucia Auth with Neon Postgres
- Define the Astro application routes
- Build the User Authentication Routes
- Deploy To Vercel
Provisioning a Serverless Postgres powered by Neon
Using Serverless Postgres database powered by Neon helps you scale down to zero. With Neon, you only have to pay for what you use.
To get started, go to the Neon console and enter the name of your choice as the project name. You can pick a region near where you will deploy your Astro application. By default, version 16 of Postgres is used. Finally, click on Create Project to create the Postgres database named neondb
(by default).
You will then be presented with a dialog that provides a connecting string of your database. Click on Pooled connection on the top right of the dialog and the connecting string automatically updates in the box below it.
All Neon connection strings have the following format:
user
is the database user.password
is the database user’s password.endpoint_hostname
is the host with neon.tech as the TLD.port
is the Neon port number. The default port number is 5432.dbname
is the name of the database. “neondb” is the default database created with each Neon project.?sslmode=require
an optional query parameter that enforces the SSL mode while connecting to the Postgres instance for better security.
Save this connecting string somewhere safe to be used as the POSTGRES_URL
further in the guide. Proceed further in this guide to create a Astro application.
Create a new Astro application
Let’s get started by creating a new Astro project. Open your terminal and run the following command:
npm create astro
is the recommended way to scaffold an Astro project quickly.
When prompted, choose:
Empty
when prompted on how to start the new project.Yes
when prompted if plan to write Typescript.Strict
when prompted how strict Typescript should be.Yes
when prompted to install dependencies.Yes
when prompted to initialize a git repository.
Once that’s done, you can move into the project directory and start the app:
The app should be running on localhost:4321.
Next, in your first terminal window, run the command below to install the necessary libraries and packages for building the application:
The above command installs the packages passed to the install command, with the -D flag specifying the libraries intended for development purposes only.
The libraries installed include:
pg
: A PostgreSQL client for Node.js.
The development-specific libraries include:
@types/pg
: Type definitions for pg.tsx
: To execute and rebuild TypeScript efficiently.dotenv
: A library for handling environment variables.
Then, make the following additions in your astro.config.mjs
file to populate the environment variables and make them accessible via process.env
object as well:
Then, make the following additions in your tsconfig.json
file to make relative imports within the project easier:
Add Tailwind CSS to the application
For styling the app, you will be using Tailwind CSS. Install and set up Tailwind at the root of our project’s directory by running:
When prompted, choose:
Yes
when prompted to install the Tailwind dependencies.Yes
when prompted to generate a minimaltailwind.config.mjs
file.Yes
when prompted to make changes to Astro configuration file.
With choices as above, the command finishes integrating TailwindCSS into your Astro project. It installed the following dependency:
tailwindcss
: TailwindCSS as a package to scan your project files to generate corresponding styles.@astrojs/tailwind
: The adapter that brings Tailwind’s utility CSS classes to every.astro
file and framework component in your project.
Enabling Server Side Rendering in Astro with Vercel
To authenticate users using server-side APIs, you’re going to enable server-side rendering in your Astro application. Execute the following command in your terminal:
When prompted, choose:
Yes
when prompted to install the Vercel dependencies.Yes
when promted to make changes to Astro configuration file.
With choices as above, the command finishes integrating Vercel adapter into your Astro project. It installed the following dependency:
@astrojs/vercel
: The adapter that allows you to deploy server-side rendered Astro application to Vercel.
Setting up a Postgres Database Connection and Schema
In this section, you’ll learn how to configure a secure connection to the Postgres database, create a client to interact with it, and populate the tables in the database.
Set up the database connection
Create an .env
file in the root directory of your project with the following enviroment variable to initiate the setup of a database connection:
The file, .env
should be kept secret and not included in Git history. Ensure that .env
is added to the .gitignore
file in your project.
Create the database client
First, create a postgres
directory in the src
directory by running the following command:
Then, to create a client that interacts with your serverless postgres, create a setup.ts
file inside the src/postgres
directory with the following code:
The code imports the dotenv
configuration, making sure that all the environment variables in the .env
file are present in the runtime. Then, the code imports the pg
library, retrieves the database URL from the environment variables, and uses it to create a new pool instance, which is subsequently exported.
Create the database schema
In the postgres
directory, create a file named schema.ts
with the following code which will allow you to create and populate the user and sessions database tables for authentication.
The code above defines how data will be stored, organized and managed in the database. Using the pool
database instance, it executes an SQL query to create a auth_user
table within the database if it does not already exist. This table comprises of three columns:
- An
id
column for storing random identifiers for each user in the table. - A
username
column for storing unique identifiers for each user in the table. - A
hashed_password
column for storing Argon2id hashed passwords for each user in the table.
A subsequent SQL query creates a user_session
table within the database if it does not already exist. This table comprises three columns:
- An
id
column for storing random identifiers for each session in the table. - An
expires_at
column for storing the timestamp with time zone for each session in the table. - An
user_id
column for the associatedid
of each associated user for each session in the table.
After executing the two SQL queries, a message is printed to the console if there’s an error during the execution.
Finally, to execute the code in the schema file, make the following addition in the scripts
of your package.json
file:
Test the database setup locally
To execute the code within schema.ts
to set up the database, run the following command in your terminal window:
If the command is executed successfully, you will see no logs in your terminal window except Finished setting up the database.
, marking the completion of the schema setup in your Postgres Database powered by Neon.
Setup Lucia Auth with Neon Postgres
To start authenticating users and managing their sessions, install Lucia and Oslo, for various auth utilities by executing the following command in your terminal:
The above command installs the packages passed to the install command. The libraries installed include:
lucia
: An open source auth library that abstracts away the complexity of handling sessions.oslo
: A collection of auth-related utilities.@lucia-auth/adapter-postgresql
: PostgreSQL adapter for Lucia.@neondatabase/serverless
: Neon’s PostgreSQL driver for JavaScript and TypeScript.
Then, create a lucia
directory in the src
directory by running the following command:
Then, create a file index.ts
inside the lucia
directory with the following code:
The code above does the following:
- Imports the Lucia class, Neon HTTP serverless driver and Lucia’s PostgreSQL adapter.
- Creates a one-shot SQL query function compatible with Neon.
- Creates a Lucia adapter with the tables
auth_user
anduser_session
. - Creates a Lucia instance that uses cookies to maintain user sessions. The
auth_session
cookie set by Lucia, is set to besecure
if your application is running in production mode (detected by PROD Vite environment variable). UsinggetUserAttributes
property, the Lucia instance is informed of the attributes that need to be fetched whenever a user information is requested. - Defines types related to the user information and the Lucia instance.
To fetch and validate the current user session, and to verify if the users’ credentials are valid while they’re signing up, create a file user.ts
with the following code:
The code above begins with the importing the lucia instance we created earlier and the types of user’s information and cookies (an Astro internal utility for cookies). Then, it exports the three function as follows:
getSessionID
: it accepts all the cookies received in a request, decode theauth_session
cookie from it, and callsreadSessionCookie
function by lucia. It returns thesession_id
associated with the particular session.getUser
: it calls thevalidateSession
function by lucia that returns the user and session information associated with the given cookies. The user information returned by this function contains only the attributes defined in thegetUserAttributes
we created earlier.validateCredentials
: it accepts in a username and password and returns a particular message if either of them didn’t meet the validity requirements.
Define the Astro application routes
With Astro, creating a .astro
or .(js|ts)
file in the src/pages
directory maps it to a route in your application. The name of the file created maps to the route’s URL pathname (with the exception of index.(astro|ts|js)
, which is the index route).
The structure below is what our pages
folder will look like at the end of this section:
├── signin.astro
├── signup.astro
├── protected.astro
├── api/
├──── sign/
└────── in.ts
└────── up.ts
└────── out.ts
protected.astro
will serve responses with dynamically created HTML to incoming requests at localhost:4321/protected.signin.astro
will serve responses with statically generated HTML to incoming requests at localhost:4321/signin.signup.astro
will serve responses with statically generated HTML to incoming requests at localhost:4321/signup.api/sign/in.ts
will serve responses as an API Endpoint to incoming requests at localhost:4321/api/sign/in.api/sign/up.ts
will serve responses as an API Endpoint to incoming requests at localhost:4321/api/sign/up.api/sign/out.ts
will serve responses as an API Endpoint to incoming requests at localhost:4321/api/sign/out.
Build the User Authentication Routes
For minimal user authentication, a user of your application should be able to sign up, sign in and sign out to your application at a given time. In this section, you’ll build the frontend pages for sign in and sign up and the API routes (in, up and out) that’ll process the user authentication logic for the same.
Build the Sign Up HTML and API Route
Create a file signup.astro
in the src/pages
directory with the following Astro code to serve incoming requests to /signup
:
The above code does the following:
- Exports a
prerender
flag set to (boolean)true
indicating the page to be statically generated at the build time. - Serves an containing a form for users to enter their username and password in.
- Passes the form data to
/api/sign/up
API Endpoint when user submits their information.
Let’s create the endpoint for users to sign up with. Create a file api/sign/up.ts
inside the src/pages
directory with the following code:
The above code does the following for incoming requests to /api/sign/up
:
- Imports the Postgres pool, the initialized lucia instance,
generateId
,Argon2id
andvalidateCredentials
functions. - Exports a
POST
function indiciating that the API route would only process the incoming POST requests. - Parses the form data in the request.
- Extracts username and password from the form data. Validates them using the
validateCredentials
function created earlier. - Creates a hashed password using Argon2id.
- Attempts to create the user in
auth_user
table using the pg client. Asusername
is a unique field in the database, any conflicts will result in an error response from the endpoint. - Creates a user session using
createSession
helper by Lucia. - Sets the cookie pertaining to the user session and redirect to the
/protected
page.
Build the Sign In HTML and API Route
Create a file signin.astro
in the src/pages
directory with the following Astro code to serve incoming requests to /signin
:
The above code does the following:
- Exports a
prerender
flag set to (boolean)true
indicating the page to be statically generated at the build time. - Serves an containing a form for users to enter their username and password in.
- Passes the form data to
/api/sign/in
API Endpoint when user submits their information.
Let’s create the endpoint for users to sign in with. Create a file api/sign/in.ts
inside the src/pages
directory with the following code:
The above code does the following for incoming requests to /api/sign/in
:
- Imports the Postgres pool, the initialized lucia instance,
Argon2id
andvalidateCredentials
functions. - Exports a
POST
function indiciating that the API route would only process the incoming POST requests. - Parses the form data in the request.
- Extracts username and password from the form data. Validates them using the
validateCredentials
function created earlier. - Checks for an existing user with the given username. If not found, sends a response with 400 Bad Request status.
- Otherwise, verifies the hashed password in the Neon Postgres with the password user entered. If they don’t match, sends a response with 400 Bad Request status.
- Otherwise, creates a user session using
createSession
helper by Lucia. - Sets the cookie pertaining to the user session and redirect to the
/protected
page.
Build the Sign Out API Route
Let’s create the endpoint for users to sign out with. Create a file api/sign/out.ts
inside the src/pages
directory with the following code:
The above code does the following for incoming requests to /api/sign/out
:
- Imports the initialized lucia instance, and
getSessionID
function. - Exports a
GET
function indiciating that the API route would only process the incoming GET requests. - Invalidates the current user session using the
invalidateSession
helper by Lucia. - Creates an empty user session cookie using
createBlankSessionCookie
helper by Lucia. - Sets the blank user session cookie.
- Redirects to the
/protected
page.
Build the Protected HTML Page
Create a file protected.astro
in the src/pages
directory with the following Astro code to serve incoming requests to /protected
:
The code above does the following to the incoming requests at /protected
:
- Imports the
getUser
function (created earlier) - Using
Astro.cookies
, verifies if there is an authenticated user with the current request. If not found, returns a 403 Forbidden status. - Otherwise, it serves the protected HTML.
Great! Now you’re able to authenticate users and protected specific rouets in your application using API Endpoints in Astro and Lucia Auth.
Deploy to Vercel
The code is now ready to deploy to Vercel. Use the following steps to deploy:
- Start by creating a GitHub repository containing your app’s code.
- Then, navigate to the Vercel Dashboard and create a New Project.
- Link the new project to the GitHub repository you just created.
- In Settings, update the Environment Variables to match those in your local
.env
file. - Deploy!
Summary & Final Thoughts
In this guide, you learned how to authenticate users in your Astro application using Lucia Auth and Serverless Postgres Database powered by Neon. Further, you learned how to create protected routes that are forbidden for un-authenticated users.
For more, join us on our Discord server to share your experiences, suggestions, and challenges.