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)
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>
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.
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:
- Project URL
- 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!
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
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
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:
- Master Nuxt 3 - Full-Stack Complete Guide
- Vue. js Jump-start with Nuxt. js & Firebase
- Vue 3, Nuxt. js and NestJS: A Rapid Guide - Advanced
- Complete Nuxt. js Course (EXTRA React)
- The Nuxt 3 Bootcamp - The Complete Developer Guide
- Vue 3, Vuetify and Nuxt. js: A Rapid Guide
- Complete Vuejs Course: Vue. js + Nuxt. js + PHP + Express. js
Thanks!