Build authenticated and paywall pages with Stripe and Xata
Tags: database, tutorial, node, learning
Published: 2024-05-08 17:10:53


In this post, you'll learn how to authenticate users using Xata, create Stripe checkouts and respond to Stripe webhooks in an Astro application.

- Set up Xata
- Create a schema with different column types
- Programmatically create Stripe checkout sessions
- Respond to Stripe webhooks
- Authenticate users with Lucia Auth

## Before you begin

### Prerequisites

You'll need the following:

- [Node.js 18](https://nodejs.org/en/blog/announcements/v18-release-announce) or later
- [pnpm](https://pnpm.io/installation) package manager
- A [Xata](https://xata.io/) account
- A [Stripe](https://stripe.com) account

### Tech Stack

The following technologies are used in this guide:

| Technology                            | Description                                                                        |
| ------------------------------------- | ---------------------------------------------------------------------------------- |
| [Xata](https://xata.io/)              | Serverless Postgres database platform for scalable, real-time applications.        |
| [Astro](https://astro.build/)         | Framework for building fast, modern websites with serverless backend support.      |
| [Stripe](https://stripe.com)          | A platform to accept payments globally.                                            |
| [Lucia Auth](https://lucia-auth.com/) | An authentication library that abstracts away the complexity of handling sessions. |

## Steps

To complete this guide, you will need to follow these steps:

- [Create a new Astro application](#create-a-new-astro-application)
- [Add Tailwind CSS to the application](#add-tailwind-css-to-the-application)
- [Enable Server Side Rendering in Astro](#enabling-server-side-rendering-in-astro)
- [Set up a Xata Database](#setting-up-a-xata-database)
  - [Install the Xata CLI](#install-the-xata-cli)
- [Create Stripe Checkout Sessions](#create-stripe-checkout-sessions)
- [Respond to Stripe Webhooks](#respond-to-stripe-webhooks)
- [Authenticate users with Lucia Auth and Xata](#authenticating-users-with-lucia-auth-and-xata)
  - [Build a Xata Adapter for Lucia Auth](#building-xata-adapter-for-lucia-auth)
  - [Initialize Lucia with Xata](#initialize-lucia-with-xata)
  - [Create Utilities for User Authentication Status](#create-utilities-for-user-authentication-status)
  - [Build Authentication Routes with Lucia Auth](#building-authentication-routes-with-lucia-auth)
    - [Build the Sign Up HTML and API Route](#build-the-sign-up-html-and-api-route)
    - [Build the Sign In HTML and API Route](#build-the-sign-in-html-and-api-route)
    - [Build the Sign Out API Route](#build-the-sign-out-api-route)
    - [Create Protected and Paid Routes](#create-protected-and-paid-routes)
- [Deploy to Vercel](#deploy-to-vercel)

## Create a new Astro application

Let’s get started by creating a new Astro project. Open your terminal and run the following command:

```bash
pnpm create astro@latest protected-and-paid-stripe-xata
```

`pnpm 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 you plan to write using 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:

```bash
cd protected-and-paid-stripe-xata
pnpm dev
```

The app should be running on [localhost:4321](http://localhost:4321/). Now, let's close the development server as we move on to integrating Tailwind CSS in the application.

## Add Tailwind CSS to the application

For styling the app, you will use Tailwind CSS. Install and set up Tailwind at the root of your project's directory by running:

```bash
pnpm astro add tailwind
```

When prompted, choose:

- `Yes` when prompted to install the Tailwind dependencies.
- `Yes` when prompted to generate a minimal `tailwind.config.mjs` file.
- `Yes` when prompted to make changes to the Astro configuration file.

The command then 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.

Now, let's move on to enabling server side rendering in the application.

## Enabling Server Side Rendering in Astro

To create checkouts using the Stripe API dynamically, you will need to server-side render your Astro application. Enable server side rendering in your Astro project by executing the following command in your terminal:

```bash
pnpm add @astrojs/vercel
```

When prompted, choose:

- `Yes` when prompted to install the Vercel dependencies.
- `Yes` when prompted to make changes to the Astro configuration file.

The command above installed the following dependency:

- `@astrojs/vercel`: The adapter that allows you to server-side render your Astro application on Vercel.

Now, let's move on to integrating Xata in the application.

## Setting up a Xata Database

After you've created a Xata account and are logged in, create a database by clicking on `+ Add database`.

![Create Xata database](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/195glrmykspogkyfdnt6.png)

### Install the Xata CLI

Obtain the Xata initialization command by clicking on a table (say, `user`), and then on the `Get code snippet` button.

![View User Table](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ztgn6cijem8siwrvrc7g.png)

A modal titled `Code snippets` is presented which contains a set of commands to integrate Xata into your project, locally.

![Code Snippet for Xata CLI](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w2prd5o7loar7w7gep8i.png)

For interacting with your Xata database using the TypeScript SDK, install the Xata CLI globally by executing the following command in your terminal:

```bash
# Installs the CLI globally
npm install -g @xata.io/cli
```

Then, link your Xata project to your Astro application by executing the following command in your terminal window:

```bash
# Initialize your project locally with the Xata CLI
xata init --db https://Rishi-Raj-Jain-s-workspace-80514q.us-east-1.xata.sh/db/astro-stripe-protected-and-paid
```

Use the following answers to the Xata CLI one-time setup question prompts to integrate Xata with Astro:

- `Yes` when prompted to add `.env` to `.gitignore`.
- `src/xata.ts` when prompted to enter the output path for the generated code by the Xata CLI.

![Setup Astro project with Xata CLI](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/luoj4uwx7hpz1jxnkotl.png)

Now, let’s move onto creating API endpoints that create checkouts dynamically using Stripe.

## Create Stripe Checkout Sessions

A Stripe checkout session, in this case, would refer to a payment link hosted by Stripe where users can pay for a given product.

Let's use the index route to display a buy button that would take users to the Stripe hosted checkout. Create an index route (`src/pages/index.astro`) with the following code:

```tsx
---
// File: src/pages/index.astro

## export const prerender = true

<html lang="en">
 <head>
   <meta charset="utf-8" />
   <meta name="viewport" content="width=device-width" />
 </head>
 <body class="font-display overflow-x-hidden">
   <form class="p-12" method="post" action="/api/stripe/checkout">
     <button class="rounded border border-black px-4 py-2 duration-150 hover:scale-105">Get My Product &rarr;</button>
   </form>
 </body>
</html>
```

The code above creates an index route in the Astro application containing a form that POSTs to `/api/stripe/checkout`. To handle the form submission, create a file `src/pages/api/stripe/checkout.ts` with the following code:

```tsx
// File: src/pages/api/stripe/checkout.ts

import Stripe from 'stripe';
import type { APIContext } from 'astro';

export async function POST({ redirect }: APIContext) {
  const STRIPE_SECRET_KEY = import.meta.env.STRIPE_SECRET_KEY;
  if (!STRIPE_SECRET_KEY) return new Response(null, { status: 500 });
  const stripe = new Stripe(STRIPE_SECRET_KEY, { apiVersion: '2023-10-16' });
  const session = await stripe.checkout.sessions.create({
    mode: 'payment',
    payment_method_configuration: 'pmc_1O2qH3SE9voLRYpuz5FLmkvn',
    line_items: [
      {
        quantity: 1,
        price_data: {
          unit_amount: 0,
          currency: 'usd',
          product: 'prod_OqWkk7Rz9Yw18f'
        }
      }
    ],
    success_url: 'http://localhost:4321'
  });
  return redirect(session.url);
}
```

The code above does the following:

- Imports the Stripe SDK and the type APIContext by Astro.
- Exports a `POST` HTTP request handler.
- Validates the presence of `STRIPE_SECRET_KEY` environment variable.
- Creates a new stripe object using the secret key and the latest api version.
- Creates a checkout with a given item adjusted to be for free. Having a checkout containing free items will help you quickly test a purchase flow manually.
- Redirects user to the newly created checkout's URL.

Let's move on to handling the webhook requests triggered by Stripe if there's a successful purchase on the payment link.

## Respond to Stripe Webhooks

Upon a successful purchase, Stripe fires a webhook POST request. To handle the webhook requests in Astro via server-side code, create a `src/pages/api/stripe/webhook.ts` file with the following code:

```tsx
// File: src/pages/api/stripe/webhook.ts

import Stripe from 'stripe';
import { getXataClient } from '@/xata';
import type { APIContext } from 'astro';

// Process rawBody from the request Object
async function getRawBody(request: Request) {
  let chunks = [];
  let done = false;
  const reader = request.body.getReader();
  while (!done) {
    const { value, done: isDone } = await reader.read();
    if (value) {
      chunks.push(value);
    }
    done = isDone;
  }
  const bodyData = new Uint8Array(chunks.reduce((acc, chunk) => acc + chunk.length, 0));
  let offset = 0;
  for (const chunk of chunks) {
    bodyData.set(chunk, offset);
    offset += chunk.length;
  }
  return Buffer.from(bodyData);
}

// Stripe API Reference
// https://stripe.com/docs/webhooks#webhook-endpoint-def
export async function POST({ request }: APIContext) {
  try {
    const STRIPE_SECRET_KEY = import.meta.env.STRIPE_SECRET_KEY;
    const STRIPE_WEBHOOK_SIG = import.meta.env.STRIPE_WEBHOOK_SIG;
    if (!STRIPE_SECRET_KEY || !STRIPE_WEBHOOK_SIG) return new Response(null, { status: 500 });
    const stripe = new Stripe(STRIPE_SECRET_KEY, { apiVersion: '2023-10-16' });
    const rawBody = await getRawBody(request);
    let event = JSON.parse(rawBody.toString());
    const sig = request.headers.get('stripe-signature');
    try {
      event = stripe.webhooks.constructEvent(rawBody, sig, STRIPE_WEBHOOK_SIG);
    } catch (err) {
      console.log(err.message);
      return new Response(`Webhook Error: ${err.message}`, { status: 400 });
    }
    if (event.type === 'checkout.session.completed' || event.type === 'payment_intent.succeeded') {
      const email = event.data.object?.customer_details?.email;
      if (email) {
        const xata = getXataClient();
        const existingRecord = await xata.db.user.filter({ email }).getFirst();
        if (existingRecord) {
          await xata.db.user.update(existingRecord.id, { paid: true });
        } else {
          await xata.db.user.create({ email, paid: true });
        }
        return new Response('marked the user as paid', { status: 200 });
      }
      return new Response('no email of the user is found', { status: 200 });
    }
    return new Response(JSON.stringify(event), { status: 404 });
  } catch (e) {
    return new Response(e.message || e.toString(), { status: 500 });
  }
}
```

The code above does the following:

- Imports the Stripe class from its SDK, `APIContext` type by Astro and `getXataClient` to access the in-memory Xata instance.
- Exports a POST HTTP handler to only respond to incoming POST request(s).
- Validates the presence of `STRIPE_SECRET_KEY` and `STRIPE_WEBHOOK_SIG` environment variables.
- Creates a Stripe webhook event from the incoming Stripe webhook request body.
- Checks for an existing record with the email entered during the checkout.
- If a record is found, the `paid` attribute is set to `true`.
- Otherwise, it creates a record with the email and `paid` attribute as `true`.

Let's move on to authenticating users with your Xata database and a Lucia library.

## Authenticating users with Lucia Auth and Xata

To start authenticating users and managing their sessions, install Lucia and Oslo, for various authentication utilities by executing the following command in your terminal:

```bash
pnpm add lucia oslo dotenv
```

The above command installs the packages passed to the `install` command. The libraries installed include:

- `lucia`: An open source authentication library that abstracts away the complexity of handling sessions.
- `dotenv`: A library for handling environment variables.
- `oslo`: A collection of authentication related utilities.

```diff
// File: astro.config.mjs

+ import 'dotenv/config'
import node from '@astrojs/node'
import tailwind from '@astrojs/tailwind'
import { defineConfig } from 'astro/config'

export default defineConfig({
  output: 'server',
  adapter: node({
    mode: 'standalone',
  }),
  integrations: [tailwind()],
+  vite: {
+    optimizeDeps: {
+      exclude: ['oslo'],
+    },
+  },
})
```

The code additions above do the following:

- Imports dotenv which makes all the environment variables accessible via the `process.env` object.
- Uses vite's [optimizeDeps](https://vitejs.dev/config/dep-optimization-options#optimizedeps-exclude) to exclude the `oslo` library from being pre-bundled, effectively, using the library as is.

Now, create a Lucia directory in the src directory by running the following command:

```bash
mkdir src/lucia
```

Let's now move on to creating a Xata adapter for our Lucia library.

### Building a Xata Adapter for Lucia Auth

Create a `src/lucia/xata.ts` file with the following code:

```tsx
// File: src/lucia/xata.ts

import { getXataClient } from '@/xata';
import type {
  Adapter,
  DatabaseSession,
  RegisteredDatabaseSessionAttributes,
  DatabaseUser,
  RegisteredDatabaseUserAttributes,
  UserId
} from 'lucia';
```

The code above imports the `getXataClient` utility to retrieve the in-memory Xata instance. It then imports the relevant types from the `lucia` module. Append the following code:

```tsx
// File: src/lucia/xata.ts

// ...

interface UserSchema extends RegisteredDatabaseUserAttributes {
  id: string;
}

interface SessionSchema extends RegisteredDatabaseSessionAttributes {
  id: string;
  user_id: string;
  expires_at: Date;
}
```

The code above defines the following types:

- `UserSchema`: A type created with `RegisteredDatabaseUserAttributes` type definition alongwith `id` attribute as a string.
- `SessionSchema`: A type created with `RegisteredDatabaseSessionAttributes` type definition along with `id` attribute as a string, `user_id` attribute as a string and `expires_at` attribute as a Date.

Append the following code to handle data transformations to Lucia's expected structure:

```tsx
// File: src/lucia/xata.ts

// ...

function transformIntoDatabaseSession(raw: SessionSchema): DatabaseSession {
  const { id, user_id: userId, expires_at: expiresAt, ...attributes } = raw;
  return {
    id,
    userId,
    expiresAt,
    attributes
  };
}

function transformIntoDatabaseUser(raw: UserSchema): DatabaseUser {
  const { id, ...attributes } = raw;
  return {
    id,
    attributes
  };
}
```

The code above defines the following methods:

- `transformIntoDatabaseSession`: returns the `SessionSchema` object in the shape of `DatabaseSession` type.
- `transformIntoDatabaseUser`: returns the `UserSchema` object in the shape of `DatabaseUser` type.

Append the following code to create the XataAdapter class:

```tsx
// File: src/lucia/xata.ts

// ...

export class XataAdapter implements Adapter {
  private controller = getXataClient();

  // ...
}
```

The code above defines a class XataAdapter based on Lucia's Adapter class. Then, it sets a `controller` private class variable equivalent to the Xata instance obtained from the `getXataClient` method. Define a method to delete a session by appending the following code:

```tsx
// File: src/lucia/xata.ts

// ...

export class XataAdapter implements Adapter {
  // ...

  public async deleteSession(sessionId: string): Promise<void> {
    await this.controller.sql`DELETE FROM "session" WHERE id=${sessionId}`;
  }
}
```

The code above defines a `deleteSession` method that uses Xata SQL queries to delete a particular session from your database. Define a method to delete all the sessions for a user by appending the following code:

```tsx
// File: src/lucia/xata.ts

// ...

export class XataAdapter implements Adapter {
  // ...

  public async deleteUserSessions(userId: UserId): Promise<void> {
    await this.controller.sql`DELETE FROM "session" WHERE user_id=${userId}`;
  }
}
```

The code above defines a `deleteUserSessions` method that uses Xata SQL queries to delete all sessions pertaining to a particular user in your database. Define a method to retrieve all the sessions for a user by appending the following code:

```tsx
// File: src/lucia/xata.ts

// ...

export class XataAdapter implements Adapter {
  // ...

  public async getUserSessions(user_id: UserId): Promise<DatabaseSession[]> {
    const records = await this.controller.db.session.filter({ user_id }).getAll();
    return records.map((val) => {
      return transformIntoDatabaseSession(val);
    });
  }
}
```

The code above defines a `getUserSessions` method that uses Xata SDK methods to retrieve all sessions pertaining to a particular user in your database. Define a method to create a session by appending the following code:

```tsx
// File: src/lucia/xata.ts

// ...

export class XataAdapter implements Adapter {
  // ...

  public async setSession(databaseSession: DatabaseSession): Promise<void> {
    await this.controller.db.session.create({
      id: databaseSession.id,
      user_id: databaseSession.userId,
      expires_at: databaseSession.expiresAt,
      ...databaseSession.attributes
    });
  }
}
```

The code above defines a `setSession` method that uses Xata SDK methods to insert a record in a session table in your database. Define a method to update a session by appending the following code:

```tsx
// File: src/lucia/xata.ts

// ...

export class XataAdapter implements Adapter {
  // ...

  public async updateSessionExpiration(sessionId: string, expiresAt: Date): Promise<void> {
    await this.controller.sql`UPDATE "session" SET expires_at=${expiresAt} WHERE id=${sessionId}`;
  }
}
```

The code above defines an `updateSessionExpiration` method that uses a Xata SQL query to update the `expires_at` attribute in a particular session in your database. Define a method to delete all expired sessions by appending the following code:

```tsx
// File: src/lucia/xata.ts

// ...

export class XataAdapter implements Adapter {
  // ...

  public async deleteExpiredSessions(): Promise<void> {
    await this.controller.sql`DELETE FROM "session" WHERE expires_at <=${new Date()}`;
  }
}
```

The code above defines a `deleteExpiredSessions` method that uses a Xata SQL query to delete all the sessions whose `expires_at` attribute have expired. Define a method to obtain a session by appending the following code:

```tsx
// File: src/lucia/xata.ts

// ...

export class XataAdapter implements Adapter {
  // ...

  private async getSession(session_id: string): Promise<DatabaseSession | null> {
    const records = await this.controller.db.session.filter({ session_id }).getFirst();
    return transformIntoDatabaseSession(records);
  }
}
```

The code above defines a `getSession` method that uses Xata SDK methods to retrieve a single matching record for a given session from your database. Define a method to obtain a user associated with a session by appending the following code:

```tsx
// File: src/lucia/xata.ts

// ...

export class XataAdapter implements Adapter {
  // ...

  private async getUserFromSessionId(session_id: string): Promise<DatabaseUser | null> {
    const theSession = await this.controller.db.session.filter({ session_id }).getFirst();
    if (theSession) {
      const { user_id } = theSession;
      const getUser = await this.controller.db.user.filter({ user_id }).getFirst();
      if (getUser) return transformIntoDatabaseUser(getUser);
    }
    return null;
  }
}
```

The code above defines a `getUserFromSessionId` method that uses Xata SDK methods to retrieve a single matching record for a given session from your database. If a session is found, it further queries to obtain the user associated with the session. Otherwise, it returns `null`.

```tsx
// File: src/lucia/xata.ts

// ...

export class XataAdapter implements Adapter {
  // ...

  public async getSessionAndUser(
    sessionId: string
  ): Promise<[session: DatabaseSession | null, user: DatabaseUser | null]> {
    const [databaseSession, databaseUser] = await Promise.all([
      this.getSession(sessionId),
      this.getUserFromSessionId(sessionId)
    ]);
    return [databaseSession, databaseUser];
  }
}
```

The code above defines a `getSessionAndUser` method that uses the `getSession` and `getUserFromSessionId` methods to obtain both the user and session information associated with the particular session id.

The XataAdapter class is now ready to be used as Lucia's adapter.

### Initialize Lucia with Xata

To use Xata with Lucia, create a file `index.ts` inside the `lucia` directory with the following code:

```tsx
// File: src/lucia/index.ts

import { Lucia } from 'lucia';
import { XataAdapter } from './xata';

const adapter = new XataAdapter();

export const lucia = new Lucia(adapter, {
  sessionCookie: {
    attributes: {
      secure: import.meta.env.PROD
    }
  },
  getUserAttributes: (attributes) => {
    return {
      paid: attributes.paid,
      email: attributes.email
    };
  }
});
```

The code above does the following:

- Imports the Lucia and XataAdapter classes.
- Creates a new instance of XataAdatper as `adapter`.
- Uses the `adapter` to create a new Lucia instance wherein the session cookie is set to `Secure` if the application is running in the production environment.
- The `lucia` instance defines the `getUserAttributes` function to only fetch `paid` and `email` attributes from the user record(s).

To create type definitions for the associated user and session, append the following code:

```tsx
// File: src/lucia/index.ts

// ...

interface DatabaseUserAttributes {
  email: string;
  paid: boolean | null;
}

declare module 'lucia' {
  interface Register {
    Lucia: typeof lucia;
    DatabaseUserAttributes: DatabaseUserAttributes;
  }
}
```

The code above defines the following types:

- `DatabaseUserAttributes`: An object that would contain an `email` attribute as a `string`, and `paid` attribute as `boolean` or `null`.
- `Lucia`: As the type of the lucia instance created.

### Create Utilities for User Authentication Status

To retrieve the authentication status of the session, you will need to obtain the user information associated with the request. Create a file `user.ts` with the following code:

```tsx
// File: src/lucia/user.ts

import { lucia } from '.';
import type { User } from 'lucia';
import type { AstroCookies } from 'astro';

export function getSessionID(cookies: AstroCookies): string | null {
  const auth_session = cookies.get('auth_session');
  if (!auth_session) return null;
  return lucia.readSessionCookie(`auth_session=${auth_session.value}`);
}

export async function getUser(cookies: AstroCookies): Promise<User | null> {
  const session_id = getSessionID(cookies);
  if (!session_id) return null;
  const { user } = await lucia.validateSession(session_id);
  return user;
}
```

The code above does the following:

- Imports the `lucia` instance and the `User` type by Lucia.
- Exports a `getSessionID` function which uses Lucia's `readSessionCookie` utility to obtain the session ID.
- Exports a `getUser` function which uses the `getSessionID` function to obtain the session id. Then, it uses Lucia's `validateSession` utility to obtain the user data information with the session.

Let's move on to creating simple user authentication forms and the API routes to handle the authentication logic.

### Building Authentication Routes with Lucia Auth

In this section, you will learn how to create simple forms in Astro, and use the form data in Astro endpoints to sign up, sign out and sign in the users in your application.

#### Build the Sign Up HTML and API Route

To serve responses to a `/signup` route in Astro, create a `src/pages/signup.astro` file with the following code:

```tsx
---
// File: src/pages/signup.astro

export const prerender = true
---

<html lang="en">
 <head>
   <meta charset="utf-8" />
   <meta name="viewport" content="width=device-width" />
 </head>
 <body class="font-display overflow-x-hidden">
   <h1>Sign Up</h1>
   <form method="post" action="/api/sign/up">
     <label for="email">Email</label>
     <input id="email" name="email" />
     <label for="password">Password</label>
     <input id="password" name="password" />
     <button>Continue</button>
   </form>
 </body>
</html>
```

The code above creates a sign up route in the Astro application containing a form that POSTs to `/api/sign/up` with the user's email and password. Create a `src/pages/api/sign/up.ts` file with the following code to handle the form submission:

```tsx
// File: src/pages/api/sign/up.ts

import { generateId } from 'lucia';
import { lucia } from '@/lucia/index';
import { getXataClient } from '@/xata';
import type { APIContext } from 'astro';
import { Argon2id } from 'oslo/password';

export async function POST({ request, redirect, cookies }: APIContext): Promise<Response> {
  const xata = getXataClient();
  const user_id = generateId(15);
  const formData = await request.formData();
  const email = (formData.get('email') as string).trim();
  const password = formData.get('password') as string;
  const hashed_password = await new Argon2id().hash(password);
  const existingRecord = await xata.db.user.filter({ email }).getFirst();
  if (existingRecord) {
    if (existingRecord.hashed_password !== null) return redirect('/signin');
    await xata.db.user.update(existingRecord.id, { user_id, hashed_password });
  } else {
    await xata.db.user.create({ email, user_id, hashed_password });
  }
  const session = await lucia.createSession(user_id, {});
  const sessionCookie = lucia.createSessionCookie(session.id);
  cookies.set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
  return redirect('/');
}
```

The code above does the following:

- Imports a utility from the Lucia SDK to create random ID(s), the `getXataClient` utility to obtain the in-memory Xata instance, Argon2id class by `oslo` to hash given password(s).
- Exports a POST HTTP handler to only respond to incoming POST request(s).
- Obtains the email and password from the request's form data.
- Hashes the password using Argon2id's `hash` method.
- Retrieves a single matching record using the email from form data.
- If no such record is found, it creates a record in the user table with the email, user id and hashed password.
- Otherwise, if the hashed password is present in the matching record, it redirects to `/signin` route. Otherwise, it updates the existing record with the newly signed up user.
- Creates a session using `createSession` helper utility by Lucia and sets the cookie.
- Redirects to the index route.

#### Build the Sign In HTML and API Route

To serve responses to a `/signin` route in Astro, create a `src/pages/signin.astro` file with the following code:

```tsx
---
// File: src/pages/signin.astro

export const prerender = true
---

<html lang="en">
 <head>
   <meta charset="utf-8" />
   <meta name="viewport" content="width=device-width" />
 </head>
 <body class="font-display overflow-x-hidden">
   <h1>Sign In</h1>
   <form method="post" action="/api/sign/in">
     <label for="email">Email</label>
     <input id="email" name="email" />
     <label for="password">Password</label>
     <input id="password" name="password" />
     <button>Continue</button>
   </form>
 </body>
</html>
```

The code above creates a sign in route in the Astro application containing a form that POSTs to `/api/sign/in` with the user's email and password. Create a `src/pages/api/sign/in.ts` file with the following code to handle the form submission:

```tsx
// File: src/pages/api/sign/in.ts

import { lucia } from '@/lucia/index';
import { getXataClient } from '@/xata';
import type { APIContext } from 'astro';
import { Argon2id } from 'oslo/password';

export async function POST({ request, cookies, redirect }: APIContext): Promise<Response> {
  const xata = getXataClient();
  const formData = await request.formData();
  const email = (formData.get('email') as string).trim();
  const password = formData.get('password') as string;
  const existingRecord = await xata.db.user.filter({ email }).getFirst();
  if (!existingRecord) return new Response('Incorrect email or password', { status: 400 });
  const validPassword = await new Argon2id().verify(existingRecord.hashed_password, password);
  if (!validPassword) return new Response('Incorrect email or password', { status: 400 });
  const session = await lucia.createSession(existingRecord.user_id, {});
  const sessionCookie = lucia.createSessionCookie(session.id);
  cookies.set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
  return redirect('/');
}
```

The code above does the following:

- Imports the initialized `lucia` instance and `getXataClient` helper utility.
- Exports a POST HTTP handler to only respond to incoming POST request(s).
- Obtains the email and password from the request's form data.
- Looks for an existing record with the email in user table of your database.
- If not found, it returns a 400 Bad Request response.
- Otherwise, it uses `Argon2id` to verify the hashed password.
- If the hashed password does not match, it returns a 400 Bad Request response.
- Otherwise, it creates a session using `createSession` helper utility by Lucia and sets the cookie.
- Redirects to the index route.

#### Build the Sign Out API Route

To sign out users in your Astro application, create a `src/pages/api/sign/out.ts` file with the following code:

```tsx
// File: src/pages/api/sign/out.ts

import { lucia } from '@/lucia/index';
import type { APIContext } from 'astro';
import { getSessionID } from '@/lucia/user';

export async function GET({ cookies, redirect }: APIContext): Promise<Response> {
  await lucia.invalidateSession(getSessionID(cookies));
  const sessionCookie = lucia.createBlankSessionCookie();
  cookies.set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
  return redirect('/');
}
```

The code above does the following:

- Imports the initialized `lucia` instance and `getSessionID` helper utility.
- Exports a GET HTTP handler to only respond to incoming GET request(s).
- Invalidates the current user session using the `invalidateSession` utility by Lucia.
- Creates an empty user session cookie using `createBlankSessionCookie` utility by Lucia.
- Sets the blank user session cookie.
- Redirects to the index route.

#### Create Protected and Paid Routes

To restrict access to a page for only paid and authenticated users, you will obtain the user information using the utilities created earlier. Create a file `src/pages/protected_and_paid.astro` with the following code:

```tsx
---
// File: src/pages/protected_and_paid.astro

import { getUser } from '@/lucia/user'

const user = await getUser(Astro.cookies)

if (!user || user.paid !== true) return new Response(null, { status: 403 })
---

<html lang="en">
 <head>
   <meta charset="utf-8" />
   <meta name="viewport" content="width=device-width" />
 </head>
 <body class="font-display overflow-x-hidden">
   <span> Protected and Paid Content </span>
 </body>
</html>
```

The code above creates a `/protected_and_paid` route in the Astro application that does the following:

- Checks if there's a user associated with the session and if they have paid.
- If the above is not true, it returns a 403 Unauthorized response. Otherwise, it returns an HTML page containing the paid and protected information.

To restrict access to a page for only authenticated users, you will obtain the user information using the utilities created earlier. Create a file `src/pages/protected.astro` with the following code:

```tsx
---
import { getUser } from '@/lucia/user'

const user = await getUser(Astro.cookies)

if (!user) return new Response(null, { status: 403 })
---

<html lang="en">
 <head>
   <meta charset="utf-8" />
   <meta name="viewport" content="width=device-width" />
 </head>
 <body class="font-display overflow-x-hidden">
   <span> Protected Content </span>
 </body>
</html>
```

The code above creates a `/protected` route in the Astro application that does the following:

- Checks if there's a user associated with the session.
- If the above is not true, it returns a 403 Unauthorized response. Otherwise, it returns an HTML page containing the protected information.

## Deploy to Vercel

The repository, 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've just created.
- In **Settings**, update the **Environment Variables** to match those in your local `.env` file.
- Deploy! 🚀

## More Information

For more detailed insights, explore the references cited in this post.

- [GitHub Repo](https://github.com/rishi-raj-jain/protected-and-paid-stripe-xata)
- [Adapter - Lucia Auth](https://lucia-auth.com/reference/main/Adapter)
- [Credentials Lucia Auth in Astro](https://lucia-auth.com/tutorials/username-and-password/astro)
- [Create a Stripe Checkout Session](https://docs.stripe.com/api/checkout/sessions/create)

## What’s next?

We'd love to hear from you if you have any feedback on this tutorial, would like to know more about Xata, or if you would like to contribute a community blog or tutorial. Reach out to us on [Discord](https://discord.com/invite/kvAcQKh7vm) or join us on [X | Twitter](https://twitter.com/xata). Happy building 🦋