In this 4-part series, we will explore the process of creating robust, scalable, and secure web applications using the powerful combination of Next.js and Supabase. NextJS, a leading React framework, offers server-side rendering, static site generation, and performance optimization, making it perfect for building dynamic and SEO-friendly websites. Supabase, on the other hand, provides a comprehensive backend-as-a-service solution, featuring a PostgreSQL database, real-time data capabilities, and streamlined authentication services.

🔗 Check out the full code for this series here.

By integrating Supabase with Next.js, we can leverage the strengths of both platforms to create full-stack applications easily. Supabase simplifies backend operations by handling database management, authentication, and real-time updates, allowing us to focus on building exceptional user experiences. Meanwhile, Next.js enhances the front-end with its robust rendering capabilities and optimization features. This integration enables us to build fast, secure, and scalable applications without the complexity of managing servers or writing extensive backend code.

Throughout this series, we will go through the following key topics:

  1. Setting Up Our Next.js App: We'll start by creating a brand new Next.js application using ShadCN and Tailwind CSS for styling. Next, we will create essential authentication pages, including Signup, Login, Forgot Password, and Reset Password.
  2. Supabase Integration: Next, we'll create a Supabase project and configure routes and middlewares to handle authentication and basic route protection. This will ensure our application is secure and only authorized users can access sensitive data.
  3. Google Authentication: We'll also set up a Google Cloud console project and integrate Google authentication into our application, providing users with a seamless login experience.
  4. Advanced Database Management: Finally, we'll dive into advanced database management techniques by using Prisma ORM to create, migrate, and query tables. We will add triggers to create and manage user data and implement Row-Level Security (RLS) in our Supabase PostgreSQL tables. This ensures that only authenticated users can manipulate data owned by them.

To streamline form handling and validation, we will use React Hook Forms for managing form state and Zod for validating form data. These tools will help us create robust and user-friendly forms that enhance the overall user experience.

Initializing a Shadcn Project with Next.js and pnpm

Shadcn UI, combined with Tailwind CSS, offers a powerful way to build visually appealing web applications. Here's how to initialize a Shadcn project with Next.js using pnpm.

  1. Install and Initialize Shadcn: Use the following command to initialize your project with default options:

    pnpm dlx shadcn@latest init -d
    

    When prompted, select Next.js as your project type. This command will set up your project with necessary dependencies and configurations. It includes setting up CSS variables for theming. The -d flag creates the project with default options.

  2. Start Development:

    Run your Next.js development server with:

    pnpm dev
    

Our project is now ready to be customized and developed further. This setup provides a solid foundation for building modern web applications with Shadcn UI and Next.js.

Route creation for Authentication

In this section we will create four basic form pages using Shadcn UI: signuploginforgot-password, and reset-password. We'll cover how to add necessary Shadcn components and create the forms.

Add Components Individually:

Use the following commands to add specific components:

pnpm dlx shadcn@latest add button input form card separator

Installing Dependencies

Let’s install “Zod” and “React Hook Forms” for form building and validations

pnpm add zod react-hook-form @hookform/resolvers

Let’s install “lucide-react” for beautiful icons

pnpm add lucide-react

Creating Form Schemas for Form Validation

We can create very refined and granular form schemas for custom validation using zod. Create a file lib/constants.ts

💡 Tip: These are just basic validations to show you how to add them. You should customise these validations as per your needs.

lib/constants.ts

// Auth Management

import { z } from "zod";

export const SignupFormSchema = z
    .object({
        email: z.string().email({
            message: "Please enter a valid email address.",
        }),
        password: z.string().min(8, {
            message: "Password must be at least 8 characters.",
        }),
        confirmPassword: z.string(),
    })
    .refine((data) => data.password === data.confirmPassword, {
        path: ["confirmPassword"],
        message: "Passwords do not match",
    });

export const LoginFormSchema = z.object({
  email: z.string().email({
    message: "Please enter a valid email address.",
  }),
  password: z.string().min(1, {
    message: "Password is required.",
  }),
})

export const ForgotPasswordFormSchema = z.object({
  email: z.string().email({
    message: "Please enter a valid email address.",
  }),
})

export const ResetPasswordFormSchema = z.object({
  password: z.string().min(8, {
    message: "Password must be at least 8 characters.",
  }),
  confirmPassword: z.string()
}).refine((data) => data.password === data.confirmPassword, {message: "Passwords do not match"})

