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.
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:
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.
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.
pnpm dlx shadcn@latest init -d
-d
flag creates the project with default options.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.
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.
Use the following commands to add specific components:
pnpm dlx shadcn@latest add button input form card separator
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
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 a new file for each form page in your app
 directory.
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>
)
}
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>
)
}
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>
)
}
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>
)
}
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?