How I built a Serverless Micro-Blogging Site Using Next.js and Fauna

I have recently built a website for our local developer community Coderplex completely Serverless. We were able to start from scratch and got it launched within just 3 weeks of time.

•

•

How I built a Serverless Micro-Blogging Site Using Next.js and Fauna
Related Posts
Authored in connection with the Write With Fauna program.

 
I have recently built a website for our local developer community Coderplex completely Serverless. We were able to start from scratch and got it launched within just 3 weeks of time. In this article, I will show what the website is about, how I built it, and also show the areas of improvement and the new features that we are planning to add to it.
The website is basically a platform like Twitter where you post updates. The only difference is that it’s aimed towards developers(has things like markdown editor), and it’s goal-centric. As soon as the users log in to the site, they will be prompted to set their goal. You will be asked to set a title for the goal, write the plan of action of how you want to achieve that goal, and finally, you will be asked to set a deadline, before which you intend to achieve that goal. After that, you will be allowed to post updates whenever you want, showing what you are doing in order to reach that goal.

Authentication

We only have one mode of authentication i.e., through GitHub login. We are using next-auth package for this.
notion image
 
Check out the following blog post which I published a few weeks ago to know more about setting up GitHub authentication using next-auth.

Setting up Fauna in Next.js

Installing Fauna

Before we go through other features of coderplex.org, let’s see how I have set up my Fauna in Next.js and how my Fauna workflow is.
Install faunadb (javascript driver for Fauna DB) as a dependency in your Next.js project
yarn add faunadb
# npm install faunadb
While developing locally, you have two choices:
  1. You can either create a test database in the Fauna cloud directly.
  1. You can set up the Fauna Dev Docker container locally.
I personally have set up the Docker container and have been using that for local development.
To know more about setting up the Fauna Dev container, go to the relevant section in the documentation.
To summarize, here is what I have done.
# Pull the latest Docker image of faunadb
docker pull fauna/faunadb:latest

# Verify if it installed correctly
docker run fauna/faunadb --help

# Run the Fauna Dev
docker run --rm --name faunadb -p 8443:8443 -p 8084:8084 fauna/faunadb

# After this, the Fauna Dev is started locally at port 8443
If you go through the documentation, you will see that there are so many ways to run the Fauna Dev. I chose the first approach because I want to start with a fresh state every time I start the database. In this approach, as soon as you stop the container, all the data will be erased, and whenever you start the container again, you will be starting with a fresh instance of Fauna.

Setting up Migrations Tool

If you come from a background of Laravel/Django/Rails/Node, you are most probably be aware of migrations. In simple terms, the migrations are the set of files. They help manage the changes in the database schema. The files are usually associated with timestamps of the time when the migrations are created. There will usually be a way to apply a migration or roll back (unapply) a migration. These are incremental steps that you need to perform to get the fresh database to the same state as the current database.
In Fauna, there is no native solution to achieve this. But very recently, an unofficial tool has been created by a developer advocate at Fauna. I have been using that tool for setting up migrations in my projects.
# Install the tool as a dev dependency
yarn add -D fauna-schema-migrate

# If you use npm, run the following command to install it.
# npm install -D fauna-schema-migrate

# Initialize the folders and config needed for this tool
npx fauna-schema-migrate init
This will create some files and folders which we later use to set up migrations for the collections and indexes that we create. Go through the GitHub REAMDE to know more about this tool. To summarize, here’s how it works.
  1. You add all your collections, indexes, etc in your fauna/resources folder.
  1. Based on the changes in the resources folder, you will be able to generate migrations. These migrations will get generated in your fauna/migrations folder.
  1. You will have the ability to see the state of the database. You will be able to see what all migrations have been applied and what migrations are yet to be applied.
  1. You will be able to apply the migrations or roll back the applied migrations.
  1. While running any of these commands, you will be asked to enter the FAUNA ADMIN KEY.
      • You will be able to generate this key from the Security tab of the Fauna dashboard.
      • You can also set the environment variables FAUNA_ADMIN_KEY, FAUNADB_DOMAIN, FAUNADB_SCHEME, and FAUNADB_PORT.
      • While connecting to the cloud database, you will only need to set FAUNA_ADMIN_KEY.
      • If you are working with the Fauna Dev Docker container, you need to set up other variables too.
        • Since I am using Fauna Dev for local development, I have set up these as per my configuration.
