← Back to Blog

Notes for Build a Real-Time Data Syncing Chat Application with Supabase and Next.js

Supabase is an open source Firebase alternative. Firebase is a free service that provides cloud-based storage and realtime communication for your mobile apps. With Supabase, you can create a simple, scalable, and secure backend for your apps.

To get started, you create an account on supabase.io by logging in with your GitHub account.

When you make your first project, you'll be able to change the name later, but use a secure password and choose your region based on where your customers are located.

After a few minutes your backend will be provisioned and you'll be able to start using its features, like

  • Postgres database
  • Authentication
  • APIs
  • Serverless functions
  • Realtime subscriptions

Table editor

table editor

Once you've created a project, you'll be able to create tables and add data to them. The contents of your table will live in this section, and you can edit it freely.

Authentication

The authentication section is where you can set up the users and their permissions. Users can be authenticated by several providers, including Google, GitHub, GitLab, Azure, Facebook, and BitBucket.

Templates for the emails that are sent out when a user wants to reset their password or make a new account are also modifiable and in this section.

At the bottom of the Settings page, you'll find server logs for the authentication attempts.

Storage

The storage section is where you can create and manage your buckets. Buckets work similarly to S3, where files are uploaded into buckets and permissions can be set on a bucket-by-bucket basis.

SQL

Supabase also lets you run SQL scripts against your databases (and see the results) directly in the admin interface. It comes with shortcuts for common activities, like creating a table or adding a column, but you can also run any SQL query you want.

Queries can be saved and starred to make them easier to re-use.

API

The API section is auto-generated documentation for interacting with your database. As you make changes to the database structure, the documentation here will auto-update to reflect the changes and instruct you on the proper way to use it.

Database

The Database section contains detailed information about your database, including the tables that are created to manage authentication and metadata about columns and filesizes.

You can manage the permission roles for your database by clicking on the roles tab, and track how many agents are using each.

Postgres extensions can be added here if you want additional functionality like a data type for case insensitive character strings or key/value pairs.

Databases are backed up daily and you can view them on the admin interface.

In the final subsection, labelled connection pooling, you can configure connections to the database and modify the user or SSL certificates you want to connect with.

Create PostgreSQL Tables Using Supabase's Interface

In the Table Editor, create a table.

If you want to prevent malicious users from being able to guess the IDs of documents, you can change the primary key of your table from an auto-incrementing integer to a UUID.

You can use foreign keys to link a column on one table to a column on another, for example to link a user ID on a message table to a user ID on a user table. Beside each foreign key is a little arrow that links to the table on the other end of the relation.

Configure Supabase Auth to Allow Users to Login with GitHub

Before you can use GitHub as an authentication provider, you need to create a new application on GitHub and add the client ID and secret to the Supabase Auth configuration.

During development, you can set the Homepage URL to http://localhost:3000, but the callback URL should link to your deployed Supabase project.

There is a full guide on how to set up GitHub authentication with Supabase.

Use Triggers to Automatically Update Your Supabase Tables

Tables are created in different databases. The tables that are auto-generated for authentication are in the auth database, and the tables created in the table editor are in the public database. Communication is not allowed between these two databases, so if you want to use the auth database to store users, you need to create a trigger to automatically update the public database every time a new user signs up.

This is documented in the Supabase documentation.

Set up a Supabase Client in Next.js

Install the Supabase client in your project:

Create a .env.local file in the root of your project and add the Supabase URL and API key from the Settings -> API section of your Supabase admin interface.

Create a custom hook called useSupabase

js
1import {createClient} from '@supabase/supabase-js'
2import {useState} from 'react'
3
4const supabase = createClient(
5 process.env.NEXT_PUBLIC_SUPABASE_URL,
6 process.env.NEXT_PUBLIC_SUPABASE_API_KEY,
7)
8
9export default function useSupabase() {
10 const [session, setSession] = useState(
11 supabase.auth.session(),
12 )
13
14 supabase.auth.onAuthStateChange(
15 async (_event, session) => {
16 setSession(session)
17 },
18 )
19
20 return {session, supabase}
21}
22

In the _app.js file, we can use this hook and inject the session and supabase info onto each page.

