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
Navigate the Supabase Admin Interface
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.
https://12345678901234567890.supabase.co/auth/v1/callback
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:
npm install @supabase/supabase-js
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.
NEXT_PUBLIC_SUPABASE_URL="https://12345678901234567890.supabase.co"NEXT_PUBLIC_SUPABASE_API_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
Create a custom hook called useSupabase
1import { createClient } from "@supabase/supabase-js"2import { useState } from "react"34const supabase = createClient(5 process.env.NEXT_PUBLIC_SUPABASE_URL,6 process.env.NEXT_PUBLIC_SUPABASE_API_KEY,7)89export default function useSupabase() {10 const [session, setSession] = useState(11 supabase.auth.session(),12 )1314 supabase.auth.onAuthStateChange(15 async (_event, session) => {16 setSession(session)17 },18 )1920 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.
1import React from 'react'2import { useSupabase } from './hooks/useSupabase'34export 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({ session, supabase }) {}2
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.
1import * as React from "react"23export default function Home({ session }) {4 const [isLoggedIn, setLoggedIn] = React.useState(false)56 React.useEffect(() => {7 setLoggedIn(Boolean(session))8 }, [session])910 return (11 <main>12 {isLoggedIn ? (13 <span> Logged in </span>14 ) : (15 <span> Not logged in </span>16 )}17 </main>18 )19}20
Set up GitHub Authorization with Supabas
1<button2 onClick={() =>3 supabase.auth.signIn({4 provider: "github",5 })6 }7>8 Log in with GitHub9</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.
http://localhost:3001
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
select * from auth.users
Retrieve Data with a Supabase API Request
1import { useSupabase } from "./hooks/useSupabase"23export default function Chat() {4 const { supabase } = useSupabase()5 const [messages, setMessages] = React.useState([])67 React.useEffect(() => {8 supabase9 .from("message")10 .select("*")11 .then(({ data: messages, error }) => {12 setMessages(messages)13 })14 }, [])1516 return (17 <div>18 <h1>Supabase Chat</h1>19 <div>20 {messages.map((message) => (21 <p key={message.id}>{message.content}</p>22 ))}23 </div>24 </div>25 )26}27
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 supabase3 .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 messagesalter publication supabase_realtime add table public.message;-- also send the previous message to the clientalter table public.message replica identity full;-- allow realtime subscriptions for new usersalter publication supabase_realtime add table public.user;-- also send the previous user to the clientalter 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.
1export default function Chat({ session, supabase }) {2 const [messages, setMessages] = React.useState([])34 async function handleSubmit(event) {5 event.preventDefault()67 const form = new FormData(event.target)8 const input = form.get("message")910 await supabase.from("message").insert([11 {12 content: input.value,13 user_id: session.user.id,14 },15 ])1617 // Clear the input after submission18 input.value = ""19 }2021 return (22 <div>23 <h1>Supabase Chat</h1>24 <div>25 {messages.map((message) => (26 <p key={message.id}>{message.content}</p>27 ))}2829 <form onSubmit={handleSubmit}>30 <input31 type="text"32 aria-label="Message"33 required34 name="message"35 />36 <button type="submit"> Send </button>37 </form>38 </div>39 </div>40 )41}42
Keep Track of the Current User Using Next.js with Supabase
1const [currentUser, setCurrentUser] = React.useState(null)23useEffect(() => {4 if (session?.user.id) {5 supabase6 .from("user")7 .select("*")8 .eq("id", session.user.id)9 .then(({ data: users }) => {10 if (users.length === 0) {11 throw new Error("User not found")12 }1314 return users[0]15 })16 .then((user) => {17 setCurrentUser(user)1819 // Subscribe to changes to the user20 return supabase21 .from(`user:id=eq.${foundUser.id}`)22 .on("UPDATE", (payload) => {23 setCurrentUser(payload.new)24 })25 .subscribe()26 })27 }28}, [session?.user.id])29
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.
1import * as React from "react"23export default function Home({4 currentUser,5 session,6 supabase,7}) {8 const [isLoggedIn, setLoggedIn] = React.useState(false)910 React.useEffect(() => {11 setLoggedIn(Boolean(session))12 }, [session])1314 function handleLogout(event) {15 event.preventDefault()1617 window.localStorage.clear()18 window.location.reload()19 }2021 return (22 <main>23 {isLoggedIn ? (24 <div>25 <div>26 Welcome,27 <span>28 {currentUser.username29 ? currentUser.username30 : session.user.email}31 </span>32 </div>3334 <button onClick={logout}>Log out</button>35 </div>36 ) : (37 <span> Not logged in </span>38 )}39 </main>40 )41}42
Update username
To update a username, insert a document with the new contents set
1import * as React from "react"23export default function Home({4 currentUser,5 session,6 supabase,7}) {8 const [editingUsername, setEditingUsername] =9 React.useState(false)1011 async function handleSubmit(event) {12 event.preventDefault()1314 const form = new FormData(event.target)15 const input = form.get("username")1617 await supabase.from("user").insert(18 [19 {20 ...currentUser,21 username: input.value,22 },23 ],24 { upsert: true },25 )2627 // Clear the input after submission28 input.value = ""2930 setEditingUsername(false)31 }3233 return (34 <main>35 <div>36 <div>37 Welcome,38 <span>39 {currentUser.username40 ? currentUser.username41 : session.user.email}42 </span>43 </div>4445 {editingUsername ? (46 <form onSubmit={handleSubmit}>47 <input48 type="text"49 aria-label="Username"50 required51 name="username"52 default-value={currentUser.username}53 />54 <button type="submit">Save</button>55 </form>56 ) : (57 <button onClick={() => setEditingUsername(true)}>58 Change Username59 </button>60 )}61 </div>62 </main>63 )64}65
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