A wave of innovation is sweeping through the food tech space, driven by the rise of serverless platforms and modern frontend frameworks. Whether you’re launching a meal-planning app, a collaborative recipe book, or a restaurant review site, developers now have powerful tools at their fingertips. In this guide, we'll explore how to build a full-stack food app using Supabase and Next.js—a dynamic duo that accelerates development without sacrificing flexibility. We'll cover authentication, real-time data sync, and file storage, giving you a solid foundation for your own supabase food app.
Why Supabase and Next.js for Food Tech?
Food apps demand more than a beautiful UI—they need user accounts, fast data updates (think live order status, collaborative menus), and the ability to handle images or documents. Achieving all of that, securely and at scale, used to be a tall order.
- Supabase: An open-source Firebase alternative that provides a hosted Postgres database, real-time subscriptions, authentication, and storage out of the box.
- Next.js: A React framework that supports hybrid static/server rendering, API routes, and optimized image handling—perfect for SEO and user experience in food apps.
By combining these, you can move from prototype to production faster, focusing on features that matter instead of boilerplate.
Setting Up: Project Bootstrapping
Let’s get started with a basic setup for a full stack food app.
1. Create a New Next.js App
npx create-next-app@latest supabase-food-app
cd supabase-food-app
npm install @supabase/supabase-js
2. Set Up Supabase Project
- Go to Supabase and create a new project.
- Note your Project URL and Anon Key from the API settings.
3. Initialize Supabase Client
Create a new file lib/supabaseClient.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);
Add these env variables to your .env.local:
NEXT_PUBLIC_SUPABASE_URL=your-supabase-url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
Authentication: User Sign Up & Sign In
Let’s enable users to register and log in—crucial for personalizing dietary preferences, saving favorites, or managing orders.
Basic Auth Flow
Supabase handles the backend; on the frontend, use React hooks for the UI.
// components/AuthForm.tsx
import { useState } from 'react';
import { supabase } from '../lib/supabaseClient';
export default function AuthForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleSignUp = async () => {
setLoading(true);
const { error } = await supabase.auth.signUp({ email, password });
setError(error?.message || null);
setLoading(false);
};
const handleSignIn = async () => {
setLoading(true);
const { error } = await supabase.auth.signInWithPassword({ email, password });
setError(error?.message || null);
setLoading(false);
};
return (
<div>
<input
type="email"
placeholder="Email"
onChange={e => setEmail(e.target.value)}
value={email}
/>
<input
type="password"
placeholder="Password"
onChange={e => setPassword(e.target.value)}
value={password}
/>
<button disabled={loading} onClick={handleSignUp}>Sign Up</button>
<button disabled={loading} onClick={handleSignIn}>Sign In</button>
{error && <p style={{ color: 'red' }}>{error}</p>}
</div>
);
}
You can plug this component anywhere in your Next.js pages.
Protecting Routes
To restrict access, check for a valid user session:
import { useEffect, useState } from 'react';
import { supabase } from '../lib/supabaseClient';
export function useUser() {
const [user, setUser] = useState(null);
useEffect(() => {
const session = supabase.auth.getSession();
setUser(session?.user ?? null);
const { data: authListener } = supabase.auth.onAuthStateChange(
(_event, session) => {
setUser(session?.user ?? null);
}
);
return () => {
authListener.subscription.unsubscribe();
};
}, []);
return user;
}
Now you can conditionally render content based on authentication status.
Real-Time Data: Live Menus and Orders
Food tech apps shine when updates are instantaneous. With Supabase, you can subscribe to changes in your database—ideal for collaborative menus or live order dashboards.
Setting Up Tables
In the Supabase dashboard, create a dishes table:
-
id(uuid, primary key, defaultgen_random_uuid()) -
name(text) -
description(text) -
price(numeric) -
image_url(text, optional) -
created_by(uuid, foreign key toauth.users)
Reading and Subscribing to Menu Updates
import { useEffect, useState } from 'react';
import { supabase } from '../lib/supabaseClient';
export function useDishes() {
const [dishes, setDishes] = useState<any[]>([]);
useEffect(() => {
// Initial fetch
supabase.from('dishes').select('*').then(({ data }) => setDishes(data || []));
// Real-time subscription
const subscription = supabase
.channel('public:dishes')
.on(
'postgres_changes',
{ event: '*', schema: 'public', table: 'dishes' },
payload => {
setDishes(prev => {
switch (payload.eventType) {
case 'INSERT':
return [...prev, payload.new];
case 'UPDATE':
return prev.map(d => (d.id === payload.new.id ? payload.new : d));
case 'DELETE':
return prev.filter(d => d.id !== payload.old.id);
default:
return prev;
}
});
}
)
.subscribe();
return () => {
supabase.removeChannel(subscription);
};
}, []);
return dishes;
}
Use this hook in your menu components to reflect changes instantly across clients.
Uploading Images: Food Photos and Storage
No food app is complete without mouthwatering images. Supabase provides object storage similar to AWS S3.
Enable Storage
Create a new storage bucket (e.g., dish-images) in your Supabase dashboard.
Handle File Uploads
import { supabase } from '../lib/supabaseClient';
export async function uploadDishImage(file: File): Promise<string | null> {
const fileExt = file.name.split('.').pop();
const filePath = `${Date.now()}.${fileExt}`;
const { error } = await supabase.storage
.from('dish-images')
.upload(filePath, file);
if (error) {
console.error('Upload failed:', error.message);
return null;
}
const { data } = supabase.storage
.from('dish-images')
.getPublicUrl(filePath);
return data?.publicUrl || null;
}
Integrate this logic into your dish creation form, storing the resulting URL in your dishes table.
Putting It All Together: A Mini Feature Example
Here’s how you might combine these building blocks for a core workflow—adding a new dish:
import { useState } from 'react';
import { supabase } from '../lib/supabaseClient';
import { uploadDishImage } from '../lib/uploadDishImage';
import { useUser } from '../hooks/useUser';
export default function AddDishForm() {
const user = useUser();
const [name, setName] = useState('');
const [desc, setDesc] = useState('');
const [price, setPrice] = useState('');
const [image, setImage] = useState<File | null>(null);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!user) return;
let imageUrl = null;
if (image) {
imageUrl = await uploadDishImage(image);
}
const { error } = await supabase.from('dishes').insert([
{
name,
description: desc,
price: parseFloat(price),
image_url: imageUrl,
created_by: user.id,
},
]);
if (error) alert(error.message);
else {
setName('');
setDesc('');
setPrice('');
setImage(null);
}
};
return (
<form onSubmit={handleSubmit}>
<input placeholder="Dish Name" value={name} onChange={e => setName(e.target.value)} />
<input placeholder="Description" value={desc} onChange={e => setDesc(e.target.value)} />
<input placeholder="Price" value={price} type="number" onChange={e => setPrice(e.target.value)} />
<input type="file" accept="image/*" onChange={e => setImage(e.target.files?.[0] || null)} />
<button type="submit" disabled={!user}>Add Dish</button>
</form>
);
}
Expanding Your Full Stack Food App
What’s great about this foundation is its extensibility. You can:
- Add nutrition info columns or tables for each dish
- Build order management with real-time status updates
- Integrate AI-powered menu analysis using APIs or platforms like LeanDine, Foodvisor, or Edamam for smarter recommendations
- Customize access control with Supabase Row Level Security, ensuring users only see their own data
Key Takeaways
Building a full stack food app with Supabase and Next.js unlocks a modern, scalable approach to food tech. You get:
- Effortless authentication and user management
- Real-time data sync for collaborative or live experiences
- Secure, scalable image and file storage
- A productive development experience with TypeScript and React
By mastering these techniques, you’ll be well-equipped to prototype, launch, and scale the next big thing in food tech. Whether you’re making a supabase food app for fun or as a commercial venture, this modern stack keeps you focused on innovation instead of infrastructure.
Top comments (0)