Secure Your Next.js App: Email & Google Authentication with Supabase, PostgreSQL RLS, and Triggers - Part 1

Published on March 15, 2025

This article might not be well formatted due to markdown render issues.

Read my blogs here
Secure Your Next.js App: Email & Google Authentication with Supabase, PostgreSQL RLS, and Triggers - Part 1

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: signup, login, forgot-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 account</CardTitle>
        <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>Email</FormLabel>
                  <FormControl>
                    <Input placeholder="you@example.com" type="email" {...field} />
                  </FormControl>
                  <FormDescription>Enter a valid email</FormDescription>
                  <FormMessage />
                </FormItem>
              )}
            />
            <FormField
              control={form.control}
              name="password"
              render={({ field }) => (
                <FormItem>
                  <FormLabel>Password</FormLabel>
                  <FormControl>
                    <Input placeholder="Create a password" type="password" {...field} />
                  </FormControl>
                  <FormDescription>Must be at least 8 characters</FormDescription>
                  <FormMessage />
                </FormItem>
              )}
            />
             <FormField
              control={form.control}
              name="confirmPassword"
              render={({ field }) => (
                <FormItem>
                  <FormLabel>Confirm password</FormLabel>
                  <FormControl>
                    <Input placeholder="Retype the password again" type="password" {...field} />
                  </FormControl>
                  <FormDescription>Must be same as the password</FormDescription>
                  <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 WITH</span>
              </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<TLoginFormSchema>({
    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 (
    <Card className="w-full max-w-md mx-auto mt-32">
      <CardHeader>
        <CardTitle className="text-2xl">Welcome back</CardTitle>
        <CardDescription>Sign in to your account to continue</CardDescription>
      </CardHeader>
      <CardContent>
        <Form {...form}>
          <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
            <FormField
              control={form.control}
              name="email"
              render={({ field }) => (
                <FormItem>
                  <FormLabel>Email</FormLabel>
                  <FormControl>
                    <Input placeholder="you@example.com" type="email" {...field} />
                  </FormControl>
                  <FormMessage />
                </FormItem>
              )}
            />
            <FormField
              control={form.control}
              name="password"
              render={({ field }) => (
                <FormItem>
                  <FormLabel>Password</FormLabel>
                  <FormControl>
                    <Input placeholder="Enter your password" type="password" {...field} />
                  </FormControl>
                  <FormMessage />
                </FormItem>
              )}
            />
            <div className="flex items-center justify-end">
              <Button variant="link"  className="px-0 font-normal" type="button">
                <a href="/forgot-password" className="text-primary font-medium hover:underline">Forgot password?</a>
              </Button>
            </div>
            <Button type="submit" className="w-full">
              Sign in
            </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 WITH</span>
              </div>
            </div>

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

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<TForgotPasswordFormSchema>({
    resolver: zodResolver(ForgotPasswordFormSchema),
    mode: "onChange",
    defaultValues: {
      email: "",
    },
  })

  function onSubmit(values: TForgotPasswordFormSchema) {
    // 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">Forgot password</CardTitle>
        <CardDescription>Enter your email to get a verification link</CardDescription>
      </CardHeader>
      <CardContent>
        <Form {...form}>
          <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
            <FormField
              control={form.control}
              name="email"
              render={({ field }) => (
                <FormItem>
                  <FormLabel>Email</FormLabel>
                  <FormControl>
                    <Input placeholder="you@example.com" type="email" {...field} />
                  </FormControl>
                  <FormMessage />
                </FormItem>
              )}
            />
            <Button type="submit" className="w-full">
              Send Email
            </Button>
          </form>
        </Form>
      </CardContent>
      <CardFooter className="flex justify-center border-t pt-6">
        <p className="text-sm text-muted-foreground">
          Remember your password?{" "}
          <a href="/login" className="text-primary font-medium hover:underline">
            Sign in
          </a>
        </p>
      </CardFooter>
    </Card>
  )
}

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<TResetPasswordFormSchema>({
    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 (
    <Card className="w-full max-w-md mx-auto mt-32">
      <CardHeader>
        <CardTitle className="text-2xl">Reset password</CardTitle>
        <CardDescription>Create a new password</CardDescription>
      </CardHeader>
      <CardContent>
        <Form {...form}>
          <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
            <FormField
              control={form.control}
              name="password"
              render={({ field }) => (
                <FormItem>
                  <FormLabel>New password</FormLabel>
                  <FormControl>
                    <Input placeholder="Create a password" type="password" {...field} />
                  </FormControl>
                  <FormDescription>Must be at least 8 characters</FormDescription>
                  <FormMessage />
                </FormItem>
              )}
            />
             <FormField
              control={form.control}
              name="confirmPassword"
              render={({ field }) => (
                <FormItem>
                  <FormLabel>Confirm password</FormLabel>
                  <FormControl>
                    <Input placeholder="Retype the password again" type="password" {...field} />
                  </FormControl>
                  <FormDescription>Must be same as the password</FormDescription>
                  <FormMessage />
                </FormItem>
              )}
            />
            <Button type="submit" className="w-full">
              Save
            </Button>
          </form>
        </Form>
      </CardContent>
    </Card>
  )
}

🗒️ 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 <Input> 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?

Ojaswi Athghara

SDE, 4+ Years

ojaswiat@gmail.com

© ojaswiat.com 2025-2027