export type TSignupFormSchema = z.infer<typeof SignupFormSchema>
export type TLoginFormSchema = z.infer<typeof LoginFormSchema>
export type TForgotPasswordFormSchema = z.infer<typeof ForgotPasswordFormSchema>
export type TResetPasswordFormSchema = z.infer<typeof ResetPasswordFormSchema>

We have created schemas using Zod and inferred their types using zod itself. Now let’s use these schemas to create forms using react-hook-form

Create Form Pages

Create a new file for each form page in your app directory.

Signup Page (app/signup/page.tsx):

"use client"

import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"

import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Separator } from "@/components/ui/separator"

import { Mail } from "lucide-react"
import { SignupFormSchema, TSignupFormSchema } from "@/lib/constants"

export default function SignupForm() {
  const form = useForm<TSignupFormSchema>({
    resolver: zodResolver(SignupFormSchema),
    mode: "onChange",
    defaultValues: {
      email: "",
      password: "",
      confirmPassword: "",
    },
  })

  function onSubmit(values: TSignupFormSchema) {
    // This would typically send the data to your API
    console.log(values)
  }

  return (
    <Card className="w-full max-w-md mx-auto mt-32">
      <CardHeader>
        <CardTitle className="text-2xl">Create an accountCardTitle>
        <CardDescription>Enter your information to get started.CardDescription>
      CardHeader>
      <CardContent>
        <Form {...form}>
          <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
            <FormField
              control={form.control}
              name="email"
              render={({ field }) => (
                <FormItem>
                  <FormLabel>EmailFormLabel>
                  <FormControl>
                    <Input placeholder="[email protected]" type="email" {...field} />
                  FormControl>
                  <FormDescription>Enter a valid emailFormDescription>
                  <FormMessage />
                FormItem>
              )}
            />
            <FormField
              control={form.control}
              name="password"
              render={({ field }) => (
                <FormItem>
                  <FormLabel>PasswordFormLabel>
                  <FormControl>
                    <Input placeholder="Create a password" type="password" {...field} />
                  FormControl>
                  <FormDescription>Must be at least 8 charactersFormDescription>
                  <FormMessage />
                FormItem>
              )}
            />
             <FormField
              control={form.control}
              name="confirmPassword"
              render={({ field }) => (
                <FormItem>
                  <FormLabel>Confirm passwordFormLabel>
                  <FormControl>
                    <Input placeholder="Retype the password again" type="password" {...field} />
                  FormControl>
                  <FormDescription>Must be same as the passwordFormDescription>
                  <FormMessage />
                FormItem>
              )}
            />
            <Button type="submit" className="w-full">
              Sign up
            Button>

            <div className="relative my-4">
              <Separator />
              <div className="absolute inset-0 flex items-center justify-center">
                <span className="bg-background px-2 text-xs text-muted-foreground">OR CONTINUE WITHspan>
              div>
            div>

            <Button type="button" variant="outline" className="w-full" onClick={() => console.log("Google sign-in")}>  
                <Mail size={16} className="mr-2" />
                Sign up with Google
            Button>
          form>
        Form>
      CardContent>
      <CardFooter className="flex justify-center border-t pt-6">
        <p className="text-sm text-muted-foreground">
          Already have an account?{" "}
          <a href="/login" className="text-primary font-medium hover:underline">
            Sign in
          a>
        p>
      CardFooter>
    Card>
  )
}

Login Page (app/login/page.tsx):

"use client"

import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"

import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Separator } from "@/components/ui/separator"

import { Mail } from "lucide-react"
import { TLoginFormSchema, LoginFormSchema } from "@/lib/constants"

