Building modern web applications that handle data efficiently while providing excellent user experiences has never been more accessible. The combination of Supabase and Next.js creates a powerful, scalable foundation for full-stack applications that can grow with your needs. This comprehensive guide will walk you through creating a complete CRUD (Create, Read, Update, Delete) application from scratch, incorporating authentication, real-time features, and production-ready deployment strategies.
What You'll Learn in This Guide
By the end of this tutorial, you'll have built a fully functional web application featuring:
- User authentication with secure session management
- Complete CRUD operations for managing data
- Real-time updates across multiple users
- Responsive design with modern UI components
- Production deployment with proper security configurations
This guide assumes basic knowledge of JavaScript and React, but provides detailed explanations for Supabase-specific concepts and Next.js patterns.
Why Supabase + Next.js Is the Perfect Stack for Modern Apps
The combination of Supabase and Next.js addresses common development challenges while maintaining flexibility and performance. Supabase provides a complete backend solution with minimal configuration, while Next.js offers optimized React development with built-in features like server-side rendering and API routes. Together, they eliminate the complexity of backend setup while delivering enterprise-grade capabilities.
This stack is particularly valuable for startups and developers who need to ship quickly without sacrificing scalability or security. The open-source nature of both tools ensures long-term viability and community support.
Understanding the Tech Stack
What Is Supabase? – The Open Source Firebase Alternative
Supabase is an open-source Backend-as-a-Service (BaaS) platform that provides essential backend services including:
- PostgreSQL Database: Full-featured relational database with JSON support
- Authentication: Built-in user management with multiple providers
- Real-time Subscriptions: Live data updates using WebSockets
- Storage: File upload and management system
- Edge Functions: Serverless functions for custom logic
Unlike proprietary alternatives, Supabase runs on standard PostgreSQL, ensuring data portability and avoiding vendor lock-in. The platform generates REST and GraphQL APIs automatically based on your database schema.
Why Choose Next.js for Your Frontend?
Next.js extends React with production-ready features that streamline development:
- File-based Routing: Automatic route generation from folder structure
- Server-Side Rendering (SSR): Improved SEO and initial load performance
- Static Site Generation (SSG): Pre-built pages for optimal performance
- API Routes: Backend functionality within the same codebase
- Image Optimization: Automatic image processing and lazy loading
Key Benefits of Combining Supabase with Next.js
The synergy between these technologies creates several advantages:
- Rapid Development: Auto-generated APIs and built-in authentication reduce boilerplate code
- Type Safety: TypeScript support across the entire stack
- Scalability: Both platforms handle growth automatically
- Developer Experience: Excellent tooling and documentation
- Cost Efficiency: Generous free tiers for development and small applications
Project Setup and Tooling
Setting Up a New Next.js Project
Create a new Next.js application with TypeScript support:
npx create-next-app@latest my-crud-app --typescript --tailwind --eslint --app
cd my-crud-app
This command creates a modern Next.js project with the App Router, TypeScript configuration, and Tailwind CSS for styling.
Installing and Configuring Supabase Client SDK
Install the Supabase JavaScript client library:
npm install @supabase/supabase-js
Create a Supabase client configuration file at lib/supabase.ts
:
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
Environment Variables: Keeping Secrets Safe
Create a .env.local
file in your project root:
NEXT_PUBLIC_SUPABASE_URL=your_supabase_project_url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
These variables are prefixed with NEXT_PUBLIC_
to make them available in the browser. Never expose service role keys in client-side code.
Designing the Database with Supabase
Creating Tables and Relationships in Supabase
Navigate to your Supabase dashboard and create a new table for your CRUD application. For this example, we'll create a "posts" table:
CREATE TABLE posts (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
title VARCHAR(255) NOT NULL,
content TEXT,
author_id UUID REFERENCES auth.users(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Create an index for better query performance
CREATE INDEX posts_author_id_idx ON posts(author_id);
Auto-Generated APIs: Let Supabase Do the Heavy Lifting
Supabase automatically generates REST and GraphQL APIs based on your database schema. Once you create the table, endpoints become immediately available:
-
GET /rest/v1/posts
- Retrieve all posts -
POST /rest/v1/posts
- Create a new post -
PATCH /rest/v1/posts?id=eq.{id}
- Update a specific post -
DELETE /rest/v1/posts?id=eq.{id}
- Delete a specific post
Configuring Row-Level Security (RLS) for Better Control
Enable Row-Level Security to ensure users can only access their own data:
-- Enable RLS on the posts table
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- Policy for users to read their own posts
CREATE POLICY "Users can read own posts" ON posts
FOR SELECT USING (auth.uid() = author_id);
-- Policy for users to insert their own posts
CREATE POLICY "Users can insert own posts" ON posts
FOR INSERT WITH CHECK (auth.uid() = author_id);
-- Policy for users to update their own posts
CREATE POLICY "Users can update own posts" ON posts
FOR UPDATE USING (auth.uid() = author_id);
-- Policy for users to delete their own posts
CREATE POLICY "Users can delete own posts" ON posts
FOR DELETE USING (auth.uid() = author_id);
Building the User Interface
Planning the App Layout and Navigation
Create a clean, responsive layout with navigation. Design your component structure:
components/
├── Layout.tsx
├── Navigation.tsx
├── PostCard.tsx
├── PostForm.tsx
└── LoadingSpinner.tsx
Creating Pages with Next.js File-Based Routing
Organize your pages using the App Router structure:
app/
├── page.tsx (home page)
├── login/
│ └── page.tsx
├── dashboard/
│ └── page.tsx
├── posts/
│ ├── page.tsx
│ ├── new/
│ │ └── page.tsx
│ └── [id]/
│ └── edit/
│ └── page.tsx
└── layout.tsx
Adding Tailwind CSS for Styling
Tailwind CSS was included during project setup. Create a consistent design system with utility classes:
// components/Button.tsx
interface ButtonProps {
children: React.ReactNode
variant?: 'primary' | 'secondary' | 'danger'
onClick?: () => void
disabled?: boolean
}
export function Button({ children, variant = 'primary', onClick, disabled }: ButtonProps) {
const baseClasses = 'px-4 py-2 rounded-md font-medium transition-colors'
const variantClasses = {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
danger: 'bg-red-600 text-white hover:bg-red-700'
}
return (
<button
className={`${baseClasses} ${variantClasses[variant]} ${disabled ? 'opacity-50 cursor-not-allowed' : ''}`}
onClick={onClick}
disabled={disabled}
>
{children}
</button>
)
}
User Authentication with Supabase
Enabling Email/Password Authentication
In your Supabase dashboard, navigate to Authentication > Settings and enable email authentication. Configure your site URL and redirect URLs for production deployment.
Creating Signup and Login Forms
Create authentication forms with proper validation:
// components/AuthForm.tsx
'use client'
import { useState } from 'react'
import { supabase } from '@/lib/supabase'
interface AuthFormProps {
mode: 'login' | 'signup'
}
export function AuthForm({ mode }: AuthFormProps) {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
setError(null)
const { error } = mode === 'login'
? await supabase.auth.signInWithPassword({ email, password })
: await supabase.auth.signUp({ email, password })
if (error) {
setError(error.message)
}
setLoading(false)
}
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
Email
</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
Password
</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
/>
</div>
{error && (
<div className="text-red-600 text-sm">{error}</div>
)}
<button
type="submit"
disabled={loading}
className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50"
>
{loading ? 'Loading...' : mode === 'login' ? 'Sign In' : 'Sign Up'}
</button>
</form>
)
}
Managing Sessions and Protecting Routes
Create a custom hook for authentication state management:
// hooks/useAuth.ts
'use client'
import { useEffect, useState } from 'react'
import { User } from '@supabase/supabase-js'
import { supabase } from '@/lib/supabase'
export function useAuth() {
const [user, setUser] = useState<User | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
// Get initial session
supabase.auth.getSession().then(({ data: { session } }) => {
setUser(session?.user ?? null)
setLoading(false)
})
// Listen for auth changes
const { data: { subscription } } = supabase.auth.onAuthStateChange((event, session) => {
setUser(session?.user ?? null)
setLoading(false)
})
return () => subscription.unsubscribe()
}, [])
return { user, loading }
}
Implementing Create Functionality
Creating a Form to Add New Records
Build a reusable form component for creating posts:
// components/PostForm.tsx
'use client'
import { useState } from 'react'
import { supabase } from '@/lib/supabase'
import { useAuth } from '@/hooks/useAuth'
interface PostFormData {
title: string
content: string
}
export function PostForm({ onSuccess }: { onSuccess?: () => void }) {
const { user } = useAuth()
const [formData, setFormData] = useState<PostFormData>({
title: '',
content: ''
})
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!user) return
setLoading(true)
setError(null)
const { error } = await supabase
.from('posts')
.insert([
{
title: formData.title,
content: formData.content,
author_id: user.id
}
])
if (error) {
setError(error.message)
} else {
setFormData({ title: '', content: '' })
onSuccess?.()
}
setLoading(false)
}
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="title" className="block text-sm font-medium text-gray-700">
Title
</label>
<input
id="title"
type="text"
value={formData.title}
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
required
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
/>
</div>
<div>
<label htmlFor="content" className="block text-sm font-medium text-gray-700">
Content
</label>
<textarea
id="content"
value={formData.content}
onChange={(e) => setFormData({ ...formData, content: e.target.value })}
rows={4}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
/>
</div>
{error && (
<div className="text-red-600 text-sm">{error}</div>
)}
<button
type="submit"
disabled={loading}
className="bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50"
>
{loading ? 'Creating...' : 'Create Post'}
</button>
</form>
)
}
Submitting Data to Supabase
The form above demonstrates the basic pattern for inserting data into Supabase. Key points:
- Always validate user authentication before operations
- Include proper error handling for network issues
- Reset form state after successful submission
- Use loading states to provide user feedback
Handling Form Validation and Errors
Implement client-side validation with libraries like Zod for type safety:
npm install zod
import { z } from 'zod'
const postSchema = z.object({
title: z.string().min(1, 'Title is required').max(255, 'Title too long'),
content: z.string().optional()
})
// Validate before submission
const validation = postSchema.safeParse(formData)
if (!validation.success) {
setError(validation.error.issues[0].message)
return
}
Reading Data from Supabase
Fetching Data Using Supabase's JavaScript Client
Create a custom hook for fetching posts:
// hooks/usePosts.ts
'use client'
import { useEffect, useState } from 'react'
import { supabase } from '@/lib/supabase'
interface Post {
id: string
title: string
content: string
created_at: string
updated_at: string
}
export function usePosts() {
const [posts, setPosts] = useState<Post[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
fetchPosts()
}, [])
const fetchPosts = async () => {
try {
const { data, error } = await supabase
.from('posts')
.select('*')
.order('created_at', { ascending: false })
if (error) throw error
setPosts(data || [])
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred')
} finally {
setLoading(false)
}
}
return { posts, loading, error, refetch: fetchPosts }
}
Displaying Data with React Components
Create a component to display the list of posts:
// components/PostList.tsx
'use client'
import { usePosts } from '@/hooks/usePosts'
import { LoadingSpinner } from './LoadingSpinner'
export function PostList() {
const { posts, loading, error } = usePosts()
if (loading) return <LoadingSpinner />
if (error) return <div className="text-red-600">Error: {error}</div>
if (posts.length === 0) return <div className="text-gray-500">No posts found</div>
return (
<div className="space-y-4">
{posts.map((post) => (
<div key={post.id} className="bg-white p-6 rounded-lg shadow">
<h3 className="text-lg font-semibold mb-2">{post.title}</h3>
{post.content && (
<p className="text-gray-600 mb-4">{post.content}</p>
)}
<div className="text-sm text-gray-400">
Created: {new Date(post.created_at).toLocaleDateString()}
</div>
</div>
))}
</div>
)
}
Adding Loading States and Error Handling
Implement comprehensive loading and error states throughout your application:
// components/LoadingSpinner.tsx
export function LoadingSpinner() {
return (
<div className="flex justify-center items-center p-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
)
}
// components/ErrorBoundary.tsx
'use client'
import { Component, ReactNode } from 'react'
interface Props {
children: ReactNode
}
interface State {
hasError: boolean
error?: Error
}
export class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = { hasError: false }
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error }
}
render() {
if (this.state.hasError) {
return (
<div className="text-center p-8">
<h2 className="text-xl font-semibold text-red-600 mb-2">Something went wrong</h2>
<p className="text-gray-600">Please refresh the page and try again.</p>
</div>
)
}
return this.props.children
}
}
Conclusion
This guide has walked you through building a complete CRUD application with Supabase and Next.js. You've learned how to implement authentication, manage data operations, add real-time features, and deploy to production. The combination of these technologies provides a powerful foundation for building modern web applications that can scale with your needs.
Continue exploring the vast capabilities of both platforms to build even more sophisticated applications. The skills you've developed here will serve as a solid foundation for your future web development projects.
Top comments (0)