DEV Community

albert nahas
albert nahas

Posted on • Originally published at leandine.hashnode.dev

Building a Full-Stack Food App with Supabase and Next.js

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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

Add these env variables to your .env.local:

NEXT_PUBLIC_SUPABASE_URL=your-supabase-url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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, default gen_random_uuid())
  • name (text)
  • description (text)
  • price (numeric)
  • image_url (text, optional)
  • created_by (uuid, foreign key to auth.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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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)