export default function LoginForm() {
  const form = useForm({
    resolver: zodResolver(LoginFormSchema),
    mode: "onChange",
    defaultValues: {
      email: "",
      password: "",
    },
  })

  function onSubmit(values: TLoginFormSchema) {
    // This would typically send the data to your API
    console.log(values)
  }

  return (
     className="w-full max-w-md mx-auto mt-32">
      
         className="text-2xl">Welcome back
        Sign in to your account to continue
      
      
         {...form}>
           onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
            
              control={form.control}
              name="email"
              render={({ field }) => (
                <FormItem>
                  Email
                  
                     placeholder="[email protected]" type="email" {...field} />
                  
                   />
                
              )}
            />
            
              control={form.control}
              name="password"
              render={({ field }) => (
                <FormItem>
                  Password
                  
                     placeholder="Enter your password" type="password" {...field} />
                  
                   />
                
              )}
            />
             className="flex items-center justify-end">
               variant="link"  className="px-0 font-normal" type="button">
                 href="/forgot-password" className="text-primary font-medium hover:underline">Forgot password?
              
            
             type="submit" className="w-full">
              Sign in
            

             className="relative my-4">
               />
               className="absolute inset-0 flex items-center justify-center">
                 className="bg-background px-2 text-xs text-muted-foreground">OR CONTINUE WITH
              
            

             type="button" variant="outline" className="w-full" onClick={() => console.log("Google sign-in")}>
               size={16} className="mr-2" />
              Sign in with Google
            
          
        
      
       className="flex justify-center border-t pt-6">
         className="text-sm text-muted-foreground">
          Don't have an account?{" "}
           href="/signup" className="text-primary font-medium hover:underline">
            Sign up
          
        
      
    
  )
}

Forgot Password Page (app/forgot-password/page.tsx):

"use client"

import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"

import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"
import { Input } from "@/components/ui/input"

import { TForgotPasswordFormSchema, ForgotPasswordFormSchema } from "@/lib/constants"

export default function SignupForm() {
  const form = useForm({
    resolver: zodResolver(ForgotPasswordFormSchema),
    mode: "onChange",
    defaultValues: {
      email: "",
    },
  })

  function onSubmit(values: TForgotPasswordFormSchema) {
    // This would typically send the data to your API
    console.log(values)
  }

  return (
     className="w-full max-w-md mx-auto mt-32">
      
         className="text-2xl">Forgot password
        Enter your email to get a verification link
      
      
         {...form}>
           onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
            
              control={form.control}
              name="email"
              render={({ field }) => (
                <FormItem>
                  Email
                  
                     placeholder="[email protected]" type="email" {...field} />
                  
                   />
                
              )}
            />
             type="submit" className="w-full">
              Send Email
            
          
        
      
       className="flex justify-center border-t pt-6">
         className="text-sm text-muted-foreground">
          Remember your password?{" "}
           href="/login" className="text-primary font-medium hover:underline">
            Sign in
          
        
      
    
  )
}

Reset Password Page (app/reset-password/page.tsx):

"use client"

import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"

import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"
import { Input } from "@/components/ui/input"

import { TResetPasswordFormSchema, ResetPasswordFormSchema } from "@/lib/constants"

export default function SignupForm() {
  const form = useForm({
    resolver: zodResolver(ResetPasswordFormSchema),
    mode: "onChange",
    defaultValues: {
      password: "",
      confirmPassword: "",
    },
  })

  function onSubmit(values: TResetPasswordFormSchema) {
    // This would typically send the data to your API
    console.log(values)
  }

  return (
     className="w-full max-w-md mx-auto mt-32">
      
         className="text-2xl">Reset password
        Create a new password
      
      
         {...form}>
           onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
            
              control={form.control}
              name="password"
              render={({ field }) => (
                <FormItem>
                  New password
                  
                     placeholder="Create a password" type="password" {...field} />
                  
                  Must be at least 8 characters
                   />
                
              )}
            />
             
              control={form.control}
              name="confirmPassword"
              render={({ field }) => (
                <FormItem>
                  Confirm password
                  
                     placeholder="Retype the password again" type="password" {...field} />
                  
                  Must be same as the password
                   />
                
              )}
            />
             type="submit" className="w-full">
              Save
            
          
        
      
    
  )
}

🗒️ Things to be noted

  1. The Zod library makes it easier to infer types from the Zod Schema and provides full type safety features of the TypeScript language.
  2. Use Zod Resolver adapter to ensure our React Hook Form works with Zod validations.
  3. The mode onChange ensures we validate the input on change of the values in the field.
{ ...mode: "onChange" }

The binding of other props, and errors in the form are taken by the render() function in the component via the { …field } prop.

As you can see, we are not handling the form onSubmit functions. We are just console logging the values from the form on submit.

Test out these pages by visiting their respective routes. In the next part of this series, we will create a Supabase project and add email signup and authentication to our app.

🔗 Check out the full code for this series here.

If you found this article useful, like, comment, and share, or just buy me a coffee?