Build a Full-Stack App with Nuxt 3, Supabase, and Tailwind CSS

by Didin J. on Jun 05, 2025 Build a Full-Stack App with Nuxt 3, Supabase, and Tailwind CSS

Build a full-stack app using Nuxt 3, Supabase, and Tailwind CSS with full CRUD, UI polish, and easy deployment to Vercel.

In today’s modern web development landscape, building full-stack applications doesn’t have to be complex or overwhelming. Thanks to powerful frameworks and platforms like Nuxt 3, Supabase, and Tailwind CSS, developers can now create scalable, full-featured apps with minimal configuration and maximum productivity. This tutorial guides you through creating a full-stack application from scratch using three key tools: Nuxt 3 for the frontend framework, Supabase for the backend-as-a-service, and Tailwind CSS for styling.

By the end of this guide, you’ll have a fully functional app (e.g., a task manager) that performs CRUD operations, looks great with utility-first styling, and connects seamlessly to a real-time database. Whether you're building a side project or looking to adopt modern tools for production, this stack provides the perfect balance of simplicity and power.


Step 1: Create the Nuxt 3 Project

Task:

Initialize a new Nuxt 3 project.

Command:

npx nuxi init nuxt3-supabase-app
cd nuxt3-supabase-app
npm install
npm run dev

