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.
We completed this project in a CodeSandbox. Fork and run it to quickly get started.
View source code on GitHub.
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.
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:
- Click on the table editor icon, and click on the Create a new table button.
- Give the table a name, add columns for the fields you want, and click the Save button.
Create a new Next.js application named cloudinary-next-demo by running the following command in the terminal:
npx 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:
npx 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:
# to navigate into the project directory
cd data-handling
# Install Bootstrap
npm install react-bootstrap bootstrap
Code language: PHP (php)
Next, import the Bootstrap CSS file into the _app.js
file.
import "bootstrap/dist/css/bootstrap.min.css";
Code language: JavaScript (javascript)
Finally, run the following command to start our dev server:
npm run dev # to run the dev server
Code language: PHP (php)
Next.js will start a live development server at http://localhost:3000.
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:
//.env.local
const SUPABASE_KEY = "PUBLIC_ANON_KEY"
const SUPABASE_URL = "PROJECT_URL"
Code language: JavaScript (javascript)
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:
//utils/Utils.js
import { createClient } from "@supabase/supabase-js";
const supabaseUrl = process.env.SUPABASE_URL;
const supabaseKey = process.env.SUPABASE_KEY;
export const supabase = createClient(supabaseUrl, supabaseKey);
Code language: JavaScript (javascript)
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:
// src/components/RegisterUser
import { useState } from "react";
import { Form, Button } from "react-bootstrap";
import { supabase } from "../utils/Utils";
const initialState = {
firstName: "",
lastName: "",
email: "",
avatar: null
};
export default function RegisterUser() {
const [formData, setFormData] = useState(initialState);
const [loading, setLoading] = useState(true);
const [status, setStatus] = useState(false);
const handleChange = async (e) => {
e.preventDefault();
setFormData({ ...formData, [e.target.name]: e.target.value });
};
// handleImage() logic here
return (
<div className="container-fluid">
<h1 className="fs-1 m-3"> Register User</h1>
{status && (
<div className="mt-3 mb-3 text-success">
User registered successfully!
</div>
)}
<div style={{ width: "400px" }}>
//render form here
</div>
</div>
);
}
Code language: JavaScript (javascript)
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 passedinitialState
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:
// src/components/RegisterUser
// import statements here
export default function RegisterUser() {
// state definitions here
const handleImage = async (e) => {
const reader = new FileReader();
reader.onloadend = async () => {
setLoading(true);
};
const files = e.target.files[0];
if (!files) return;
const data = new FormData();
data.append("file", files);
data.append("upload_preset", "c_tags");
const res = await fetch(
"CLOUDINARY_UPLOAD_URL",
{
method: "POST",
body: data
}
);
const file = await res.json();
setFormData({ ...formData, avatar: file.secure_url });
setLoading(false);
};
return ();
}
Code language: JavaScript (javascript)
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:
const createUser = async () => {
const res = await supabase.from("profiles").insert([
{
first_name: `${formData.firstName}`,
last_name: `${formData.lastName}`,
email: `${formData.email}`,
avatar: `${formData.avatar}`
}
]);
if (res.error === null && res.status === 201) {
setStatus(true);
setFormData(initialState);
setTimeout(() => {
setStatus(false);
}, 5000);
}
};
Code language: JavaScript (javascript)
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:
//pages/index.js
import { useState } from "react";
import RegisterUser from "../components/RegisterUser";
export default function IndexPage() {
return (
<div className="container-fluid" >
<RegisterUser />
</div>
);
}
Code language: JavaScript (javascript)
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.
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:
// components/GetUsers.js
import React, { useState, useEffect } from "react";
import { Button, Card, ListGroup, ListGroupItem } from "react-bootstrap";
import { supabase } from "../utils/Utils";
function GetUsers() {
const [users, setUsers] = useState();
const [showUsers, setShowUsers] = useState(false);
async function getUsers() {
const res = await supabase.from("profiles").select();
setUsers(res.data);
}
return (
<div>
<Button
variant="success"
onClick={() => {
getUsers();
setShowUsers(!showUsers);
}}
>
{!showUsers ? "Show registered users" : "Hide registered users"}
</Button>
{showUsers &&
users &&
users.map((user) => (
<Card key={user.id} className="mt-3 mb-3" style={{ width: "15rem" }}>
<Card.Img variant="top" src={user.avatar} />
<Card.Body>
<Card.Title>{user.first_name + " " + user.last_name}</Card.Title>
</Card.Body>
<ListGroup className="list-group-flush">
<ListGroupItem> Email: {user.email}</ListGroupItem>
</ListGroup>
</Card>
))}
</div>
);
}
export default GetUsers;
Code language: JavaScript (javascript)
In the snippets above, we:
- Import necessary packages from Bootstrap and Supabase.
- Define state variables for
users
andshowUsers
to conditionally show the users. - Create the
getUsers
function to fetch theusers
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.
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.