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.
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:
- getBookmarksByUsername — retrieves all bookmarks for a specific user
- 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.