This will:

  • Scaffold a fresh Nuxt 3 app
  • Install dependencies
  • Start the development server (usually on http://localhost:3000)

Build a Full-Stack App with Nuxt 3, Supabase, and Tailwind CSS - nuxt3-welcome


Step 2: Install Tailwind CSS in Nuxt 3

1. Install Tailwind and dependencies

Run this command inside your Nuxt project folder:

npm install -D tailwindcss @tailwindcss/cli
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss-cli init -p

This will generate two config files:

  • tailwind.config.js
  • postcss.config.js

2. Configure Tailwind to scan Nuxt files

Open tailwind.config.js and update the content section:

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    './components/**/*.{vue,js}',
    './layouts/**/*.vue',
    './pages/**/*.vue',
    './app.vue',
    './nuxt.config.ts',
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

3. Create the Tailwind CSS file

Create a new file: assets/css/tailwind.css

@tailwind base;
@tailwind components;
@tailwind utilities;

4. Register Tailwind in Nuxt config

Open nuxt.config.ts and add:

export default defineNuxtConfig({
  compatibilityDate: '2025-05-15',
  devtools: { enabled: true },
  css: ['@/assets/css/tailwind.css']
})

5. Run the project

npm run dev

To confirm Tailwind is working, edit app.vue like this:

<template>
  <div class="p-6 text-center">
    <h1 class="text-3xl font-bold text-blue-600">Nuxt 3 + Tailwind CSS is working! 🚀</h1>
  </div>
</template>

Build a Full-Stack App with Nuxt 3, Supabase, and Tailwind CSS - nuxt3-tailwindcss


Step 3: Set Up Supabase and Connect It to Nuxt 3

We'll now:

1. Set up a Supabase project

2. Create a table (e.g., tasks)

3. Install the Supabase JS client

4. Connect Supabase to Nuxt

1. Create a Supabase Project

1. Go to https://supabase.com/

2. Log in and click "New project"

3. Choose:

  •    Project name
  •    * Password
  •    * Region

4. Click Create new project

Once it’s ready, go to the "Table Editor" tab.

2. Create a Table: tasks

Click "New Table" and enter:

Column Name Type Notes
id UUID Primary Key (default)
title Text Required
is_done Boolean Default: false
created_at Timestamp Default: now()

Click "Save" when done.

Build a Full-Stack App with Nuxt 3, Supabase, and Tailwind CSS - supabase table

3. Enable Public Access with RLS Policy

By default, Supabase enables RLS (Row-Level Security), which blocks insert/select unless you define rules.

1. Go to your Supabase dashboard:

2. Navigate to Table Editor → tasks table

3. Click the "RLS" tab

    If it says "RLS is enabled", click "Enable Policies" (if not already)

4. Add a new policy:

    ✅ Allow read access:

  • Name: Allow read
  • Action: SELECT
  • Using Expression: true

    ✅ Allow insert access:

  • Name: Allow insert
  • Action: INSERT
  • With Expression: true

    ✅ You can use the “Quickstart” template → Full Access to apply all at once.

4. Get Supabase Project URL and API Key

Go to Settings → API in your project, and note:

  1. Project URL
  2. anon public API key

We’ll use these next.

5. Install the Supabase client in your Nuxt app

In your Nuxt project:

npm install @supabase/supabase-js

6. Add the Supabase client setup to your app

Create a new file: composables/useSupabase.ts

import { createClient } from '@supabase/supabase-js'

const supabaseUrl = 'https://your-project-url.supabase.co'
const supabaseKey = 'your-anon-public-key'

export const supabase = createClient(supabaseUrl, supabaseKey)

🔒 Replace the supabaseUrl and supabaseKey with your actual credentials from Step 3.


Step 4: Fetch and Display Tasks from Supabase

We'll:

  • Create a /tasks page
  • Fetch tasks using our Supabase client
  • Display them with basic Tailwind styling

1. Create pages/tasks.vue

Create this file: pages/tasks.vue

<template>
  <div class="p-6 max-w-xl mx-auto">
    <h1 class="text-2xl font-bold mb-4">📋 Tasks</h1>

    <div v-if="loading" class="text-gray-500">Loading tasks...</div>
    <ul v-else class="space-y-2">
      <li v-for="task in tasks" :key="task.id" class="p-4 bg-white rounded shadow">
        <div class="flex justify-between items-center">
          <span :class="{ 'line-through text-gray-400': task.is_done }">
            {{ task.title }}
          </span>
          <span class="text-sm text-gray-500">
            {{ new Date(task.created_at).toLocaleDateString() }}
          </span>
        </div>
      </li>
    </ul>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { supabase } from '@/composables/useSupabase'

const tasks = ref<any[]>([])
const loading = ref(true)

const fetchTasks = async () => {
  const { data, error } = await supabase
    .from('tasks')
    .select('*')
    .order('created_at', { ascending: false })

  if (error) {
    console.error('Error fetching tasks:', error)
  } else {
    tasks.value = data
  }

  loading.value = false
}

onMounted(fetchTasks)
</script>

2. Add a navigation link

To make it easier, you can edit app.vue to include a simple nav:

<template>
  <div class="p-6">
    <nav class="mb-4 space-x-4">
      <NuxtLink to="/" class="text-blue-500 hover:underline">Home</NuxtLink>
      <NuxtLink to="/tasks" class="text-blue-500 hover:underline">Tasks</NuxtLink>
    </nav>

    <NuxtPage />
  </div>
</template>

3. Visit the page

Go to:

http://localhost:3000/tasks

You should see your task list styled with Tailwind!

Build a Full-Stack App with Nuxt 3, Supabase, and Tailwind CSS - tasks list


Step 5: Add a Form to Create Tasks

We’ll update pages/tasks.vue to include a form above the task list.

1. Update <template> to add the form

Replace the template section with:

<template>
  <div class="p-6 max-w-xl mx-auto">
    <h1 class="text-2xl font-bold mb-4">📋 Tasks</h1>

    <!-- Add Task Form -->
    <form @submit.prevent="addTask" class="flex mb-6 space-x-2">
      <input
        v-model="newTask"
        type="text"
        placeholder="New task"
        class="flex-1 border rounded px-3 py-2"
        required
      />
      <button
        type="submit"
        class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
      >
        Add
      </button>
    </form>

    <!-- Loading or Task List -->
    <div v-if="loading" class="text-gray-500">Loading tasks...</div>
    <ul v-else class="space-y-2">
      <li
        v-for="task in tasks"
        :key="task.id"
        class="p-4 bg-white rounded shadow"
      >
        <div class="flex justify-between items-center">
          <span :class="{ 'line-through text-gray-400': task.is_done }">
            {{ task.title }}
          </span>
          <span class="text-sm text-gray-500">
            {{ new Date(task.created_at).toLocaleDateString() }}
          </span>
        </div>
      </li>
    </ul>
  </div>
</template>

2. Update <script setup> with form logic

Add to the existing script:

const newTask = ref('')

const addTask = async () => {
  if (!newTask.value.trim()) return

  const { data, error } = await supabase
    .from('tasks')
    .insert([{ title: newTask.value, is_done: false }])
    .select()

  if (error) {
    console.error('Error adding task:', error)
    return
  }

  tasks.value.unshift(...data)
  newTask.value = ''
}

3. Test it

  • Go to /tasks
  • Enter a new task and click Add
  • You should see it appear instantly at the top of the list

Build a Full-Stack App with Nuxt 3, Supabase, and Tailwind CSS - add task


Step 6: Toggle Task Completion

We’ll:

1. Add a checkbox to each task

2. Update the is_done field in Supabase when toggled

1. Update the <li> in the task list

In pages/tasks.vue, replace the inside of your v-for block with this:

<li
  v-for="task in tasks"
  :key="task.id"
  class="p-4 bg-white rounded shadow flex justify-between items-center"
>
  <label class="flex items-center space-x-3">
    <input
      type="checkbox"
      v-model="task.is_done"
      @change="toggleDone(task)"
      class="h-4 w-4"
    />
    <span :class="{ 'line-through text-gray-400': task.is_done }">
      {{ task.title }}
    </span>
  </label>
  <span class="text-sm text-gray-500">
    {{ new Date(task.created_at).toLocaleDateString() }}
  </span>
</li>

2. Add toggleDone() method to <script setup>

Below your addTask() function, add this:

const toggleDone = async (task: any) => {
  const { error } = await supabase
    .from('tasks')
    .update({ is_done: task.is_done })
    .eq('id', task.id)

  if (error) {
    console.error('Error updating task:', error)
  }
}

3. Test It

  • Go to /tasks
  • Click a checkbox to mark a task done or undo it
  • Refresh the page — it should stay updated

Build a Full-Stack App with Nuxt 3, Supabase, and Tailwind CSS - task toggle


Step 7: Add a Delete Button to Each Task

We’ll:

  • Add a delete button next to each task
  • Call Supabase to delete it
  • Update the local task list

1. Update Task Item Template

In pages/tasks.vue, update each <li> (inside v-for) like this:

<li
  v-for="task in tasks"
  :key="task.id"
  class="p-4 bg-white rounded shadow flex justify-between items-center"
>
  <div class="flex items-center space-x-3">
    <input
      type="checkbox"
      v-model="task.is_done"
      @change="toggleDone(task)"
      class="h-4 w-4"
    />
    <span :class="{ 'line-through text-gray-400': task.is_done }">
      {{ task.title }}
    </span>
  </div>

  <div class="flex items-center space-x-4">
    <span class="text-sm text-gray-500">
      {{ new Date(task.created_at).toLocaleDateString() }}
    </span>
    <button
      @click="deleteTask(task.id)"
      class="text-red-500 hover:text-red-700 text-sm"
      title="Delete"
    >
      🗑️
    </button>
  </div>
</li>

2. Add deleteTask() in <script setup>

const deleteTask = async (id: string) => {
  const { error } = await supabase
    .from('tasks')
    .delete()
    .eq('id', id)

  if (error) {
    console.error('Error deleting task:', error)
    return
  }

  tasks.value = tasks.value.filter(task => task.id !== id)
}

3. Test It

  • Go to /tasks
  • Click the 🗑️ button — the task should disappear immediately
  • Refresh the page — it should stay gone


Step 8: Polish the UI

We’ll upgrade the design by:

  • Adding hover effects, transitions, and spacing
  • Improving empty and loading states
  • Showing toast-like feedback on actions (optional)

1. Add Transitions + Hover Effects

Enhance each task item by updating this in your <li>:

<li
  v-for="task in tasks"
  :key="task.id"
  class="p-4 bg-white rounded shadow flex justify-between items-center transition hover:shadow-md"
>

Adds a subtle lift effect on hover.

2. Improve the Empty State

After:

<div v-if="loading" class="text-gray-500">Loading tasks...</div>

Add:

<div v-else-if="tasks.length === 0" class="text-gray-400 italic">
  No tasks yet. Add one above!
</div>

This avoids showing an empty list.

3. Optional: Add Toast Messages (with Nuxt UI or custom)

Here’s a simple custom version using a ref:

a. Add to <script setup>:

const message = ref('')

const showMessage = (text: string) => {
  message.value = text
  setTimeout(() => (message.value = ''), 3000)
}

Then update your actions like so:

const addTask = async () => {
  // ...
  showMessage('Task added!')
}
const deleteTask = async (id: string) => {
  // ...
  showMessage('Task deleted')
}

b. Display message in <template>:

<div v-if="message" class="mb-4 p-3 rounded bg-green-100 text-green-700">
  {{ message }}
</div>

You can later replace this with a proper toast plugin if needed.

4. Bonus: Add some Tailwind “softness”

Update body styles in app.vue or a global CSS file (like app.vue wrapper):

<template>
  <div class="min-h-screen bg-gray-50 text-gray-800 font-sans p-4">
    <nav class="mb-6">
      <NuxtLink to="/" class="mr-4 text-blue-600 hover:underline">Home</NuxtLink>
      <NuxtLink to="/tasks" class="text-blue-600 hover:underline">Tasks</NuxtLink>
    </nav>
    <NuxtPage />
  </div>
</template>


Step 9: Deploy Your App to Vercel

Here’s the full process:

1. Push to GitHub (if not already)

If you haven’t yet:

git init
git add .
git commit -m "Initial Nuxt 3 + Supabase app"
gh repo create your-repo-name --public --source=. --remote=origin
git push -u origin main

If you're not using GitHub CLI, just create a repo on GitHub and push manually.

2. Go to vercel.com and log in

  • Log in with GitHub (or GitLab)
  • Click “Add New → Project”
  • Select your repo

3. Configure Build Settings

Vercel will auto-detect Nuxt 3.

But confirm these:

Setting Value
Framework Nuxt.js
Build Command npm run build
Output Dir .output/public

Important: Nuxt 3 uses .output/public, not .nuxt/dist.

4. Add Supabase ENV variables

Under “Environment Variables”, add:

SUPABASE_URL=https://your-project.supabase.co
SUPABASE_ANON_KEY=your-anon-key

These should match what you used in useSupabase.ts.

Then, update useSupabase.ts to load from runtime env:

const supabaseUrl = process.env.SUPABASE_URL!
const supabaseKey = process.env.SUPABASE_ANON_KEY!

In Nuxt 3, prefix with process.env for runtime on Vercel (or use runtime config if you want to go advanced).

5. Deploy!

Click Deploy and wait ~30–60 seconds.

You’ll get a live link like:

https://your-app-name.vercel.app

That’s it — your full-stack Nuxt 3 + Supabase app is now live!


Conclusion

In this guide, we built a full-stack web application using Nuxt 3, Supabase, and Tailwind CSS — a modern stack that makes it fast and enjoyable to ship full-featured apps.

We covered:

  • ⚙️ Setting up Nuxt 3 with Tailwind CSS
  • 🗃️ Creating and connecting to a Supabase backend
  • 📝 Implementing full CRUD (Create, Read, Update, Delete) for tasks
  • 🎨 Polishing the UI with Tailwind’s utility classes
  • 🌍 Deploying the app to Vercel for instant live hosting

This setup offers an excellent foundation for more advanced features like user authentication, real-time updates, and API integration — all of which can be added incrementally thanks to the flexibility of Nuxt and Supabase.

Whether you're building a task manager, journal app, or admin dashboard, this stack is ready to scale with you.

You can get the full source code on our GitHub.

That's just the basics. If you need more deep learning about the Nuxt frameworks, you can take the following cheap course:

Thanks!