# This is the default secret for Fauna Dev
export FAUNA_ADMIN_KEY=secret 

export FAUNADB_DOMAIN=localhost
export FAUNADB_SCHEME=http
export FAUNADB_PORT=8443

Authentication and Authorization in Fauna

Fauna has its own authentication system. But in this project, I have been using a next-auth adapter for authentication. Basically what this means is that I will handle all the authentication and authorization elsewhere(in the serverless functions of my app), and only allow the users to access the resources that they are authorized to access. Doing things this way is definitely not ideal. Fauna offers a very powerful security system out of the box. But it’s a bit tricky to make it work with next-auth system. But I am definitely planning to make use of Fauna’s security system in the future and will try to make it work with next-auth without losing any of the capabilities of Fauna.

Next.js Serverless Function Setup for Fauna

This is how all my serverless functions look like in my Next.js app

Fauna DB Client Setup

I use Docker container locally, and use Fauna Cloud when the app is in production
import faunadb from 'faunadb'

// Checking if the app is in production
const isProduction = process.env.NODE_ENV === 'production'

// Using Fauna Dev Docker container when running locally
// Using Fauna Cloud when in production
const client = new faunadb.Client({
  secret: process.env.FAUNADB_SECRET ?? 'secret',
  scheme: isProduction ? 'https' : 'http',
  domain: isProduction ? 'db.fauna.com' : 'localhost',
  ...(isProduction ? {} : { port: 8443 }),
})

Requires Authentication

If it requires the user to be authenticated, then I use next-auth getSession function to check if the user is authenticated.
import { getSession } from 'next-auth/client'

const Handler = async (req, res) => {

    // If the user needs authentication
  const session = await getSession({ req })

  if (!session) {
    return res.status(401).json({ message: 'Not logged in (UnAuthenticated)' })
  }

    // other code
    // ...
    // ...
    // ...
}

export default Handler

Requires Authorization

If only a particular user is authorized to run this function, then I send the id of the authorized user in the request body, and manually check if the currently logged-in user is the same as that of the authorized user.
import { getSession } from 'next-auth/client'

const Handler = async (req, res) => {

    // If the user needs authentication
  const session = await getSession({ req })

  if (!session) {
    return res.status(401).json({ message: 'Not logged in (UnAuthenticated)' })
  }

  const { authorizedUserId } = req.body

  const loggedInUserId = session.user.id

  if (loggedInUserId !== authorizedUserId) {
    return res.status(403).json({ message: 'Access Forbidden' })
  }

    // other code
    // ...
    // ...
    // ...

}

export default Handler
Putting it all together, this is how a typical serverless function looks like in my Nextjs app.
import { getSession } from 'next-auth/client'

import faunadb from 'faunadb'
const isProduction = process.env.NODE_ENV === 'production'
const client = new faunadb.Client({
  secret: process.env.FAUNADB_SECRET ?? 'secret',
  scheme: isProduction ? 'https' : 'http',
  domain: isProduction ? 'db.fauna.com' : 'localhost',
  ...(isProduction ? {} : { port: 8443 }),
})
const q = faunadb.query

const Handler = async (req, res) => {

    // If the user needs authentication
  const session = await getSession({ req })
  if (!session) {
    return res.status(401).json({ message: 'Not logged in (UnAuthenticated)' })
  }

  const { authorizedUserId } = req.body

  const loggedInUserId = session.user.id

  if (loggedInUserId !== authorizedUserId) {
    return res.status(403).json({ message: 'Access Forbidden' })
  }

    try {
    const response: any = await client.query(
            q.Do(
                // Execute some fauna code here
            )
        )

        // Some code
        // ...
        // ...
        // ...
    res.status(200).json(response)
  } catch (error) {
    console.error(error)
    res.status(500).json({ message: error.message })
  }
}

