Handling Data Storage with Cloudinary and Supabase

Ekene Eze

Handling data storage with Cloudinary and Supabase in NextJS

When building Jamstack applications, data storage is often a significant consideration. The popular choices are often CMSs and for good reason. However, In this post, we'll explore a different approach that combines Cloudinary and Supabase to store and retrieve user data upon registration.

Users often provide their name, email, and avatar when signing up for online accounts. What if we can store the data in a database like Supabase and deliver the images via Cloudinary. This would allow us to have a central data store but also give us the ability to apply transformations and on-the-fly manipulations to our media assets.

Let's dive in and find out how we can achieve that. First, here are the technologies we'll be using to demonstrate this approach.

  • Cloudinary is a platform on which we can upload, store, manage, transform, and deliver images and videos for web and mobile applications.

  • Supabase is the Open Source Firebase Alternative that makes it easy to create backend functionalities for our project in less than 2 minutes.

  • Next.js is a React-based frontend development framework that supports server-side rendering and static site generation.

Sandbox

We completed this project in a CodeSandbox. Fork and run it to quickly get started.

View source code on GitHub.

Prerequisites

This post requires the following:

  • Experience with JavaScript and React.
  • Installation of Node.js.
  • Familiarity with Next.js.
  • A Cloudinary account. Signup is free!
  • A Supabase account. Signup here.

Setting Up and Installations

First, create an unsigned upload preset on Cloudinary. This will make it possible for us to upload data to Cloudinary from a client.

To do that, click on the Settings >> Uploads

Now, scroll down to upload presets and click on “Add upload presets.”

  • Create an upload preset name.
  • Set signing mode to unsigned.
  • Create the desired folder to house the image uploads.

Next, we'll log in to Supabase and create a new project.

After creating the project in a new or an already existing organization, copy and save the anon public key and the project URL. We'll be using them later in this application.

Next, let's create a Supabase Schema that will hold our data:

  1. Click on the table editor icon, and click on the Create a new table button.
  2. Give the table a name, add columns for the fields you want, and click the Save button.

Setup a Next.js project

Create a new Next.js application named cloudinary-next-demo by running the following command in the terminal:

1npx create-next-app data-handling

This will create a data-handling application for you. After that, change directory into the data-handling project you created with the command:

Next, run the following command in the terminal to create a new Next.js application:

1npx create-next-app data-handling

The above command creates a starter next.js application in the project folder.

Next, install the bootstrap library for styling with the following command:

1# to navigate into the project directory
2cd data-handling
3
4# Install Bootstrap
5npm install react-bootstrap bootstrap

Next, import the Bootstrap CSS file into the _app.js file.

1import "bootstrap/dist/css/bootstrap.min.css";

Finally, run the following command to start our dev server:

1npm run dev # to run the dev server

Next.js will start a live development server at http://localhost:3000.

Handling Data

When a user submits the registration form, we'll send their data to Supabase for storage. Consequently, we'll retrieve the submitted image of the user and host it on Cloudinary. From there, we can apply transformations and serve them to the client on request.

First, let's set up our Supabase configurations locally.

Create a .env.local file in the root directory and store our supabase credentials:

1//.env.local
2const SUPABASE_KEY = "PUBLIC_ANON_KEY"
3const SUPABASE_URL = "PROJECT_URL"

Next, create a utils folder in the project's root directory and add a Utils.js file with the following snippets in it to configure our Supabase instance:

1//utils/Utils.js
2import { createClient } from "@supabase/supabase-js";
3
4const supabaseUrl = process.env.SUPABASE_URL;
5const supabaseKey = process.env.SUPABASE_KEY;
6
7export const supabase = createClient(supabaseUrl, supabaseKey);

User Registration

To handle the user registration logic, we'll add a components folder in the root directory ofo our project and create a RegisterUser.js file within. Update the file with this snippet:

1// src/components/RegisterUser
2import { useState } from "react";
3import { Form, Button } from "react-bootstrap";
4import { supabase } from "../utils/Utils";
5const initialState = {
6 firstName: "",
7 lastName: "",
8 email: "",
9 avatar: null
10};
11export default function RegisterUser() {
12 const [formData, setFormData] = useState(initialState);
13 const [loading, setLoading] = useState(true);
14 const [status, setStatus] = useState(false);
15
16 const handleChange = async (e) => {
17 e.preventDefault();
18 setFormData({ ...formData, [e.target.name]: e.target.value });
19 };
20
21 // handleImage() logic here
22
23 return (
24 <div className="container-fluid">
25 <h1 className="fs-1 m-3"> Register User</h1>
26 {status && (
27 <div className="mt-3 mb-3 text-success">
28 User registered successfully!
29 </div>
30 )}
31 <div style={{ width: "400px" }}>
32 //render form here
33 </div>
34 </div>
35 );
36}

