Supabase has been growing by 100x with 2k searches per month. If you’re wondering what supabase is or wanting to learn about it. you’re in the right place. This article is to explain about basics of Supabase and building a simple application with Supabase, Next.js.
According to the official documentation,
Supabase is an open source Firebase alternative. It provides all the backend services you need to build a product. Supabase uses Postgres database with real-time capabilities. Basically, supabase provides an interface to manage postgres database that you can use to create table and insert, edit and delete data in the table.
We can use REST API or client libraries from supabase to access the data in the postgres database. Supabase is not just about accessing the database. it also provides some solutions out of the box such as Authentication, File Storage and Real-time capabilities.
Let’s create a new project in Supabase that creates a credentials to access the database.
Go to https://app.supabase.io/
Click on New Project
It will launch the database and required credential to access them.
Once you create the database and credentials to access it, let’s create a Next.js application and integrate supabase into it.
To give a taste of what we are going to build in this application. Here’s a demo to explain this simple application.
https://github.com/ganeshmani/supabase-examples/tree/main/NextjsCRUD
Let’s create a Next.js application and install supabase-js
client library to integration Supabase.
1npx create-next-app NextjsCRUD --ts2cd NextjsCRUD
Then install the supabase client library to access supabase in the Next.js application.
1npm install @supabase/supabase-js
Once you install the dependancy, add environment variables for Supabase url and supabase key to access supabase by creating .env.local
in the project root directory.
1NEXT_PUBLIC_SUPABASE_URL = YOUR_SUPABASE_URL2NEXT_PUBLIC_SUPABASE_ANON_KEY = YOUR_SUPABASE_ANON_KEY
Now that you have API credentials, let’s create a helper to initialize the supabase client. This supabase instances and credentials are exposed on the browser and it’s safer since we have Row level security enabled in our database.
create lib/supabaseClient.js
and add the following code,
1import { createClient } from "@supabase/supabase-js"23const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || ""4const supabaseToken = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || ""56export const supabaseClient = createClient(supabaseUrl, supabaseToken)
It creates a supabase client with supabase url and supabase token that you provide via environment variable. you can use the supabase client wherever you need to access the database.
For styles, we are going to use Chakra UI. It’s opinionated. you can use your own preferred styling like TailwindCSS, Material UI etc.
To setup chakra UI, install the dependancy @chakra-ui/react
1npm install @chakra-ui/react
Then, set it up inside the _app.tsx
,
1import "../styles/globals.css"2import type { AppProps } from "next/app"3import { ChakraProvider } from "@chakra-ui/react"45function MyApp({ Component, pageProps }: AppProps) {6 return (7 <ChakraProvider>8 <Component {...pageProps} />9 </ChakraProvider>10 )11}1213export default MyApp
Authentication using Supabase is straightforward and supabase provides a solution out of the box. Supabase provides several ways to authenticate users. They are,
For authentication, we are going to use Magic links, so users can sign in with their email without using passwords.
login.tsx
1import { useState } from "react";2import type { NextPage } from "next";3import { supabaseClient } from "../lib/supabaseClient";45import {6 Flex,7 Box,8 FormControl,9 FormLabel,10 Input,11 Checkbox,12 Stack,13 Link,14 Button,15 Heading,16 Text,17 useColorModeValue,18} from "@chakra-ui/react";1920const Login: NextPage = () => {21 const [loading, setLoading] = useState(false);22 const [email, setEmail] = useState("");2324 const handleLogin = async (email: string) => {25 try {26 setLoading(true);27 const { error } = await supabaseClient.auth.signIn({ email });28 if (error) throw error;29 alert("Check your email for the login link!");30 } catch (error: any) {31 alert(error.error_description || error.message);32 } finally {33 setLoading(false);34 }35 };36 return (37 <Flex38 minH={"100vh"}39 align={"center"}40 justify={"center"}41 bg={useColorModeValue("gray.50", "gray.800")}42 >43 <Stack spacing={8} mx={"auto"} maxW={"lg"} py={12} px={6}>44 <Stack align={"center"}>45 <Heading fontSize={"4xl"}>Sign in to your account</Heading>46 </Stack>47 <Box48 rounded={"lg"}49 bg={useColorModeValue("white", "gray.700")}50 boxShadow={"lg"}51 p={8}52 >53 <Stack spacing={4}>54 <FormControl id="email">55 <FormLabel>Email address</FormLabel>56 <Input57 type="email"58 placeholder="Enter your email"59 value={email}60 onChange={(e) => setEmail(e.target.value)}61 />62 </FormControl>63 <Stack spacing={10}>64 <Button65 isLoading={loading}66 bg={"blue.400"}67 onClick={(e) => {68 e.preventDefault();69 handleLogin(email);70 }}71 color={"white"}72 _hover={{73 bg: "blue.500",74 }}75 >76 Send magic link77 </Button>78 </Stack>79 </Stack>80 </Box>81 </Stack>82 </Flex>83 );84};85export default Login;
Here, we have a input to capture the email address and button to handle the login functionality.
Once user clicks the Send magic link
button, we call the handleLogin
function that uses supbase auth signin.
1const handleLogin = async (email: string) => {2 try {3 setLoading(true);4 const { error } = await supabaseClient.auth.signIn({ email }); //Supabase Auth signin5 if (error) throw error;6 alert("Check your email for the login link!");7 } catch (error: any) {8 alert(error.error_description || error.message);9 } finally {10 setLoading(false);11 }12 };
Then, user receives a magic link in their email to login to the application.
It will redirect the user into the application’s home page. We can also configure the redirect URL via supabase auth settings. It’ll update the redirection after the user clicks the login link.
To configure redirect URL for post authentication,
You can also access the users list who signed in your application via users section in supabase.
Once the user login, supabase stores the user information in the session. we can access the user information via supabase sessions. We need that information to validate the user everytime when they visit the protected routes. it can be home page or dashboard.
For that, let’s create a custom hooks to get user information from supabase session and access it via user
variable. In that way, wherever we need to user information, we don’t need to write the same functionality inside the component again.
To learn about custom hooks in react: refer the documentation here
create hooks/useUser.ts
and add the following code,
1import { useState, useEffect } from "react"2import { supabaseClient } from "../lib/supabaseClient"3const useUser = () => {4 const [user, setUser] = useState < any > null5 const [isLoading, setLoading] = useState < boolean > true6 const [token, setToken] = useState < any > null78 useEffect(() => {9 const supabaseSession = supabaseClient.auth.session()1011 if (supabaseSession?.user?.id) {12 setUser(supabaseSession.user)13 setToken(supabaseSession.access_token)14 }1516 setLoading(false)1718 supabaseClient.auth.onAuthStateChange((_event, session) => {19 if (session?.user?.id) {20 setUser(session.user)21 setToken(session.access_token)22 }23 setLoading(false)24 })25 }, [supabaseClient])2627 return {28 user,29 isLoading,30 token,31 }32}3334export default useUser
Here, we are using useState
to store user info, loading state and token. Initially, we have loading: true
to indicate the component that it’s loading. Once we get user information from supabase session. we store them into user
state in the hooks and set loading: false
.
Now, let’s add this step into index.tsx
here to verify if the user is authenticated or not. if not, we can redirect to /login
page.
1const Home: NextPage = () => {2 const router = useRouter()3 const { user, isLoading, token } = useUser()45 useEffect(() => {6 if (!user && !isLoading) {7 router.push("/login")8 }9 }, [user, isLoading, router])1011 if (isLoading) {12 return <div>Loading</div>13 } else {14 return (15 <Layout>16 <Container>//CRUD API - Todo Container</Container>1718 <Toaster />19 </Layout>20 )21 }22}2324export default Home
Now, the user is logged in, let’s see how to implement CRUD API using Supabase in Next.js application.
Since this tutorial is to learn how to implement CRUD functionality using Supabase, Next.js. we will go with a classic example. i.e., Todo Application.
First and foremost, create a new table in the postgres database. We can use supabase UI to create a table.
Once you add and create a table. we can use it via supabase client in the react application. whenever you need a new column or change a column, you can edit the table and update it.
TODO UI
Here’s a simple UI behaviour to understand the functionality. When you add a new item and enter it, it will create a todo item. it will be added into the list automatically( we will use a react-query to achieve that). you can edit or delete a todo item from the list.
If you’re new to React Query, React Query is a manage the server state of client side application. it is often described as data-fetching library of React. it makes fetching, caching, synchronizing and updating server state in the React application.
create Todos/index.tsx
to handle the list and Todos/Add.tsx
for handling inserting todo item. Once you create those components, import them into index.tsx
.
1return (2 <Layout>3 <Container>4 <AddTodo />5 <Todos />6 </Container>78 <Toaster />9 </Layout>10)
Todo/Add.tsx
1import { useState, useEffect } from "react"2import { useMutation, useQueryClient } from "react-query"3import toast, { Toaster } from "react-hot-toast"4import { User } from "@supabase/supabase-js"5import { Container, Input, ListItem, OrderedList } from "@chakra-ui/react"6import { supabaseClient } from "@lib/supabaseClient"7import useUser from "@hooks/useUser"89const AddTodo = () => {10 const [currentItem, setCurrentItem] = useState < string > ""11 const { user } = useUser()12 const queryClient = useQueryClient()1314 const { mutate: addTodoMutation } = useMutation(15 async (payload: { item: string, user: User }) => {16 const { data, error } = await supabaseClient.from("todos").insert([17 {18 name: payload.item,19 user_id: payload.user.id,20 },21 ])2223 if (error) {24 console.log("Error", error)25 }26 return data27 },28 {29 onSuccess: () => {30 toast.success("Item Added successfully")31 setCurrentItem("")32 return queryClient.invalidateQueries("todos")33 },34 }35 )3637 const _handleAddItem = async (item: string) => {38 addTodoMutation({ item, user: user })39 }4041 return (42 <Input43 type={"text"}44 value={currentItem}45 placeholder="Enter item here"46 onChange={e => setCurrentItem(e.target.value)}47 onKeyPress={e => {48 if (e.key === "Enter") {49 _handleAddItem(currentItem)50 }51 }}52 />53 )54}5556export default AddTodo
Here, we have an input for user to add the todo item. When user press enter, we invoke _handleAddItem
function that handles the data insert.
Since we are using react-query for queries and mutations. we will use useMutation
to handle function to insert data.
1const { mutate: addTodoMutation } = useMutation(2 async (payload: { item: string, user: User }) => {3 const { data, error } = await supabaseClient.from("todos").insert([4 {5 name: payload.item,6 user_id: payload.user.id,7 },8 ])910 if (error) {11 console.log("Error", error)12 }13 return data14 },15 {16 onSuccess: () => {17 toast.success("Item Added successfully")18 setCurrentItem("")19 return queryClient.invalidateQueries("todos")20 },21 }22)
One important thing to note here is invalidateQueries
. it’s an important feature of react query to invalidate the todo list whenever a new item is added.
1return queryClient.invalidateQueries("todos")
When it is invalid, react-query fetches the data again for the tag todos
and update the list. we will discuss it when we implement the GET request using supabase.
To implement todo list, create Todos/index.tsx
and add the following code,
1import { useState } from "react";2import {3 QueryClient,4 useMutation,5 useQuery,6 useQueryClient,7} from "react-query";8import { supabaseClient } from "@lib/supabaseClient";9import {10 Spinner,11 Stack,12 ListItem,13 OrderedList,14 ListIcon,15 HStack,16 VStack,17 Flex,18 Text,19} from "@chakra-ui/react";20import { DeleteIcon, EditIcon } from "@chakra-ui/icons";21import toast from "react-hot-toast";2223const Todos = () => {24 const [isOpen, setIsOpen] = useState<boolean>(false);25 const [selectedItem, setSelectedItem] = useState<{26 id: number;27 name: string;28 }>();2930 const queryClient = useQueryClient();3132 const { data, isLoading, isError, isSuccess } = useQuery(33 "todos",34 async () => {35 const { data, error } = await supabaseClient36 .from("todos")37 .select()38 .order("id", { ascending: true });3940 if (error) {41 throw new Error(error.message);42 }4344 return data;45 }46 );4748 if (isLoading) {49 return (50 <Stack>51 <Spinner size="xl" />52 </Stack>53 );54 }5556 return (57 <>58 <VStack>59 {data!.map((todo: any) => (60 <HStack key={todo.id} spacing="24px">61 <Flex p={6} w="300px" h="50px" justifyContent="space-between">62 <Text>{todo.name}</Text>6364 <Flex w="10px">65 <DeleteIcon66 cursor={"pointer"}67 color="red.500"68 mr="2"69 onClick={() => {}}70 />71 <EditIcon72 cursor={"pointer"}73 onClick={() => {}}74 />75 </Flex>76 </Flex>77 </HStack>78 ))}79 </VStack>80 </>81 );82};8384export default Todos;
To fetch the data from database, we implement useQuery
hook from react-query with a function to fetch the data from supabase.
1const { data, isLoading, isError, isSuccess } = useQuery("todos", async () => {2 const { data, error } = await supabaseClient3 .from("todos")4 .select()5 .order("id", { ascending: true })67 if (error) {8 throw new Error(error.message)9 }1011 return data12})
Important thing to note here is the supabaseClient.from
. It specifies the table that we need to fetch the data from and we can use select
to specify the column that we need to fetch.
To edit the data in the list, user clicks on the edit icon and update the item. To achieve that behaviour, we will have a modal that shows the selected item and user can update it.
Create Todos/UpdateModal.tsx
and add the following code,
1import { useState, useEffect } from "react"2import { useMutation, useQueryClient } from "react-query"3import {4 Modal,5 ModalOverlay,6 ModalContent,7 ModalBody,8 ModalCloseButton,9 ModalHeader,10 useDisclosure,11 Input,12} from "@chakra-ui/react"13import toast from "react-hot-toast"1415//Internal dependencies16import { supabaseClient } from "@lib/supabaseClient"1718type Prop = {19 todo: {20 id: number,21 name: string,22 },23 isOpen: boolean,24 onClose: () => void,25}2627const UpdateTodoModal = ({ todo, isOpen, onClose }: Prop) => {28 const [currentItem, setCurrentItem] = useState(todo?.name)2930 useEffect(() => {31 if (todo) {32 setCurrentItem(todo.name)33 }34 }, [todo])3536 const queryClient = useQueryClient()3738 const { mutate } = useMutation(39 async (item: any) => {40 const { data, error } = await supabaseClient41 .from("todos")42 .update({ name: item.name })43 .match({ id: item.id })4445 if (error) {46 toast.error("Something went wrong")47 return error48 }4950 return data51 },52 {53 onSuccess: () => {54 toast.success("Item Updated successfully")55 setCurrentItem("")56 onClose()57 return queryClient.refetchQueries("todos")58 },59 }60 )6162 const _handleUpdate = (item: string) => {63 mutate({ id: todo.id, name: item })64 }6566 return (67 <Modal isOpen={isOpen} onClose={onClose}>68 <ModalOverlay />69 <ModalContent>70 <ModalHeader>Update Item</ModalHeader>71 <ModalCloseButton />72 <ModalBody pb={6} pt={6}>73 <Input74 type={"text"}75 value={currentItem}76 placeholder="Enter item here"77 onChange={e => setCurrentItem(e.target.value)}78 onKeyPress={e => {79 if (e.key === "Enter") {80 _handleUpdate(currentItem)81 }82 }}83 />84 </ModalBody>85 </ModalContent>86 </Modal>87 )88}8990export default UpdateTodoModal
This is similar to insert query in supabase except using update
function with match
by id.
1const { data, error } = await supabaseClient2 .from("todos")3 .update({ name: item.name })4 .match({ id: item.id })
To delete an item from the list, add mutation query inside Todos/index.tsx
with delete functionality.
1const { mutate } = useMutation(2 async (id: number) => {3 const { data, error } = await supabaseClient4 .from("todos")5 .delete()6 .match({ id })78 if (error) {9 toast.error("Something went wrong")10 return error11 }1213 return data14 },15 {16 onSuccess: () => {17 toast.success("Item Deleted successfully")18 return queryClient.refetchQueries("todos")19 },20 }21)
Then, update DeleteIcon
onClick functionality with the delete function,
1const onDeleteItemClick = async (id: number) => {2 await mutate(id)3}45return (6 <DeleteIcon7 cursor={"pointer"}8 color="red.500"9 mr="2"10 onClick={() => onDeleteItemClick(todo.id)}11 />12)
complete code for Todos/index.tsx
will be,
1import { useState } from "react";2import {3 QueryClient,4 useMutation,5 useQuery,6 useQueryClient,7} from "react-query";8import { supabaseClient } from "@lib/supabaseClient";9import {10 Spinner,11 Stack,12 ListItem,13 OrderedList,14 ListIcon,15 HStack,16 VStack,17 Flex,18 Text,19} from "@chakra-ui/react";20import { DeleteIcon, EditIcon } from "@chakra-ui/icons";21import toast from "react-hot-toast";2223import UpdateTodoModal from "./UpdateModal";2425const Todos = () => {26 const [isOpen, setIsOpen] = useState<boolean>(false);27 const [selectedItem, setSelectedItem] = useState<{28 id: number;29 name: string;30 }>();3132 const queryClient = useQueryClient();3334 const { data, isLoading, isError, isSuccess } = useQuery(35 "todos",36 async () => {37 const { data, error } = await supabaseClient38 .from("todos")39 .select()40 .order("id", { ascending: true });4142 if (error) {43 throw new Error(error.message);44 }4546 return data;47 }48 );4950 const { mutate } = useMutation(51 async (id: number) => {52 const { data, error } = await supabaseClient53 .from("todos")54 .delete()55 .match({ id });5657 if (error) {58 toast.error("Something went wrong");59 return error;60 }6162 return data;63 },64 {65 onSuccess: () => {66 toast.success("Item Deleted successfully");67 return queryClient.refetchQueries("todos");68 },69 }70 );7172 if (isLoading) {73 return (74 <Stack>75 <Spinner size="xl" />76 </Stack>77 );78 }7980 const handleUpdateModalClose = () => {81 setIsOpen(!isOpen);82 };8384 const onItemClick = (todo: { id: number; name: string }) => {85 setSelectedItem(todo);86 setIsOpen(true);87 };8889 const onDeleteItemClick = async (id: number) => {90 await mutate(id);91 };9293 return (94 <>95 <VStack>96 {data!.map((todo: any) => (97 <HStack key={todo.id} spacing="24px">98 <Flex p={6} w="300px" h="50px" justifyContent="space-between">99 <Text>{todo.name}</Text>100101 <Flex w="10px">102 <DeleteIcon103 cursor={"pointer"}104 color="red.500"105 mr="2"106 onClick={() => onDeleteItemClick(todo.id)}107 />108 <EditIcon109 cursor={"pointer"}110 onClick={() => onItemClick(todo)}111 />112 </Flex>113 </Flex>114 </HStack>115 ))}116 </VStack>117 <UpdateTodoModal118 todo={selectedItem!}119 isOpen={isOpen}120 onClose={handleUpdateModalClose}121 />122 </>123 );124};125126export default Todos;
Complete source code is available here
No spam, ever. Unsubscribe anytime.