js
1import React from 'react'
2import { useSupabase } from './hooks/useSupabase'
3
4export default function App({ Component, pageProps }) {
5 const { session, supabase } = useSupabase()
6 return (
7 <Component {...{ session, supabase }} {...pageProps} />
8}
9

They're now automatically accessible on each page

1export default function Home({
2 session,
3 supabase,
4}) {}
5

Set up a Login Page in Next.js with Supabase's auth.session()

You can run a Next.js app in development mode on a different port by setting the PORT environment variable in the start script

PORT=3001 npm run dev

The session prop will be truthy if the user is logged in, and falsy if the user is not logged in.

js
1import * as React from 'react'
2
3export default function Home({session}) {
4 const [
5 isLoggedIn,
6 setLoggedIn,
7 ] = React.useState(false)
8
9 React.useEffect(() => {
10 setLoggedIn(Boolean(session))
11 }, [session])
12
13 return (
14 <main>
15 {isLoggedIn ? (
16 <span> Logged in </span>
17 ) : (
18 <span> Not logged in </span>
19 )}
20 </main>
21 )
22}
23

Set up GitHub Authorization with Supabas

js
1<button
2 onClick={() =>
3 supabase.auth.signIn({
4 provider: 'github',
5 })
6 }
7>
8 Log in with GitHub
9</button>
10

Manage Third-Party Authorization Errors in Supabase

If you don't want to require the user to confirm their email when they log in with a third-party provider, you can check the "Disable email confirmations" switch in the authentication settings page.

Also make sure the site URL is correct. If you changed the port number for development, you should also change that here.

If you fail to authorize a user the first time, perhaps because of configuration issues, you can delete them from your users table and try again once the configuration has been corrected.

Executing Raw SQL using Supabase's Interface

Retrieve Data with a Supabase API Request

1import {useSupabase} from './hooks/useSupabase'
2
3export default function Chat() {
4 const {supabase} = useSupabase()
5 const [messages, setMessages] = React.useState(
6 [],
7 )
8
9 React.useEffect(() => {
10 supabase
11 .from('message')
12 .select('*')
13 .then(({data: messages, error}) => {
14 setMessages(messages)
15 })
16 }, [])
17
18 return (
19 <div>
20 <h1>Supabase Chat</h1>
21 <div>
22 {messages.map((message) => (
23 <p key={message.id}>
24 {message.content}
25 </p>
26 ))}
27 </div>
28 </div>
29 )
30}
31

Subscribe to Database Changes using Supabase's Realtime Client

Supabase currently (August 2021) does not have a way to authenticate subscriptions, but that feature is planned in the roadmap.

1React.useEffect(() => {
2 supabase
3 .from('message')
4 .on('INSERT', (payload) =>
5 setMessages((messages) =>
6 [].concat(messages, payload.new),
7 ),
8 )
9 .subscribe()
10}, [])
11

Enable Realtime Only for Individual Tables using supabase_realtime

For performance reasons, Supabase does not automatically track all the data necessary for realtime subscriptions to all tables and all data, but you can configure it to do so.

In the SQL tab, you can create a publication to add each table you want to subscribe to, and alter the table to replicate with the information you want to be available to subscribers.

begin;
-- drop the publication if exists
drop publication if exists supabase_realtime;
-- re-create the publication
create publication supabase_realtime;
commit;
-- allow realtime subscriptions for new messages
alter publication supabase_realtime add table public.message;
-- also send the previous message to the client
alter table public.message replica identity full;
-- allow realtime subscriptions for new users
alter publication supabase_realtime add table public.user;
-- also send the previous user to the client
alter table public.user replica identity full;

Insert Submitted Data to Supabase Tables

If the application is rendering messages by subscribing to the messages collection, the only thing you need to do is to insert the message into the messages collection and it should appear on the page.

js
1export default function Chat({
2 session,
3 supabase,
4}) {
5 const [messages, setMessages] = React.useState(
6 [],
7 )
8
9 async function handleSubmit(event) {
10 event.preventDefault()
11
12 const form = new FormData(event.target)
13 const input = form.get('message')
14
15 await supabase.from('message').insert([
16 {
17 content: input.value,
18 user_id: session.user.id,
19 },
20 ])
21
22 // Clear the input after submission
23 input.value = ''
24 }
25
26 return (
27 <div>
28 <h1>Supabase Chat</h1>
29 <div>
30 {messages.map((message) => (
31 <p key={message.id}>
32 {message.content}
33 </p>
34 ))}
35
36 <form onSubmit={handleSubmit}>
37 <input
38 type="text"
39 aria-label="Message"
40 required
41 name="message"
42 />
43 <button type="submit"> Send </button>
44 </form>
45 </div>
46 </div>
47 )
48}
49

Keep Track of the Current User Using Next.js with Supabase

1const [
2 currentUser,
3 setCurrentUser,
4] = React.useState(null)
5
6useEffect(() => {
7 if (session?.user.id) {
8 supabase
9 .from('user')
10 .select('*')
11 .eq('id', session.user.id)
12 .then(({data: users}) => {
13 if (users.length === 0) {
14 throw new Error('User not found')
15 }
16
17 return users[0]
18 })
19 .then((user) => {
20 setCurrentUser(user)
21
22 // Subscribe to changes to the user
23 return supabase
24 .from(`user:id=eq.${foundUser.id}`)
25 .on('UPDATE', (payload) => {
26 setCurrentUser(payload.new)
27 })
28 .subscribe()
29 })
30 }
31}, [session?.user.id])
32

Logout and Update Users with React and Supebase's upsert Method

Log out

Supabase has a built in function for logging out, but it may be preferable to do it manually by clearing the local storage and forcing a reload.

js
1import * as React from 'react'
2
3export default function Home({
4 currentUser,
5 session,
6 supabase,
7}) {
8 const [
9 isLoggedIn,
10 setLoggedIn,
11 ] = React.useState(false)
12
13 React.useEffect(() => {
14 setLoggedIn(Boolean(session))
15 }, [session])
16
17 function handleLogout(event) {
18 event.preventDefault()
19
20 window.localStorage.clear()
21 window.location.reload()
22 }
23
24 return (
25 <main>
26 {isLoggedIn ? (
27 <div>
28 <div>
29 Welcome,
30 <span>
31 {currentUser.username
32 ? currentUser.username
33 : session.user.email}
34 </span>
35 </div>
36
37 <button onClick={logout}>
38 Log out
39 </button>
40 </div>
41 ) : (
42 <span> Not logged in </span>
43 )}
44 </main>
45 )
46}
47

Update username

To update a username, insert a document with the new contents set

js
1import * as React from 'react'
2
3export default function Home({
4 currentUser,
5 session,
6 supabase,
7}) {
8 const [
9 editingUsername,
10 setEditingUsername,
11 ] = React.useState(false)
12
13 async function handleSubmit(event) {
14 event.preventDefault()
15
16 const form = new FormData(event.target)
17 const input = form.get('username')
18
19 await supabase.from('user').insert(
20 [
21 {
22 ...currentUser,
23 username: input.value,
24 },
25 ],
26 {upsert: true},
27 )
28
29 // Clear the input after submission
30 input.value = ''
31
32 setEditingUsername(false)
33 }
34
35 return (
36 <main>
37 <div>
38 <div>
39 Welcome,
40 <span>
41 {currentUser.username
42 ? currentUser.username
43 : session.user.email}
44 </span>
45 </div>
46
47 {editingUsername ? (
48 <form onSubmit={handleSubmit}>
49 <input
50 type="text"
51 aria-label="Username"
52 required
53 name="username"
54 default-value={currentUser.username}
55 />
56 <button type="submit">Save</button>
57 </form>
58 ) : (
59 <button
60 onClick={() =>
61 setEditingUsername(true)
62 }
63 >
64 Change Username
65 </button>
66 )}
67 </div>
68 </main>
69 )
70}
71

Request User Details for a Given User Using Supabase's API

const [users, setUsers] = React.useState({})
React.useEffect(() => {
const userIdSet = new Set(
messages.map((message) => message.user_id),
)
const usersToGet = Array.from(userIdSet).filter(
(id) => !users[id],
)
if (
Object.keys(users).length === 0 ||
usersToGet.length !== 0
) {
supabase
.from('user')
.select('id, username')
.in('id', usersToGet)
.then(({data}) => {
const newUsers = {}
data.forEach(
(user) => (newUsers[user.id] = user),
)
setUsers((users) => {
return {
...users,
...newUsers,
}
})
})
}
}, [messages])

Retrieve and Displaying User Details with User Subscriptions

In order for the usernames to update in the application when tehy're changed, we need to set up a subscription for it.

React.useEffect(() => {
supabase
.from('user')
.on('INSERT', (payload) =>
setUsers((users) => {
const user = users[payload.new.id]
return user
? {
...users,
[payload.new.id]: payload.new,
}
: users
}),
)
.subscribe()
}, [])

Deploy a Supabase Application to Production with Cloudflare Pages

Once you deploy your application, you need to update the Site URL in your authentication settings to match the new production URL