export default Handler

NextAuth

We need a few collections and indexes to work with next-auth’s Fauna adapter. So, let’s add those in fauna/resources folder. I have created two directories inside the resources folder. One is for collections and another for indexes.
These are the collections that I need for the next-auth’s Fauna adapter.
// fauna/resources/collections/accounts.fql
CreateCollection({
  name: 'accounts'
})

// fauna/resources/collections/users.fql
CreateCollection({
  name: 'users'
})

// fauna/resources/collections/sessions.fql
CreateCollection({
  name: 'sessions'
})

// fauna/resources/collections/verification_requests.fql
CreateCollection({
  name: 'verification_requests'
})
Each of the above FQL queries is in a different file inside the resources/collections folder.
We also need a few indexes for the next-auth’s Fauna adapter.
// fauna/resources/indexes/account_by_provider_account_id.fql
CreateIndex({
  name: 'account_by_provider_account_id',
  source: Collection('accounts'),
  unique: true,
  terms: [
    { field: ['data', 'providerId'] },
    { field: ['data', 'providerAccountId'] },
  ],
})

// fauna/resources/indexes/session_by_token.fql
CreateIndex({
  name: 'session_by_token',
  source: Collection('sessions'),
  unique: true,
  terms: [{ field: ['data', 'sessionToken'] }],
})

// fauna/resources/indexes/user_by_email.fql
CreateIndex({
  name: 'user_by_email',
  source: Collection('users'),
  unique: true,
  terms: [{ field: ['data', 'email'] }],
})

// fauna/resources/indexes/verification_request_by_token.fql
CreateIndex({
  name: 'verification_request_by_token',
  source: Collection('verification_requests'),
  unique: true,
  terms: [{ field: ['data', 'token'] }],
})

// fauna/resources/indexes/user_by_username.fql
CreateIndex({
  name: 'user_by_username',
  source: Collection('users'),
  unique: true,
  terms: [{ field: ['data', 'username'] }],
})
Now we need to generate the corresponding migrations for these resources. To do that, you can just run npx fauna-schema-migrate generate. This will create a new folder in the fauna/migrations folder, inside which there are files for each of the resource files that we created. You can apply all of these migrations in one go by running npx fauna-schema-migrate apply all.
Now that we are all set up, let’s start working on the actual features of the application.

Requirements and Features

In the coderplex.org app, each user will be able to do all of the following things:
  • Goals
    • Set a goal with title, description(goal plan – which also accepts MDX), and deadline.
    • Currently, only 1 user will correspond to 1 goal. In other words, each goal will have a single participant, but in the future, multiple users can participate in the same goal. So the schema should take care of that.
  • Updates
    • Post an update to the goal
      • An update is very similar to a tweet that you have on Twitter. The only difference is that each update will correspond to a goal.
      • The update will also accept markdown(MDX).
    • Edit your update
    • Like any update
  • Comments
    • Add a comment to an update (also accepts MDX)
    • Edit your comment.
    • Like any comment
  • Follow any other user
  • Notifications
    • Whenever another user likes your update/comment.
    • Whenever another user follows you.
    • Whenever another user comments on your update.
    • Users should see the unread notification count.
    • When they open the notifications, they should also be able to differentiate unread notifications from read-notifications.
    • All the notifications should be marked as read, by the next time they open the notifications, and the notification count should be reset to 0.
  • Users should be able to edit/add the details like their social media links, their name, etc.
These are the requirements and features that we have in the coderplex.org app.

Modeling the data

These are the following collections that I have created.
  • goals
  • goal_participants
  • goal_updates
  • update_likes
Bhanu Teja P

Written by

Bhanu Teja P

24yo developer and blogger. I quit my software dev job to make it as an independent maker. I write about bootsrapping Feather.