Creating an online guestbook using Supabase

Boston CartwrightBoston Cartwright | March 7, 2021 | react
11 min read | ––– views
Photo by Jessica Lewis on Unsplash
Photo by Jessica Lewis on Unsplash

How to use the new Firebase alternative to add a cool feature to your site!

Want to check this out later?

Introduction

When I migrated my blog from Gatsby to Nextjs, I took inspiration from many developer blogs. One of them was Lee Robinson's, who has a sweet guestbook on his site for visitors to leave a message. After seeing his guestbook, I ended up building one on my own site using Supabase, an open source firebase alternative. Here's how I did it:


Table Design

If you are unfamiliar with Supabase, it allows you to quickly and easily create a backend for your site. It provides a Postgres Database, authentication, and more. In order to display guestbook signings, we need to store them, so let's use Supabase's table features.

First, we need to design the table:

| user | name | message | timestamp | | ------ | ---- | -------------------------------- | ------------------- | | abc123 | John | Awesome Site! 😎 | 2021-02-19T20:31:02 | | def456 | Jane | Thanks for all your articles! 😃 | 2021-02-19T20:31:02 | | | | | |

There are four columns, user for a uuid identifying the user from authentication (which we will go over later), name, denoting the signers name, message denoting the message they leave, and lastly timestamp.

Creating this table in Supabase was super simple, we can create it just by using the web interface, which Supabase has some awesome documentation and videos on how to do it.

Creating the Supabase client

In order to connect to Supabase in our project, first we have to create a client connection. This can be done in just a few lines. Let's create two files, one to create an anonymous connection (for retrieving data) and a service connection (for creating, updating, and deleting).

import { createClient } from '@supabase/supabase-js'

export const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
)

Authentication

The next step was to set up authentication so that we can limit who can sign and so that users can delete their messages if they please.

Supabase excels at making authentication extremely easy to set up into our application. The best part was that Supabase already has a set of React components to easily add everything into our project using the package @supabase/ui.

With this, all we need to do is use their Auth component :

import { supabase } from '@lib/initSupabase'

function AuthenticationPage() {
  return (
    <Auth
      supabaseClient={supabase}
      providers={['google', 'github']}
      view={'sign_in'}
      socialLayout="horizontal"
      socialButtonSize="xlarge"
    />
  )
}

Supabase provides a great Next.js example providing some additional functionality, including updating your password and signing out.

Don't forget to add the UserContextProvider somewhere in our project so that it can identify users!

Retrieving messages

Once authentication is up and running, next we need to create the API route to retrieve the messages.

To do this, we can creat a new file in pages/api/guestbook.js.

Starting with just a GET request, and getting the data from my Supabase table was simple:

import { anonSupabase: supabase } from '@lib/initSupabase'

export default async function guestbook(req, res) {
    const { data } = await anonSupabase
    .from('guestbook') // <-- table name
    .select('*') // <-- get all columns

    return res.status(200).json(data)
}

Now we can retrieve the messages by making a simple GET request to /api/guestbook!

curl localhost:3000/api/guestbook
[
  {
    "user":"a1acefdc-2c61-4ac2-819e-b94fdsgw275f",
    "name":"John Doe",
    "message":"Awesome Site! 😎",
    "timestamp":"2021-02-19T19:29:03.34894"
    },
    {
      "user":"6aa3fb38-cf17-4129-9b7d-5d3522d8e63e8",
      "name":"Jane Doe",
      "message":"Thanks for all your articles! 😃",
      "timestamp":"2021-02-19T20:31:02.863419"
    }
]

Displaying messages

Now that we can retrieve messages, we can build a React component to display them (view the final here):

import useSWR from 'swr'

export default function Messages() {
  const { data: messages } = useSWR('/api/guestbook')
  return (
    <section>
      {messages.map((message) => {
        return (
          <div key={message.user}>
            <div>{message.message}</div>
            <small>
              {`${message.name} | ${new Date(message.timestamp).toLocaleString(
                'en',
                dateOptions
              )} | `}
            </small>
          </div>
        )
      })}
    </section>
  )
}

