← Back to Blog

Build a Full-Stack App with Fauna, Next.js, and Auth0

A tutorial on building a serverless full-stack application using FaunaDB for the database, Next.js for the frontend, and Auth0 for authentication.

faunanextjsauth0serverless

Introduction

This article demonstrates creating a bookmark management platform where authenticated users can create shortened URLs and manage public/private bookmark visibility.

The technology stack includes:

  • Next.js for server and client-side rendering with API routes deployed as serverless functions
  • Fauna as the serverless database for storing bookmark data
  • Auth0 for handling user authentication and authorization

All three services offer free tiers suitable for development and small-scale projects.

Fauna Setup

After signing up for a Fauna account, create a new database (suggested name: "bookmarks") and a collection called "Bookmarks".

Create two indexes:

  1. getBookmarksByUsername — retrieves all bookmarks for a specific user
  2. getBookmarkByUsernameAndSlug — retrieves individual bookmarks using both username and slug

Create a server-level security key for database access from the Next.js application.

Auth0 Configuration

Create a "Regular Web Applications" type application with these callback settings:

  • Allowed Callback URLs: http://localhost:3000/api/callback
  • Allowed Logout URLs: http://localhost:3000/

Next.js Application Setup

npx create-next-app [name-of-the-app]
cd [name-of-the-app]
npm run dev

Install dependencies:

npm install faunadb swr react-hook-form @auth0/nextjs-auth0

Create a .env.local file:

SERVER_PATH=http://localhost:3000
FAUNA_SERVER_KEY=[FAUNA_SERVER_KEY]
NEXT_PUBLIC_AUTH0_CLIENT_ID=[Auth0 dashboard value]
NEXT_PUBLIC_AUTH0_DOMAIN=[Auth0 dashboard value]
NEXT_PUBLIC_AUTH0_SCOPE="openid profile"
NEXT_PUBLIC_REDIRECT_URI="http://localhost:3000/api/callback"
NEXT_PUBLIC_POST_LOGOUT_REDIRECT_URI="http://localhost:3000"
AUTH0_CLIENT_SECRET=[Auth0 dashboard value]
SESSION_COOKIE_SECRET=[32 character secret]
SESSION_COOKIE_LIFETIME=7200

Client Libraries

lib/fauna-auth.js:

import faunadb from "faunadb";

export const serverClient = new faunadb.Client({
  secret: process.env.FAUNA_SERVER_KEY,
});

lib/auth0.js:

import { initAuth0 } from "@auth0/nextjs-auth0";

export default initAuth0({
  clientId: process.env.NEXT_PUBLIC_AUTH0_CLIENT_ID,
  clientSecret: process.env.AUTH0_CLIENT_SECRET,
  scope: process.env.NEXT_PUBLIC_AUTH0_SCOPE || "openid profile",
  domain: process.env.NEXT_PUBLIC_AUTH0_DOMAIN,
  redirectUri: process.env.NEXT_PUBLIC_REDIRECT_URI || "http://localhost:3000/api/callback",
  postLogoutRedirectUri: process.env.NEXT_PUBLIC_POST_LOGOUT_REDIRECT_URI || "http://localhost:3000/",
  session: {
    cookieSecret: process.env.SESSION_COOKIE_SECRET,
    cookieLifetime: Number(process.env.SESSION_COOKIE_LIFETIME) || 7200,
  },
});

API Routes

pages/api/[username]/index.js — List bookmarks for a user:

import { query as q } from "faunadb";
import { serverClient } from "../../../lib/fauna-auth";

export default async (req, res) => {
  const { username } = req.query;
  try {
    const bookmarks = await serverClient.query(
      q.Map(
        q.Paginate(q.Match(q.Index("getBookmarksByUsername"), username)),
        q.Lambda("X", q.Get(q.Var("X")))
      )
    );
    res.status(200).json(bookmarks.data);
  } catch (e) {
    res.status(500).json({ error: e.message });
  }
};

pages/api/add.js — Create a bookmark:

import { query as q } from "faunadb";
import { serverClient } from "../../lib/fauna-auth";
import auth0 from "../../lib/auth0";

export default auth0.requireAuthentication(async function addBookmark(req, res) {
  const { title, slug, url, description, isPrivate } = req.body;
  const { user } = await auth0.getSession(req);

  try {
    await serverClient.query(
      q.Create(q.Collection("Bookmarks"), {
        data: { username: user.nickname, title, slug, url, description, isPrivate },
      })
    );
    res.status(200).end();
  } catch (e) {
    res.status(500).json({ error: e.message });
  }
});

pages/api/[username]/[slug]/delete.js — Delete a bookmark:

import { query as q } from "faunadb";
import { serverClient } from "../../../../lib/fauna-auth";
import auth0 from "../../../../lib/auth0";

export default auth0.requireAuthentication(async function deleteBookmark(req, res) {
  const { username, slug } = req.query;
  const { user } = await auth0.getSession(req);

  if (user.nickname != username) {
    res.status(500).json({ error: "Invalid User" });
  }

  try {
    await serverClient.query(
      q.Map(
        q.Paginate(
          q.Match(q.Index("getBookmarkByUsernameAndSlug"), [username, slug]),
          { size: 1 }
        ),
        q.Lambda("X", q.Delete(q.Var("X")))
      )
    );
    res.status(200).end();
  } catch (e) {
    res.status(500).json({ error: e.message });
  }
});

Redirect Page

pages/[username]/[slug].js — URL shortener redirect:

export async function getServerSideProps(context) {
  const { username, slug } = context.query;
  try {
    const res = await fetch(
      `${process.env.SERVER_PATH}/api/${username}/${slug}`,
      { method: "GET" }
    );
    if (res.status === 200) {
      const bookmark = await res.json();
      context.res.writeHead(303, { Location: bookmark.url });
      context.res.end();
    } else {
      throw new Error(await res.text());
    }
  } catch (error) {
    return { props: { error: error.message } };
  }
}

Conclusion

This project demonstrates that modern serverless services like Fauna, Next.js, and Auth0 make it straightforward to build secure full-stack applications without managing backend infrastructure. The complete project code is available on GitHub, and Vercel is recommended for deployment due to seamless Next.js integration.