In the snippet above, we:

  • Import the packages we'll need to add the necessary logic and styling to our registration form.
  • Initialized the initialState object and added some properties to it.
  • Created formData state and passed initialState to it.
  • Created the loading state to disable the Register button when uploading media assets to Cloudinary.
  • Initialized a status state to conditionally render the status of the supabase user registration.
  • Created the handleChange function to handle changes in our form input fields.

Next, let's define a function to handle the image upload to Cloudinary:

1// src/components/RegisterUser
2// import statements here
3export default function RegisterUser() {
4 // state definitions here
5
6 const handleImage = async (e) => {
7 const reader = new FileReader();
8 reader.onloadend = async () => {
9 setLoading(true);
10 };
11 const files = e.target.files[0];
12 if (!files) return;
13 const data = new FormData();
14 data.append("file", files);
15 data.append("upload_preset", "c_tags");
16 const res = await fetch(
17 "CLOUDINARY_UPLOAD_URL",
18 {
19 method: "POST",
20 body: data
21 }
22 );
23 const file = await res.json();
24 setFormData({ ...formData, avatar: file.secure_url });
25 setLoading(false);
26 };
27
28 return ();
29}

Here, we defined the handleImage() function to upload the selected image asset from the computer to Cloudinary. Then we used a callback to return the image URL to the client for rendering.

Finally, let's define a createUser() function that will get the user inputs from the form fields along with the image URL from Cloudinary and store them in our Supabase database.

When this is successful, we'll set the status to be true if there are no errors, and also clear the form fields:

1const createUser = async () => {
2 const res = await supabase.from("profiles").insert([
3 {
4 first_name: `${formData.firstName}`,
5 last_name: `${formData.lastName}`,
6 email: `${formData.email}`,
7 avatar: `${formData.avatar}`
8 }
9 ]);
10 if (res.error === null && res.status === 201) {
11 setStatus(true);
12 setFormData(initialState);
13 setTimeout(() => {
14 setStatus(false);
15 }, 5000);
16 }
17 };

The complete code snippets for the RegisterUser.js component is available in this Github gist if you'd like to copy it from a single place:

https://gist.github.com/kenny-io/b029df5525d4d331b8a079c9a1bb35df

Next, let's import RegisterUser.js inside our index.js and render it like this:

1//pages/index.js
2
3import { useState } from "react";
4import RegisterUser from "../components/RegisterUser";
5export default function IndexPage() {
6 return (
7 <div className="container-fluid" >
8 <RegisterUser />
9 </div>
10 );
11}

Now in the browser, we can choose an image, fill out the form and click on the Register button to successfully register a user in our supabase project.

Retrieve users from Supabase

To get all the users we've saved to our Supabase database, create a getUsers.js file in the components folder and update it with the following snippet:

1// components/GetUsers.js
2import React, { useState, useEffect } from "react";
3import { Button, Card, ListGroup, ListGroupItem } from "react-bootstrap";
4import { supabase } from "../utils/Utils";
5
6function GetUsers() {
7 const [users, setUsers] = useState();
8 const [showUsers, setShowUsers] = useState(false);
9
10 async function getUsers() {
11 const res = await supabase.from("profiles").select();
12 setUsers(res.data);
13 }
14
15 return (
16 <div>
17 <Button
18 variant="success"
19 onClick={() => {
20 getUsers();
21 setShowUsers(!showUsers);
22 }}
23 >
24 {!showUsers ? "Show registered users" : "Hide registered users"}
25 </Button>
26 {showUsers &&
27 users &&
28 users.map((user) => (
29 <Card key={user.id} className="mt-3 mb-3" style={{ width: "15rem" }}>
30 <Card.Img variant="top" src={user.avatar} />
31 <Card.Body>
32 <Card.Title>{user.first_name + " " + user.last_name}</Card.Title>
33 </Card.Body>
34 <ListGroup className="list-group-flush">
35 <ListGroupItem> Email: {user.email}</ListGroupItem>
36 </ListGroup>
37 </Card>
38 ))}
39 </div>
40 );
41}
42export default GetUsers;

In the snippets above, we:

  • Import necessary packages from Bootstrap and Supabase.
  • Define state variables for users and showUsers to conditionally show the users.
  • Create the getUsers function to fetch the users in our Supabase project and update the users variable in state.
  • Render a button that will show the registered users.
  • Loop through the users data to display all users.

Next, let's import the GetUsers.js component and render it inside index.js. When we click the show registered user button in the browser, we will see all the users registered in the supabase project.

Conclusion

In this post, we went over the process of storing user data to Supabase and serving images from Cloudinary when reading user data. This helps consolidate our data without restricting the functionalities that Cloudinary provides to help us manage assets. Feel free to fork the CodeSandbox here and extend the project as you see fit.

Ekene Eze

Director of DX at Plasmic

I work at Plasmic as the Director of Developer Experience and I love executing projects that help other software engineers. Passionate about sharing with the community, I often write, present workshops, create courses, and speak at conferences about web development concepts and best practices.