Looking good ! 😎

Creating messages

It's great that our users can now see all the messages left on the site, but that functionality doesn't provide any value if the users can't add their own message!

Let's implement that functionality now, starting with the API. This can be done like so:

// get user token
const token = req.headers.token
// authenticate user
const { data: user, error } = await serviceSupabase.auth.api.getUser(token)
// if user not authenticated, error
if (error) return res.status(401).json({ error: error.message })

// add message to database
const { data, error: err } = await serviceSupabase.from('guestbook').insert(
  [
    {
      user: user.id, // user uuid (primary key)
      name: req.body.name, // name
      message: req.body.message, // message
    }, // timestamp is created automatically by Supabase at insert time
  ],
  { upsert: true } // upsert, we can use this to either insert or update the row
)

// if there is an error posting, return the error
if (err) return res.status(500).json({ error: err })

// return the posted data
return res.status(200).json(data)

Adding this onto our existing API call in pages/api/guestbook.js:


import { anonSupabase: supabase } from '@lib/initSupabase'

export default async function guestbook(req, res) {
    if (req.method === 'GET') {
        const { data } = await anonSupabase
        .from('guestbook') // <-- table name
        .select('*') // <-- get all columns

        return res.status(200).json(data)
    }

    if(req.method === 'POST') {
        // get user token
        const token = req.headers.token
        // authenticate user
        const { data: user, error } = await serviceSupabase.auth.api.getUser(token)
        // if user not authenticated, error
        if (error) return res.status(401).json({ error: error.message })

        // add message to database
        const { data, error: err } = await serviceSupabase.from('guestbook').insert(
        [
            {
            user: user.id, // user uuid (primary key)
            name: req.body.name, // name
            message: req.body.message, // message
            }, // timestamp is created automatically by Supabase at insert time
        ],
        { upsert: true } // upsert, we can use this to either insert or update the row
        )

        // if there is an error posting, return the error
        if (err) return res.status(500).json({ error: err })

        // return the posted data
        return res.status(200).json(data)
    }
}

Now that we have an API to send messages, we can create a form to fill out and call it, but we only want users to fill out the form if they are signed in. This is pretty simple to set up with Supabase.

If they are not signed in, show a button to sign in which redirects them to the authentication page. Otherwise, show the form to add a message.

export default function Guestbook() {
  const { user, session } = Auth.useUser()
  const { data, error } = useSWR(
    session ? ['/api/getUser', session.access_token] : null,
    fetcher
  )

  const onSubmit = async (e) => {
    e.preventDefault()
    const fullName = data?.['user_metadata']?.['full_name']
    await fetch('/api/guestbook', {
      method: 'POST',
      headers: new Headers({
        'Content-Type': 'application/json',
        token: session.access_token,
      }),
      credentials: 'same-origin',
      body: JSON.stringify({
        name: fullName,
        message: e.target.message.value,
      }),
    })
    revalidate()
  }

  return (
    <div>
    {/* other stuff omitted for brevity... */}
      <section>
        <h2>Sign my guestbook</h2>
        {user ? (
          <div className="">
            <form onSubmit={onSubmit}>
              <input
                type="text"
                name="message"
                placeholder={`${
                  userHasSubmitted ? 'Update' : 'Write'
                } your message here...`}
              />
              <button
                type="submit"
              >
                Sign
              </button>
            </form>
            {userHasSubmitted ? <span>Thank you for signing!</span> : null}
          </div>
        ) : (
          <button
            onClick={() => router.push('/auth')}
          >
            Sign in
          </button>
        )}
        <small>
          Your information is only used to display your name and reply by email.
        </small>
      </section>
    <div>
  )
}

Put them all together, and we have your guestbook!

Some more features we can add include deleting a post or sorting them by user or date!

Final Comments

Overall I really enjoyed using Supabase, it made it super easy and simple to set up a backend for my site and am looking forward to build some new features with it in the future! I definitely recommend it.


What do you think? Let me know @bstncartwright! 😀