Database Integration
Learn how to work with Supabase PostgreSQL database in your NuxtFast application

Lorenzo
Author
NuxtFast uses Supabase's PostgreSQL database to provide a powerful, scalable, and real-time database solution. This guide covers everything you need to know about working with your database.
Overview
Supabase provides:
- PostgreSQL database with full SQL support
- Real-time subscriptions for live data updates
- Row Level Security (RLS) for data protection
- Automatic API generation from your database schema
- Database migrations for version control
Quick Start
1. Database Schema
Create your database tables using Supabase's SQL editor or migrations:
-- Create a profiles table
CREATE TABLE profiles (
id UUID REFERENCES auth.users ON DELETE CASCADE,
username TEXT UNIQUE,
full_name TEXT,
avatar_url TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
PRIMARY KEY (id)
);
-- Enable RLS
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
-- Create policies
CREATE POLICY "Users can view own profile" ON profiles
FOR SELECT USING (auth.uid() = id);
CREATE POLICY "Users can update own profile" ON profiles
FOR UPDATE USING (auth.uid() = id);
2. Basic Database Operations
Here's how to perform CRUD operations with the Supabase client:
<template>
<div>
<div v-if="pending" class="loading loading-spinner"></div>
<div v-else>
<div v-for="profile in profiles" :key="profile.id" class="card">
<div class="card-body">
<h3 class="card-title">{{ profile.full_name }}</h3>
<p>@{{ profile.username }}</p>
</div>
</div>
</div>
</div>
</template>
<script setup>
const supabase = useSupabaseClient()
// Fetch data
const { data: profiles, pending } = await useAsyncData('profiles', async () => {
const { data, error } = await supabase
.from('profiles')
.select('*')
.order('created_at', { ascending: false })
if (error) throw error
return data
})
</script>
Data Fetching
Using useAsyncData
For server-side rendering and automatic caching:
const {
data: posts,
pending,
error,
} = await useAsyncData('posts', async () => {
const { data, error } = await supabase
.from('posts')
.select(
`
*,
profiles(username, avatar_url)
`
)
.order('created_at', { ascending: false })
if (error) throw error
return data
})
Client-side Fetching
For interactive data that changes frequently:
const posts = ref([])
const loading = ref(true)
const fetchPosts = async () => {
loading.value = true
const { data, error } = await supabase.from('posts').select('*').eq('published', true).limit(10)
if (error) {
console.error('Error fetching posts:', error)
} else {
posts.value = data
}
loading.value = false
}
Real-time Subscriptions
Listen for database changes in real-time:
<script setup>
const supabase = useSupabaseClient()
const messages = ref([])
onMounted(() => {
// Subscribe to changes
const channel = supabase
.channel('messages')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'messages',
},
payload => {
messages.value.push(payload.new)
}
)
.on(
'postgres_changes',
{
event: 'DELETE',
schema: 'public',
table: 'messages',
},
payload => {
const index = messages.value.findIndex(m => m.id === payload.old.id)
if (index > -1) {
messages.value.splice(index, 1)
}
}
)
.subscribe()
// Cleanup on unmount
onUnmounted(() => {
supabase.removeChannel(channel)
})
})
</script>
Database Queries
Filtering and Sorting
// Filter by status
const { data } = await supabase.from('tasks').select('*').eq('status', 'completed')
// Multiple filters
const { data } = await supabase
.from('posts')
.select('*')
.eq('published', true)
.gte('created_at', '2024-01-01')
.order('created_at', { ascending: false })
// Text search
const { data } = await supabase.from('posts').select('*').textSearch('title', 'database')
Joins and Relations
// Join with related tables
const { data } = await supabase.from('posts').select(`
*,
profiles!posts_author_id_fkey(
username,
full_name,
avatar_url
),
categories(
name,
slug
)
`)
// Nested relations
const { data } = await supabase.from('users').select(`
*,
posts(
*,
comments(
*,
profiles(username)
)
)
`)
Aggregations
// Count records
const { count } = await supabase
.from('posts')
.select('*', { count: 'exact', head: true })
.eq('published', true)
// Group by and count
const { data } = await supabase.from('posts').select('category, count(*)').group('category')
Data Mutations
Creating Records
const createPost = async postData => {
const { data, error } = await supabase
.from('posts')
.insert({
title: postData.title,
content: postData.content,
author_id: user.value.id,
published: false,
})
.select()
if (error) {
console.error('Error creating post:', error)
return null
}
return data[0]
}
Updating Records
const updatePost = async (id, updates) => {
const { data, error } = await supabase
.from('posts')
.update(updates)
.eq('id', id)
.eq('author_id', user.value.id) // Security check
.select()
if (error) {
console.error('Error updating post:', error)
return null
}
return data[0]
}
Deleting Records
const deletePost = async id => {
const { error } = await supabase
.from('posts')
.delete()
.eq('id', id)
.eq('author_id', user.value.id) // Security check
if (error) {
console.error('Error deleting post:', error)
return false
}
return true
}
Database Functions
Using RPC Functions
Create custom functions in your database:
-- Database function
CREATE OR REPLACE FUNCTION get_user_stats(user_id UUID)
RETURNS JSON AS $$
BEGIN
RETURN (
SELECT json_build_object(
'post_count', COUNT(posts.id),
'comment_count', COUNT(comments.id),
'total_likes', SUM(posts.likes)
)
FROM posts
LEFT JOIN comments ON comments.post_id = posts.id
WHERE posts.author_id = user_id
);
END;
$$ LANGUAGE plpgsql;
Call from your application:
const getUserStats = async userId => {
const { data, error } = await supabase.rpc('get_user_stats', { user_id: userId })
if (error) {
console.error('Error getting user stats:', error)
return null
}
return data
}
Migrations
Creating Migrations
Create migration files to track database changes:
-- migrations/001_initial_schema.sql
CREATE TABLE profiles (
id UUID REFERENCES auth.users ON DELETE CASCADE,
username TEXT UNIQUE,
full_name TEXT,
avatar_url TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
PRIMARY KEY (id)
);
-- Enable RLS
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
-- Policies
CREATE POLICY "Users can view own profile" ON profiles
FOR SELECT USING (auth.uid() = id);
Running Migrations
Use Supabase CLI to manage migrations:
# Create a new migration
supabase migration new create_posts_table
# Apply migrations
supabase db push
# Reset database (development only)
supabase db reset
Performance Optimization
Indexing
Add indexes for frequently queried columns:
-- Index on frequently filtered columns
CREATE INDEX idx_posts_published ON posts(published);
CREATE INDEX idx_posts_author_id ON posts(author_id);
-- Composite index for complex queries
CREATE INDEX idx_posts_published_created ON posts(published, created_at DESC);
-- Text search index
CREATE INDEX idx_posts_search ON posts USING gin(to_tsvector('english', title || ' ' || content));
Query Optimization
// Use select() to limit returned columns
const { data } = await supabase
.from('posts')
.select('id, title, created_at') // Only get what you need
.limit(10)
// Use pagination for large datasets
const { data } = await supabase
.from('posts')
.select('*')
.range(0, 9) // Get first 10 records
.order('created_at', { ascending: false })
Security Best Practices
Row Level Security
Always use RLS to protect your data:
-- Enable RLS on all tables
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- Users can only see published posts or their own drafts
CREATE POLICY "Posts are viewable by everyone" ON posts
FOR SELECT USING (
published = true OR auth.uid() = author_id
);
-- Users can only edit their own posts
CREATE POLICY "Users can edit own posts" ON posts
FOR UPDATE USING (auth.uid() = author_id);
Input Validation
Validate data on both client and server:
const createPost = async postData => {
// Client-side validation
if (!postData.title || postData.title.length < 3) {
throw new Error('Title must be at least 3 characters')
}
const { data, error } = await supabase.from('posts').insert(postData).select()
if (error) throw error
return data[0]
}
Error Handling
const fetchUserPosts = async userId => {
try {
const { data, error } = await supabase.from('posts').select('*').eq('author_id', userId)
if (error) {
// Handle different error types
if (error.code === 'PGRST116') {
console.error('No posts found for user')
return []
}
throw error
}
return data
} catch (error) {
console.error('Unexpected error:', error)
throw error
}
}
Testing
Database Testing
Use Supabase's testing utilities:
// tests/database.test.js
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_ANON_KEY)
describe('Database Operations', () => {
test('should create and fetch posts', async () => {
const { data, error } = await supabase
.from('posts')
.insert({ title: 'Test Post', content: 'Test content' })
.select()
expect(error).toBeNull()
expect(data[0].title).toBe('Test Post')
})
})