Supabase Tutorial: Backend Completo em 15 Minutos
Domine Supabase do zero: authentication robusta, database PostgreSQL, APIs automáticas e real-time subscriptions. Implementação prática para projetos modernos.
Por que isso é importante
Supabase elimina 80% da complexidade backend em projetos web modernos. Enquanto configurar authentication, database e APIs tradicionalmente leva semanas, Supabase entrega tudo isso em minutos com PostgreSQL real e recursos enterprise.
O que é Supabase
Supabase é uma plataforma Backend-as-a-Service (BaaS) que oferece infraestrutura completa para aplicações modernas. Diferente do Firebase, utiliza PostgreSQL como database principal, fornecendo SQL completo e recursos avançados.
ℹ️Recursos Principais
Database PostgreSQL com interface visual, Authentication com múltiplos providers, APIs REST/GraphQL geradas automaticamente, Real-time subscriptions nativas, Storage para arquivos e Edge Functions serverless.
Supabase
Backend moderno com PostgreSQL
Prós
- PostgreSQL completo
- Open source
- SQL nativo
- Real-time built-in
Contras
- Comunidade menor
- Menos integrações
Firebase
BaaS do Google
Prós
- Ecosistema Google
- Comunidade grande
- Muitas integrações
Contras
- NoSQL limitado
- Vendor lock-in
- Pricing imprevisível
Setup Inicial do Projeto
Configuração completa do ambiente Supabase com Next.js e TypeScript para máxima produtividade.
# Instalar CLI global
npm install -g supabase
# Criar projeto Next.js com Supabase
npx create-next-app@latest meu-app --typescript --tailwind --eslint
cd meu-app
# Instalar dependências Supabase
npm install @supabase/supabase-js @supabase/ssr
# Inicializar projeto local
supabase init
# Iniciar containers locais
supabase start
# Verificar status
supabase status
// lib/supabase.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, {
auth: {
persistSession: true,
autoRefreshToken: true,
},
db: {
schema: 'public',
},
})
// Tipos TypeScript automáticos
export type Database = {
public: {
Tables: {
profiles: {
Row: {
id: string
username: string | null
full_name: string | null
avatar_url: string | null
created_at: string
}
Insert: {
id: string
username?: string | null
full_name?: string | null
avatar_url?: string | null
}
Update: {
id?: string
username?: string | null
full_name?: string | null
avatar_url?: string | null
}
}
}
}
}
Database Setup e Schema
Criação de tabelas PostgreSQL com Row Level Security (RLS) para segurança máxima dos dados.
-- Database Schema com RLS
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- Tabela de perfis de usuário
CREATE TABLE profiles (
id UUID REFERENCES auth.users(id) ON DELETE CASCADE PRIMARY KEY,
username TEXT UNIQUE,
full_name TEXT,
avatar_url TEXT,
website TEXT,
bio TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Habilitar Row Level Security
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
-- Policy: usuários veem apenas próprios dados
CREATE POLICY "Usuários podem ver próprio perfil"
ON profiles FOR SELECT
USING (auth.uid() = id);
-- Policy: usuários podem atualizar próprios dados
CREATE POLICY "Usuários podem atualizar próprio perfil"
ON profiles FOR UPDATE
USING (auth.uid() = id);
-- Policy: usuários podem inserir próprios dados
CREATE POLICY "Usuários podem inserir próprio perfil"
ON profiles FOR INSERT
WITH CHECK (auth.uid() = id);
-- Tabela de posts
CREATE TABLE posts (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
title TEXT NOT NULL,
content TEXT,
author_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
published BOOLEAN DEFAULT false,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- RLS para posts
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- Policy: todos podem ver posts publicados
CREATE POLICY "Posts publicados são visíveis"
ON posts FOR SELECT
USING (published = true);
-- Policy: autores podem ver próprios posts
CREATE POLICY "Autores podem ver próprios posts"
ON posts FOR SELECT
USING (auth.uid() = author_id);
-- Trigger para updated_at automático
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ language 'plpgsql';
CREATE TRIGGER update_profiles_updated_at
BEFORE UPDATE ON profiles
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_posts_updated_at
BEFORE UPDATE ON posts
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
Authentication Completa
Implementação robusta de authentication com email, OAuth e proteção de rotas.
// hooks/useAuth.ts
'use client'
import { useState, useEffect, createContext, useContext } from 'react'
import { User, Session } from '@supabase/supabase-js'
import { supabase } from '@/lib/supabase'
interface AuthContextType {
user: User | null
session: Session | null
loading: boolean
signIn: (email: string, password: string) => Promise<any>
signUp: (email: string, password: string) => Promise<any>
signOut: () => Promise<void>
signInWithGitHub: () => Promise<any>
resetPassword: (email: string) => Promise<any>
}
const AuthContext = createContext<AuthContextType | undefined>(undefined)
export function useAuth() {
const context = useContext(AuthContext)
if (context === undefined) {
throw new Error('useAuth deve ser usado dentro de AuthProvider')
}
return context
}
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null)
const [session, setSession] = useState<Session | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
// Verificar sessão inicial
const getSession = async () => {
const { data: { session }, error } = await supabase.auth.getSession()
if (error) {
console.error('Erro ao obter sessão:', error)
} else {
setSession(session)
setUser(session?.user ?? null)
}
setLoading(false)
}
getSession()
// Escutar mudanças de auth
const { data: { subscription } } = supabase.auth.onAuthStateChange(
async (event, session) => {
console.log('Auth event:', event)
setSession(session)
setUser(session?.user ?? null)
setLoading(false)
// Criar perfil na primeira vez
if (event === 'SIGNED_IN' && session?.user) {
await createUserProfile(session.user)
}
}
)
return () => subscription.unsubscribe()
}, [])
// Criar perfil de usuário
const createUserProfile = async (user: User) => {
const { data, error } = await supabase
.from('profiles')
.select('id')
.eq('id', user.id)
.single()
if (error && error.code === 'PGRST116') {
// Usuário não existe, criar perfil
const { error: insertError } = await supabase
.from('profiles')
.insert({
id: user.id,
username: user.email?.split('@')[0],
full_name: user.user_metadata?.full_name || '',
avatar_url: user.user_metadata?.avatar_url || '',
})
if (insertError) {
console.error('Erro ao criar perfil:', insertError)
}
}
}
// Login com email/senha
const signIn = async (email: string, password: string) => {
setLoading(true)
const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
})
setLoading(false)
if (error) throw error
return data
}
// Registro
const signUp = async (email: string, password: string) => {
setLoading(true)
const { data, error } = await supabase.auth.signUp({
email,
password,
options: {
emailRedirectTo: `${window.location.origin}/auth/callback`,
},
})
setLoading(false)
if (error) throw error
return data
}
// Login com GitHub
const signInWithGitHub = async () => {
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: `${window.location.origin}/auth/callback`,
}
})
if (error) throw error
return data
}
// Reset de senha
const resetPassword = async (email: string) => {
const { data, error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: `${window.location.origin}/auth/reset-password`,
})
if (error) throw error
return data
}
// Logout
const signOut = async () => {
setLoading(true)
const { error } = await supabase.auth.signOut()
setLoading(false)
if (error) throw error
}
const value = {
user,
session,
loading,
signIn,
signUp,
signOut,
signInWithGitHub,
resetPassword,
}
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
)
}
⚠️Proteção de Rotas
Implemente middleware para proteger rotas administrativas e garantir que apenas usuários autenticados acessem recursos restritos.
// middleware.ts
import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export async function middleware(req: NextRequest) {
const res = NextResponse.next()
const supabase = createMiddlewareClient({ req, res })
// Verificar sessão
const {
data: { session },
} = await supabase.auth.getSession()
// Rotas protegidas
const protectedPaths = ['/dashboard', '/profile', '/admin']
const isProtectedPath = protectedPaths.some(path =>
req.nextUrl.pathname.startsWith(path)
)
// Redirecionar se não autenticado
if (isProtectedPath && !session) {
const redirectUrl = req.nextUrl.clone()
redirectUrl.pathname = '/auth/login'
redirectUrl.searchParams.set('redirectedFrom', req.nextUrl.pathname)
return NextResponse.redirect(redirectUrl)
}
// Redirecionar admin se não for admin
if (req.nextUrl.pathname.startsWith('/admin') && session) {
const { data: profile } = await supabase
.from('profiles')
.select('role')
.eq('id', session.user.id)
.single()
if (profile?.role !== 'admin') {
return NextResponse.redirect(new URL('/dashboard', req.url))
}
}
return res
}
export const config = {
matcher: ['/dashboard/:path*', '/profile/:path*', '/admin/:path*']
}
Real-time Subscriptions
Implementação de subscriptions em tempo real para atualizações instantâneas da interface.
// hooks/useRealtime.ts
'use client'
import { useEffect, useState } from 'react'
import { supabase } from '@/lib/supabase'
import { Database } from '@/lib/supabase'
type Profile = Database['public']['Tables']['profiles']['Row']
type Post = Database['public']['Tables']['posts']['Row']
export function useRealtimeProfiles() {
const [profiles, setProfiles] = useState<Profile[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
// Buscar dados iniciais
const fetchProfiles = async () => {
try {
const { data, error } = await supabase
.from('profiles')
.select('*')
.order('created_at', { ascending: false })
if (error) throw error
setProfiles(data || [])
} catch (error) {
console.error('Erro ao buscar perfis:', error)
} finally {
setLoading(false)
}
}
fetchProfiles()
// Subscription para mudanças em tempo real
const subscription = supabase
.channel('profiles-realtime')
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'profiles'
},
(payload) => {
console.log('Mudança detectada:', payload)
switch (payload.eventType) {
case 'INSERT':
setProfiles(prev => [payload.new as Profile, ...prev])
break
case 'UPDATE':
setProfiles(prev =>
prev.map(profile =>
profile.id === payload.new.id
? { ...profile, ...payload.new as Profile }
: profile
)
)
break
case 'DELETE':
setProfiles(prev =>
prev.filter(profile => profile.id !== payload.old.id)
)
break
}
}
)
.subscribe((status) => {
console.log('Subscription status:', status)
})
return () => {
subscription.unsubscribe()
}
}, [])
return { profiles, loading }
}
// Hook para posts em tempo real
export function useRealtimePosts() {
const [posts, setPosts] = useState<Post[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
const fetchPosts = async () => {
try {
const { data, error } = await supabase
.from('posts')
.select(`
*,
author:profiles(username, avatar_url)
`)
.eq('published', true)
.order('created_at', { ascending: false })
if (error) throw error
setPosts(data || [])
} catch (error) {
console.error('Erro ao buscar posts:', error)
} finally {
setLoading(false)
}
}
fetchPosts()
// Subscription com filtro para posts publicados
const subscription = supabase
.channel('posts-realtime')
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'posts',
filter: 'published=eq.true'
},
(payload) => {
switch (payload.eventType) {
case 'INSERT':
// Buscar dados completos com join
fetchPostWithAuthor(payload.new.id)
break
case 'UPDATE':
if (payload.new.published) {
fetchPostWithAuthor(payload.new.id)
} else {
setPosts(prev =>
prev.filter(post => post.id !== payload.new.id)
)
}
break
case 'DELETE':
setPosts(prev =>
prev.filter(post => post.id !== payload.old.id)
)
break
}
}
)
.subscribe()
const fetchPostWithAuthor = async (postId: string) => {
const { data, error } = await supabase
.from('posts')
.select(`
*,
author:profiles(username, avatar_url)
`)
.eq('id', postId)
.single()
if (data && !error) {
setPosts(prev => {
const exists = prev.find(p => p.id === postId)
if (exists) {
return prev.map(p => p.id === postId ? data : p)
} else {
return [data, ...prev]
}
})
}
}
return () => {
subscription.unsubscribe()
}
}, [])
return { posts, loading }
}
Storage e Upload de Arquivos
Sistema completo de upload, gerenciamento e otimização de arquivos com Supabase Storage.
// utils/storage.ts
import { supabase } from '@/lib/supabase'
export interface UploadResult {
filePath: string
publicUrl: string
fileSize: number
}
// Upload de avatar com validação
export async function uploadAvatar(file: File, userId: string): Promise<UploadResult> {
// Validações
const maxSize = 5 * 1024 * 1024 // 5MB
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp']
if (file.size > maxSize) {
throw new Error('Arquivo muito grande. Máximo 5MB.')
}
if (!allowedTypes.includes(file.type)) {
throw new Error('Tipo de arquivo não permitido. Use JPG, PNG ou WebP.')
}
// Gerar nome único
const fileExt = file.name.split('.').pop()
const fileName = `${userId}-${Date.now()}.${fileExt}`
const filePath = `avatars/${fileName}`
// Upload com metadata
const { data, error } = await supabase.storage
.from('avatars')
.upload(filePath, file, {
cacheControl: '3600',
upsert: false,
metadata: {
userId,
originalName: file.name,
uploadedAt: new Date().toISOString(),
}
})
if (error) throw error
// Obter URL pública
const { data: { publicUrl } } = supabase.storage
.from('avatars')
.getPublicUrl(filePath)
return {
filePath: data.path,
publicUrl,
fileSize: file.size,
}
}
// Upload com resize automático
export async function uploadWithResize(
file: File,
bucket: string,
maxWidth: number = 800,
quality: number = 0.8
): Promise<UploadResult> {
// Criar canvas para resize
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
const img = new Image()
return new Promise((resolve, reject) => {
img.onload = async () => {
// Calcular novas dimensões
const ratio = Math.min(maxWidth / img.width, maxWidth / img.height)
canvas.width = img.width * ratio
canvas.height = img.height * ratio
// Desenhar imagem redimensionada
ctx?.drawImage(img, 0, 0, canvas.width, canvas.height)
// Converter para blob
canvas.toBlob(async (blob) => {
if (!blob) {
reject(new Error('Falha ao processar imagem'))
return
}
try {
// Upload da imagem processada
const fileExt = file.name.split('.').pop()
const fileName = `processed-${Date.now()}.${fileExt}`
const filePath = `${bucket}/${fileName}`
const { data, error } = await supabase.storage
.from(bucket)
.upload(filePath, blob)
if (error) throw error
const { data: { publicUrl } } = supabase.storage
.from(bucket)
.getPublicUrl(filePath)
resolve({
filePath: data.path,
publicUrl,
fileSize: blob.size,
})
} catch (error) {
reject(error)
}
}, file.type, quality)
}
img.onerror = () => reject(new Error('Falha ao carregar imagem'))
img.src = URL.createObjectURL(file)
})
}
// Gerenciar arquivos do usuário
export async function getUserFiles(userId: string, bucket: string) {
const { data, error } = await supabase.storage
.from(bucket)
.list('', {
limit: 100,
offset: 0,
search: userId,
})
if (error) throw error
return data?.map(file => ({
...file,
publicUrl: supabase.storage.from(bucket).getPublicUrl(file.name).data.publicUrl,
})) || []
}
// Deletar arquivo
export async function deleteFile(bucket: string, filePath: string) {
const { error } = await supabase.storage
.from(bucket)
.remove([filePath])
if (error) throw error
}
// Listar buckets disponíveis
export async function listBuckets() {
const { data, error } = await supabase.storage.listBuckets()
if (error) throw error
return data
}
// Criar bucket programaticamente
export async function createBucket(
name: string,
isPublic: boolean = true,
allowedMimeTypes?: string[]
) {
const { data, error } = await supabase.storage.createBucket(name, {
public: isPublic,
allowedMimeTypes,
fileSizeLimit: 10 * 1024 * 1024, // 10MB
})
if (error) throw error
return data
}
Edge Functions Serverless
Funções serverless para lógica backend customizada, processamento de dados e integrações externas.
// supabase/functions/send-notification/index.ts
import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
}
interface NotificationRequest {
userId: string
title: string
message: string
type: 'email' | 'push' | 'sms'
metadata?: Record<string, any>
}
serve(async (req) => {
// Handle CORS
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders })
}
try {
// Verificar autenticação
const authHeader = req.headers.get('Authorization')
if (!authHeader) {
throw new Error('Token de autorização necessário')
}
// Criar cliente Supabase com service role
const supabase = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
)
// Verificar usuário
const { data: { user }, error: authError } = await supabase.auth.getUser(
authHeader.replace('Bearer ', '')
)
if (authError || !user) {
throw new Error('Token inválido')
}
// Parse do body
const { userId, title, message, type, metadata }: NotificationRequest =
await req.json()
// Validações
if (!userId || !title || !message || !type) {
throw new Error('Dados obrigatórios: userId, title, message, type')
}
// Buscar dados do usuário
const { data: profile, error: profileError } = await supabase
.from('profiles')
.select('*')
.eq('id', userId)
.single()
if (profileError || !profile) {
throw new Error('Usuário não encontrado')
}
let result: any = {}
// Processar notificação baseado no tipo
switch (type) {
case 'email':
result = await sendEmail(profile, title, message, metadata)
break
case 'push':
result = await sendPushNotification(profile, title, message, metadata)
break
case 'sms':
result = await sendSMS(profile, title, message, metadata)
break
default:
throw new Error('Tipo de notificação inválido')
}
// Salvar log da notificação
const { error: logError } = await supabase
.from('notification_logs')
.insert({
user_id: userId,
type,
title,
message,
status: 'sent',
metadata: {
...metadata,
result,
sent_at: new Date().toISOString(),
}
})
if (logError) {
console.error('Erro ao salvar log:', logError)
}
return new Response(
JSON.stringify({
success: true,
message: 'Notificação enviada com sucesso',
result
}),
{
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 200,
},
)
} catch (error) {
console.error('Erro na function:', error)
return new Response(
JSON.stringify({
error: error.message || 'Erro interno do servidor'
}),
{
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 400,
},
)
}
})
// Função para enviar email
async function sendEmail(profile: any, title: string, message: string, metadata?: any) {
const emailData = {
personalizations: [{
to: [{ email: profile.email }],
subject: title,
}],
from: {
email: Deno.env.get('FROM_EMAIL') || 'noreply@seuapp.com',
name: 'Seu App'
},
content: [{
type: 'text/html',
value: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h2 style="color: #333;">${title}</h2>
<p style="color: #666; line-height: 1.6;">${message}</p>
${metadata?.actionUrl ? `
<a href="${metadata.actionUrl}"
style="background: #007bff; color: white; padding: 12px 24px;
text-decoration: none; border-radius: 4px; display: inline-block;">
${metadata.actionText || 'Ver Detalhes'}
</a>
` : ''}
</div>
`
}],
}
const response = await fetch('https://api.sendgrid.com/v3/mail/send', {
method: 'POST',
headers: {
'Authorization': `Bearer ${Deno.env.get('SENDGRID_API_KEY')}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(emailData),
})
if (!response.ok) {
const error = await response.text()
throw new Error(`Falha ao enviar email: ${error}`)
}
return { provider: 'sendgrid', messageId: response.headers.get('x-message-id') }
}
// Função para push notification
async function sendPushNotification(profile: any, title: string, message: string, metadata?: any) {
// Implementar com FCM, OneSignal, etc.
const pushData = {
to: profile.push_token,
title,
body: message,
data: metadata || {},
}
// Exemplo com FCM
const response = await fetch('https://fcm.googleapis.com/fcm/send', {
method: 'POST',
headers: {
'Authorization': `key=${Deno.env.get('FCM_SERVER_KEY')}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(pushData),
})
if (!response.ok) {
throw new Error('Falha ao enviar push notification')
}
const result = await response.json()
return { provider: 'fcm', messageId: result.multicast_id }
}
// Função para SMS
async function sendSMS(profile: any, title: string, message: string, metadata?: any) {
if (!profile.phone) {
throw new Error('Usuário não possui telefone cadastrado')
}
const smsData = {
to: profile.phone,
body: `${title}
${message}`,
}
// Exemplo com Twilio
const response = await fetch(`https://api.twilio.com/2010-04-01/Accounts/${Deno.env.get('TWILIO_ACCOUNT_SID')}/Messages.json`, {
method: 'POST',
headers: {
'Authorization': `Basic ${btoa(`${Deno.env.get('TWILIO_ACCOUNT_SID')}:${Deno.env.get('TWILIO_AUTH_TOKEN')}`)}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
To: profile.phone,
From: Deno.env.get('TWILIO_PHONE_NUMBER') || '',
Body: smsData.body,
}),
})
if (!response.ok) {
throw new Error('Falha ao enviar SMS')
}
const result = await response.json()
return { provider: 'twilio', messageId: result.sid }
}
Performance e Otimizações
Técnicas avançadas para maximizar performance, reduzir latência e otimizar queries.
Connection Pooling
Gerenciar conexões PostgreSQL eficientemente
Query Optimization
Indexes, explain plans e query performance
Caching Strategy
Redis, CDN e cache de aplicação
Real-time Scaling
Gerenciar subscriptions em alta escala
// utils/performance.ts
import { supabase } from '@/lib/supabase'
// Pagination eficiente com cursor
export async function getPaginatedData<T>(
table: string,
options: {
cursor?: string
limit?: number
orderBy?: string
ascending?: boolean
select?: string
filters?: Record<string, any>
} = {}
) {
const {
cursor,
limit = 20,
orderBy = 'created_at',
ascending = false,
select = '*',
filters = {}
} = options
let query = supabase
.from(table)
.select(select, { count: 'exact' })
.order(orderBy, { ascending })
.limit(limit)
// Aplicar filtros
Object.entries(filters).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
query = query.eq(key, value)
}
})
// Cursor pagination
if (cursor) {
query = ascending
? query.gt(orderBy, cursor)
: query.lt(orderBy, cursor)
}
const { data, error, count } = await query
if (error) throw error
const nextCursor = data && data.length > 0
? data[data.length - 1][orderBy]
: null
return {
data: data || [],
nextCursor,
hasMore: data ? data.length === limit : false,
total: count || 0,
}
}
// Query com cache Redis
export async function getCachedQuery<T>(
cacheKey: string,
queryFn: () => Promise<T>,
ttlSeconds: number = 300
): Promise<T> {
// Tentar buscar do cache primeiro
try {
const cached = localStorage.getItem(cacheKey)
if (cached) {
const { data, timestamp } = JSON.parse(cached)
const isValid = Date.now() - timestamp < ttlSeconds * 1000
if (isValid) {
return data
}
}
} catch (error) {
console.warn('Erro ao ler cache:', error)
}
// Executar query se não está em cache
const result = await queryFn()
// Salvar no cache
try {
localStorage.setItem(cacheKey, JSON.stringify({
data: result,
timestamp: Date.now(),
}))
} catch (error) {
console.warn('Erro ao salvar cache:', error)
}
return result
}
// Batch operations para múltiplas queries
export async function batchQueries(operations: Promise<any>[]) {
const results = await Promise.allSettled(operations)
const successful = results
.filter((result): result is PromiseFulfilledResult<any> =>
result.status === 'fulfilled'
)
.map(result => result.value)
const errors = results
.filter((result): result is PromiseRejectedResult =>
result.status === 'rejected'
)
.map(result => result.reason)
return { successful, errors }
}
// Connection singleton otimizada
class SupabaseManager {
private static instance: SupabaseManager
private connectionPool: Map<string, any> = new Map()
static getInstance(): SupabaseManager {
if (!SupabaseManager.instance) {
SupabaseManager.instance = new SupabaseManager()
}
return SupabaseManager.instance
}
getConnection(config?: any) {
const key = JSON.stringify(config || 'default')
if (!this.connectionPool.has(key)) {
const client = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
auth: {
persistSession: true,
autoRefreshToken: true,
},
db: {
schema: 'public',
},
...config,
}
)
this.connectionPool.set(key, client)
}
return this.connectionPool.get(key)
}
clearConnections() {
this.connectionPool.clear()
}
}
export const supabaseManager = SupabaseManager.getInstance()
Deploy e Configuração de Produção
Configuração completa para ambiente de produção com monitoring, backup e segurança.
# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://seu-projeto.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=sua-chave-publica
SUPABASE_SERVICE_ROLE_KEY=sua-chave-privada
# Configurações de produção
DATABASE_URL=postgresql://postgres:[password]@db.[ref].supabase.co:5432/postgres
SUPABASE_JWT_SECRET=seu-jwt-secret
# Integrações externas
SENDGRID_API_KEY=sua-chave-sendgrid
TWILIO_ACCOUNT_SID=seu-sid-twilio
FCM_SERVER_KEY=sua-chave-fcm
// scripts/backup.ts
import { createClient } from '@supabase/supabase-js'
import fs from 'fs/promises'
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
)
async function backupTables() {
const tables = ['profiles', 'posts', 'comments', 'notification_logs']
const backupData: Record<string, any[]> = {}
for (const table of tables) {
console.log(`📦 Fazendo backup da tabela: ${table}`)
const { data, error } = await supabase
.from(table)
.select('*')
if (error) {
console.error(`❌ Erro na tabela ${table}:`, error)
continue
}
backupData[table] = data || []
console.log(`✅ ${table}: ${data?.length || 0} registros`)
}
// Salvar backup com timestamp
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
const filename = `backup-${timestamp}.json`
await fs.writeFile(filename, JSON.stringify(backupData, null, 2))
console.log(`💾 Backup salvo: ${filename}`)
return filename
}
// Executar backup
if (require.main === module) {
backupTables()
.then(filename => console.log(`🎉 Backup concluído: ${filename}`))
.catch(console.error)
}
// pages/api/health.ts
import { NextApiRequest, NextApiResponse } from 'next'
import { supabase } from '@/lib/supabase'
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'GET') {
return res.status(405).json({ error: 'Method not allowed' })
}
try {
const checks = await Promise.allSettled([
// Database health
supabase.from('profiles').select('id').limit(1),
// Auth health
supabase.auth.getSession(),
// Storage health
supabase.storage.listBuckets(),
])
const dbHealth = checks[0].status === 'fulfilled'
const authHealth = checks[1].status === 'fulfilled'
const storageHealth = checks[2].status === 'fulfilled'
const isHealthy = dbHealth && authHealth && storageHealth
const healthData = {
status: isHealthy ? 'healthy' : 'unhealthy',
timestamp: new Date().toISOString(),
services: {
database: dbHealth ? 'ok' : 'error',
auth: authHealth ? 'ok' : 'error',
storage: storageHealth ? 'ok' : 'error',
},
version: process.env.npm_package_version || '1.0.0',
uptime: process.uptime(),
}
res.status(isHealthy ? 200 : 503).json(healthData)
} catch (error) {
res.status(503).json({
status: 'unhealthy',
timestamp: new Date().toISOString(),
error: 'Health check failed',
})
}
}
Migrations e Versionamento de Schema
Sistema robusto de migrations para evolução controlada do database em produção, mantendo integridade e versionamento adequado.
// migrations/20240120_001_create_users_system.sql
-- Migration: Sistema de usuários avançado
-- Autor: DevTeam
-- Data: 2024-01-20
BEGIN;
-- Extensões necessárias
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
-- Enum para roles de usuário
CREATE TYPE user_role AS ENUM ('user', 'moderator', 'admin', 'super_admin');
-- Enum para status de conta
CREATE TYPE account_status AS ENUM ('active', 'inactive', 'suspended', 'pending_verification');
-- Tabela de organizações
CREATE TABLE organizations (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
name TEXT NOT NULL,
slug TEXT UNIQUE NOT NULL,
description TEXT,
logo_url TEXT,
website TEXT,
subscription_plan TEXT DEFAULT 'free' CHECK (subscription_plan IN ('free', 'pro', 'enterprise')),
max_users INTEGER DEFAULT 5,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Índices para organizações
CREATE INDEX idx_organizations_slug ON organizations(slug);
CREATE INDEX idx_organizations_plan ON organizations(subscription_plan);
-- Tabela de perfis expandida
ALTER TABLE profiles ADD COLUMN IF NOT EXISTS organization_id UUID REFERENCES organizations(id);
ALTER TABLE profiles ADD COLUMN IF NOT EXISTS role user_role DEFAULT 'user';
ALTER TABLE profiles ADD COLUMN IF NOT EXISTS status account_status DEFAULT 'pending_verification';
ALTER TABLE profiles ADD COLUMN IF NOT EXISTS last_login_at TIMESTAMP WITH TIME ZONE;
ALTER TABLE profiles ADD COLUMN IF NOT EXISTS email_verified BOOLEAN DEFAULT false;
ALTER TABLE profiles ADD COLUMN IF NOT EXISTS phone TEXT;
ALTER TABLE profiles ADD COLUMN IF NOT EXISTS timezone TEXT DEFAULT 'UTC';
ALTER TABLE profiles ADD COLUMN IF NOT EXISTS preferences JSONB DEFAULT '{}';
-- Índices para profiles
CREATE INDEX idx_profiles_organization ON profiles(organization_id);
CREATE INDEX idx_profiles_role ON profiles(role);
CREATE INDEX idx_profiles_status ON profiles(status);
CREATE INDEX idx_profiles_email_verified ON profiles(email_verified);
-- Tabela de convites
CREATE TABLE invitations (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
email TEXT NOT NULL,
role user_role DEFAULT 'user',
invited_by UUID REFERENCES auth.users(id),
token TEXT UNIQUE NOT NULL DEFAULT encode(gen_random_bytes(32), 'hex'),
expires_at TIMESTAMP WITH TIME ZONE DEFAULT (NOW() + INTERVAL '7 days'),
accepted_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Índices para invitations
CREATE INDEX idx_invitations_token ON invitations(token);
CREATE INDEX idx_invitations_email ON invitations(email);
CREATE INDEX idx_invitations_org ON invitations(organization_id);
-- Tabela de auditoria
CREATE TABLE audit_logs (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
user_id UUID REFERENCES auth.users(id),
organization_id UUID REFERENCES organizations(id),
action TEXT NOT NULL,
resource_type TEXT NOT NULL,
resource_id TEXT,
metadata JSONB DEFAULT '{}',
ip_address INET,
user_agent TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Índices para audit_logs
CREATE INDEX idx_audit_logs_user ON audit_logs(user_id);
CREATE INDEX idx_audit_logs_org ON audit_logs(organization_id);
CREATE INDEX idx_audit_logs_action ON audit_logs(action);
CREATE INDEX idx_audit_logs_created_at ON audit_logs(created_at);
-- RLS Policies
-- Organizations
ALTER TABLE organizations ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Usuários podem ver própria organização"
ON organizations FOR SELECT
USING (
id IN (
SELECT organization_id
FROM profiles
WHERE id = auth.uid()
)
);
CREATE POLICY "Admins podem atualizar organização"
ON organizations FOR UPDATE
USING (
id IN (
SELECT organization_id
FROM profiles
WHERE id = auth.uid()
AND role IN ('admin', 'super_admin')
)
);
-- Invitations
ALTER TABLE invitations ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Admins podem gerenciar convites"
ON invitations FOR ALL
USING (
organization_id IN (
SELECT organization_id
FROM profiles
WHERE id = auth.uid()
AND role IN ('admin', 'super_admin')
)
);
CREATE POLICY "Usuários podem ver próprios convites"
ON invitations FOR SELECT
USING (email = auth.email());
-- Audit Logs
ALTER TABLE audit_logs ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Usuários podem ver próprios logs"
ON audit_logs FOR SELECT
USING (user_id = auth.uid());
CREATE POLICY "Admins podem ver logs da organização"
ON audit_logs FOR SELECT
USING (
organization_id IN (
SELECT organization_id
FROM profiles
WHERE id = auth.uid()
AND role IN ('admin', 'super_admin')
)
);
-- Funções auxiliares
-- Função para atualizar updated_at
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ language 'plpgsql';
-- Triggers para updated_at
CREATE TRIGGER update_organizations_updated_at
BEFORE UPDATE ON organizations
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- Função para criar organização padrão
CREATE OR REPLACE FUNCTION create_default_organization_for_user()
RETURNS TRIGGER AS $$
DECLARE
org_id UUID;
BEGIN
-- Criar organização pessoal
INSERT INTO organizations (name, slug, description)
VALUES (
COALESCE(NEW.full_name, 'Organização Pessoal'),
LOWER(REPLACE(COALESCE(NEW.username, NEW.id::text), ' ', '-')),
'Organização pessoal criada automaticamente'
)
RETURNING id INTO org_id;
-- Atualizar perfil com organização
NEW.organization_id = org_id;
NEW.role = 'admin';
RETURN NEW;
END;
$$ language 'plpgsql';
-- Trigger para criar organização automática
CREATE TRIGGER create_org_for_new_profile
BEFORE INSERT ON profiles
FOR EACH ROW
WHEN (NEW.organization_id IS NULL)
EXECUTE FUNCTION create_default_organization_for_user();
-- Função para log de auditoria
CREATE OR REPLACE FUNCTION log_user_action(
p_action TEXT,
p_resource_type TEXT,
p_resource_id TEXT DEFAULT NULL,
p_metadata JSONB DEFAULT '{}'
)
RETURNS UUID AS $$
DECLARE
log_id UUID;
user_org_id UUID;
BEGIN
-- Buscar organização do usuário
SELECT organization_id INTO user_org_id
FROM profiles
WHERE id = auth.uid();
-- Inserir log
INSERT INTO audit_logs (
user_id,
organization_id,
action,
resource_type,
resource_id,
metadata
)
VALUES (
auth.uid(),
user_org_id,
p_action,
p_resource_type,
p_resource_id,
p_metadata
)
RETURNING id INTO log_id;
RETURN log_id;
END;
$$ language 'plpgsql' SECURITY DEFINER;
-- Seeds de dados iniciais
INSERT INTO organizations (id, name, slug, description, subscription_plan) VALUES
('00000000-0000-0000-0000-000000000001', 'CrazyStack', 'crazystack', 'Organização principal da CrazyStack', 'enterprise');
COMMIT;
ℹ️Migration Management
Use sempre transações (BEGIN/COMMIT) em migrations, documente mudanças e teste em ambiente de desenvolvimento primeiro. Mantenha migrations atômicas e reversíveis.
// scripts/migrate.ts
import { createClient } from '@supabase/supabase-js'
import fs from 'fs/promises'
import path from 'path'
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
)
interface Migration {
id: string
filename: string
sql: string
applied_at?: string
}
// Tabela para controle de migrations
const MIGRATIONS_TABLE = `
CREATE TABLE IF NOT EXISTS _migrations (
id TEXT PRIMARY KEY,
filename TEXT NOT NULL,
applied_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
`
async function initMigrationsTable() {
const { error } = await supabase.rpc('exec_sql', {
sql_query: MIGRATIONS_TABLE
})
if (error) {
console.error('Erro ao criar tabela de migrations:', error)
throw error
}
}
async function getAppliedMigrations(): Promise<string[]> {
const { data, error } = await supabase
.from('_migrations')
.select('id')
.order('applied_at')
if (error) throw error
return data?.map(m => m.id) || []
}
async function getMigrationFiles(): Promise<Migration[]> {
const migrationsDir = path.join(process.cwd(), 'migrations')
const files = await fs.readdir(migrationsDir)
const migrations: Migration[] = []
for (const file of files.filter(f => f.endsWith('.sql'))) {
const id = file.replace('.sql', '')
const filepath = path.join(migrationsDir, file)
const sql = await fs.readFile(filepath, 'utf8')
migrations.push({ id, filename: file, sql })
}
return migrations.sort((a, b) => a.id.localeCompare(b.id))
}
async function runMigration(migration: Migration): Promise<void> {
console.log(`📦 Aplicando migration: ${migration.filename}`)
try {
// Executar SQL da migration
const { error: sqlError } = await supabase.rpc('exec_sql', {
sql_query: migration.sql
})
if (sqlError) throw sqlError
// Registrar migration como aplicada
const { error: recordError } = await supabase
.from('_migrations')
.insert({
id: migration.id,
filename: migration.filename
})
if (recordError) throw recordError
console.log(`✅ Migration aplicada: ${migration.filename}`)
} catch (error) {
console.error(`❌ Erro na migration ${migration.filename}:`, error)
throw error
}
}
async function runMigrations() {
try {
console.log('🚀 Iniciando migrations...')
// Inicializar tabela de controle
await initMigrationsTable()
// Buscar migrations aplicadas e disponíveis
const [appliedMigrations, availableMigrations] = await Promise.all([
getAppliedMigrations(),
getMigrationFiles()
])
// Filtrar migrations pendentes
const pendingMigrations = availableMigrations.filter(
migration => !appliedMigrations.includes(migration.id)
)
if (pendingMigrations.length === 0) {
console.log('✨ Nenhuma migration pendente')
return
}
console.log(`📋 ${pendingMigrations.length} migrations pendentes`)
// Aplicar migrations em ordem
for (const migration of pendingMigrations) {
await runMigration(migration)
}
console.log('🎉 Todas as migrations foram aplicadas com sucesso!')
} catch (error) {
console.error('💥 Erro ao executar migrations:', error)
process.exit(1)
}
}
// Executar se chamado diretamente
if (require.main === module) {
runMigrations()
}
API REST Automática e Customização
Aproveitamento máximo da API REST gerada automaticamente pelo Supabase, com customizações avançadas e endpoints específicos.
// lib/api-client.ts
import { supabase } from './supabase'
import { Database } from './database.types'
type Tables = Database['public']['Tables']
type Profile = Tables['profiles']['Row']
type Post = Tables['posts']['Row']
type Organization = Tables['organizations']['Row']
// Cliente API tipado
export class SupabaseAPI {
// Métodos para Profiles
static async getProfile(userId: string): Promise<Profile | null> {
const { data, error } = await supabase
.from('profiles')
.select(`
*,
organization:organizations(*)
`)
.eq('id', userId)
.single()
if (error) throw new APIError('Erro ao buscar perfil', error)
return data
}
static async updateProfile(
userId: string,
updates: Partial<Profile>
): Promise<Profile> {
const { data, error } = await supabase
.from('profiles')
.update(updates)
.eq('id', userId)
.select()
.single()
if (error) throw new APIError('Erro ao atualizar perfil', error)
// Log da ação
await this.logAction('update', 'profile', userId, { updates })
return data
}
// Métodos para Posts com filtros avançados
static async getPosts(options: {
authorId?: string
organizationId?: string
published?: boolean
search?: string
tags?: string[]
page?: number
limit?: number
sortBy?: 'created_at' | 'updated_at' | 'title'
sortOrder?: 'asc' | 'desc'
} = {}): Promise<{
posts: Post[]
total: number
page: number
totalPages: number
}> {
const {
authorId,
organizationId,
published = true,
search,
tags,
page = 1,
limit = 20,
sortBy = 'created_at',
sortOrder = 'desc'
} = options
let query = supabase
.from('posts')
.select(`
*,
author:profiles!author_id(username, full_name, avatar_url),
organization:organizations!organization_id(name, slug)
`, { count: 'exact' })
// Aplicar filtros
if (authorId) query = query.eq('author_id', authorId)
if (organizationId) query = query.eq('organization_id', organizationId)
if (published !== undefined) query = query.eq('published', published)
// Busca por texto
if (search) {
query = query.or(`title.ilike.%${search}%,content.ilike.%${search}%`)
}
// Filtro por tags (assumindo coluna tags JSONB)
if (tags && tags.length > 0) {
const tagFilters = tags.map(tag => `tags.cs.[""${tag}""]`).join(',')
query = query.or(tagFilters)
}
// Ordenação e paginação
const offset = (page - 1) * limit
query = query
.order(sortBy, { ascending: sortOrder === 'asc' })
.range(offset, offset + limit - 1)
const { data, error, count } = await query
if (error) throw new APIError('Erro ao buscar posts', error)
return {
posts: data || [],
total: count || 0,
page,
totalPages: Math.ceil((count || 0) / limit)
}
}
static async createPost(postData: {
title: string
content: string
published?: boolean
tags?: string[]
metadata?: Record<string, any>
}): Promise<Post> {
const { data: userProfile } = await supabase
.from('profiles')
.select('organization_id')
.eq('id', (await supabase.auth.getUser()).data.user?.id!)
.single()
const { data, error } = await supabase
.from('posts')
.insert({
...postData,
author_id: (await supabase.auth.getUser()).data.user?.id!,
organization_id: userProfile?.organization_id,
})
.select()
.single()
if (error) throw new APIError('Erro ao criar post', error)
await this.logAction('create', 'post', data.id, postData)
return data
}
// Métodos para Organizations
static async getOrganizationMembers(orgId: string): Promise<Profile[]> {
const { data, error } = await supabase
.from('profiles')
.select(`
*,
user:auth.users!id(email, created_at)
`)
.eq('organization_id', orgId)
.order('role', { ascending: false })
.order('created_at', { ascending: true })
if (error) throw new APIError('Erro ao buscar membros', error)
return data || []
}
static async inviteUserToOrganization(
email: string,
role: 'user' | 'moderator' | 'admin' = 'user'
): Promise<void> {
const user = await supabase.auth.getUser()
if (!user.data.user) throw new Error('Usuário não autenticado')
const { data: profile } = await supabase
.from('profiles')
.select('organization_id, role')
.eq('id', user.data.user.id)
.single()
if (!profile || !['admin', 'super_admin'].includes(profile.role!)) {
throw new Error('Permissão insuficiente')
}
const { error } = await supabase
.from('invitations')
.insert({
organization_id: profile.organization_id,
email,
role,
invited_by: user.data.user.id
})
if (error) throw new APIError('Erro ao criar convite', error)
await this.logAction('invite', 'user', email, { role })
}
// Métodos para Analytics
static async getOrganizationStats(orgId: string): Promise<{
totalMembers: number
totalPosts: number
publishedPosts: number
draftPosts: number
recentActivity: any[]
}> {
const [membersResult, postsResult, activityResult] = await Promise.all([
supabase
.from('profiles')
.select('id', { count: 'exact', head: true })
.eq('organization_id', orgId),
supabase
.from('posts')
.select('published', { count: 'exact' })
.eq('organization_id', orgId),
supabase
.from('audit_logs')
.select(`
action,
resource_type,
created_at,
user:profiles!user_id(username, full_name)
`)
.eq('organization_id', orgId)
.order('created_at', { ascending: false })
.limit(10)
])
const totalMembers = membersResult.count || 0
const allPosts = postsResult.data || []
const publishedPosts = allPosts.filter(p => p.published).length
const draftPosts = allPosts.length - publishedPosts
return {
totalMembers,
totalPosts: allPosts.length,
publishedPosts,
draftPosts,
recentActivity: activityResult.data || []
}
}
// Método para logging de ações
private static async logAction(
action: string,
resourceType: string,
resourceId: string,
metadata: any = {}
): Promise<void> {
try {
await supabase.rpc('log_user_action', {
p_action: action,
p_resource_type: resourceType,
p_resource_id: resourceId,
p_metadata: metadata
})
} catch (error) {
console.warn('Erro ao registrar log:', error)
}
}
// Busca universal com full-text search
static async searchAll(query: string, filters?: {
types?: ('posts' | 'profiles' | 'organizations')[]
organizationId?: string
}): Promise<{
posts: Post[]
profiles: Profile[]
organizations: Organization[]
}> {
const searchTypes = filters?.types || ['posts', 'profiles', 'organizations']
const results: any = { posts: [], profiles: [], organizations: [] }
const searches = []
if (searchTypes.includes('posts')) {
searches.push(
supabase
.from('posts')
.select('*')
.textSearch('title', query)
.eq('published', true)
.limit(10)
)
}
if (searchTypes.includes('profiles')) {
searches.push(
supabase
.from('profiles')
.select('*')
.or(`full_name.ilike.%${query}%,username.ilike.%${query}%`)
.limit(10)
)
}
if (searchTypes.includes('organizations')) {
searches.push(
supabase
.from('organizations')
.select('*')
.or(`name.ilike.%${query}%,description.ilike.%${query}%`)
.limit(10)
)
}
const searchResults = await Promise.allSettled(searches)
searchTypes.forEach((type, index) => {
const result = searchResults[index]
if (result.status === 'fulfilled' && result.value.data) {
results[type] = result.value.data
}
})
return results
}
}
// Classe de erro customizada
export class APIError extends Error {
constructor(message: string, public originalError?: any) {
super(message)
this.name = 'APIError'
}
}
// Hooks React para usar a API
export function useProfile(userId?: string) {
const [profile, setProfile] = useState<Profile | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
if (!userId) return
const fetchProfile = async () => {
try {
setLoading(true)
const data = await SupabaseAPI.getProfile(userId)
setProfile(data)
} catch (err) {
setError(err instanceof Error ? err.message : 'Erro desconhecido')
} finally {
setLoading(false)
}
}
fetchProfile()
}, [userId])
return { profile, loading, error }
}
⚠️API Customizada
Para casos específicos que a API REST automática não atende, crie endpoints customizados usando Edge Functions ou páginas API do Next.js com validação adequada.
Testing e Quality Assurance
Estratégias completas de teste para aplicações Supabase, incluindo testes unitários, integração e end-to-end.
// __tests__/supabase.test.ts
import { createClient } from '@supabase/supabase-js'
import { Database } from '@/lib/database.types'
// Setup de teste com Supabase local
const supabaseUrl = process.env.SUPABASE_TEST_URL || 'http://localhost:54321'
const supabaseKey = process.env.SUPABASE_TEST_ANON_KEY || 'test-key'
const supabase = createClient<Database>(supabaseUrl, supabaseKey)
describe('Supabase Integration Tests', () => {
let testUserId: string
let testOrgId: string
beforeAll(async () => {
// Setup inicial para testes
await setupTestDatabase()
})
afterAll(async () => {
// Cleanup após testes
await cleanupTestDatabase()
})
beforeEach(async () => {
// Reset do estado antes de cada teste
await resetTestData()
})
describe('Authentication', () => {
test('deve criar usuário com email e senha', async () => {
const email = `test-${Date.now()}@example.com`
const password = 'password123'
const { data, error } = await supabase.auth.signUp({
email,
password,
})
expect(error).toBeNull()
expect(data.user).toBeDefined()
expect(data.user?.email).toBe(email)
testUserId = data.user!.id
})
test('deve fazer login com credenciais válidas', async () => {
const email = `test-${Date.now()}@example.com`
const password = 'password123'
// Criar usuário primeiro
await supabase.auth.signUp({ email, password })
// Fazer login
const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
})
expect(error).toBeNull()
expect(data.user).toBeDefined()
expect(data.session).toBeDefined()
})
test('deve falhar com credenciais inválidas', async () => {
const { data, error } = await supabase.auth.signInWithPassword({
email: 'invalid@example.com',
password: 'wrongpassword',
})
expect(error).toBeDefined()
expect(data.user).toBeNull()
})
})
describe('Database Operations', () => {
test('deve criar perfil de usuário', async () => {
const profileData = {
id: testUserId,
username: 'testuser',
full_name: 'Test User',
bio: 'Test bio',
}
const { data, error } = await supabase
.from('profiles')
.insert(profileData)
.select()
.single()
expect(error).toBeNull()
expect(data).toMatchObject(profileData)
})
test('deve criar organização', async () => {
const orgData = {
name: 'Test Organization',
slug: 'test-org',
description: 'Test organization description',
}
const { data, error } = await supabase
.from('organizations')
.insert(orgData)
.select()
.single()
expect(error).toBeNull()
expect(data).toMatchObject(orgData)
testOrgId = data.id
})
test('deve aplicar RLS corretamente', async () => {
// Tentar acessar dados de outro usuário deve falhar
const { data, error } = await supabase
.from('profiles')
.select('*')
.eq('id', 'other-user-id')
expect(data).toEqual([]) // RLS deve bloquear acesso
})
})
describe('Real-time Subscriptions', () => {
test('deve receber updates em tempo real', async () => {
const updates: any[] = []
// Configurar subscription
const subscription = supabase
.channel('test-channel')
.on(
'postgres_changes',
{ event: '*', schema: 'public', table: 'profiles' },
(payload) => {
updates.push(payload)
}
)
.subscribe()
// Aguardar conexão
await new Promise(resolve => setTimeout(resolve, 1000))
// Criar novo perfil
await supabase
.from('profiles')
.insert({
id: 'test-realtime-user',
username: 'realtimetest',
})
// Aguardar update
await new Promise(resolve => setTimeout(resolve, 1000))
expect(updates.length).toBeGreaterThan(0)
expect(updates[0].eventType).toBe('INSERT')
subscription.unsubscribe()
})
})
describe('Storage Operations', () => {
test('deve fazer upload de arquivo', async () => {
const fileName = 'test-file.txt'
const fileContent = 'Test file content'
const file = new Blob([fileContent], { type: 'text/plain' })
const { data, error } = await supabase.storage
.from('test-bucket')
.upload(fileName, file)
expect(error).toBeNull()
expect(data?.path).toBe(fileName)
})
test('deve deletar arquivo', async () => {
const fileName = 'test-delete.txt'
const file = new Blob(['test'], { type: 'text/plain' })
// Upload primeiro
await supabase.storage
.from('test-bucket')
.upload(fileName, file)
// Deletar
const { error } = await supabase.storage
.from('test-bucket')
.remove([fileName])
expect(error).toBeNull()
})
})
describe('Edge Functions', () => {
test('deve chamar edge function com sucesso', async () => {
const { data, error } = await supabase.functions.invoke('test-function', {
body: { message: 'Hello from test' }
})
expect(error).toBeNull()
expect(data).toBeDefined()
})
})
})
// Utilities para testes
async function setupTestDatabase() {
// Criar buckets de teste
await supabase.storage.createBucket('test-bucket', {
public: true,
fileSizeLimit: 1024 * 1024, // 1MB
})
// Executar migrations de teste se necessário
const { error } = await supabase.rpc('exec_sql', {
sql_query: `
-- Adicionar dados de teste se necessário
INSERT INTO test_data (name) VALUES ('test') ON CONFLICT DO NOTHING;
`
})
if (error) console.warn('Setup warning:', error)
}
async function cleanupTestDatabase() {
// Limpar dados de teste
await supabase.rpc('exec_sql', {
sql_query: `
DELETE FROM profiles WHERE username LIKE 'test%';
DELETE FROM organizations WHERE slug LIKE 'test%';
`
})
// Deletar bucket de teste
await supabase.storage.deleteBucket('test-bucket')
}
async function resetTestData() {
// Reset entre testes
await supabase.rpc('exec_sql', {
sql_query: `
TRUNCATE TABLE audit_logs;
DELETE FROM profiles WHERE username LIKE 'test%';
`
})
}
// __tests__/api-client.test.ts
import { SupabaseAPI, APIError } from '@/lib/api-client'
import { supabase } from '@/lib/supabase'
// Mock do Supabase para testes unitários
jest.mock('@/lib/supabase', () => ({
supabase: {
from: jest.fn(),
auth: {
getUser: jest.fn(),
},
rpc: jest.fn(),
}
}))
const mockSupabase = supabase as jest.Mocked<typeof supabase>
describe('SupabaseAPI', () => {
beforeEach(() => {
jest.clearAllMocks()
})
describe('getProfile', () => {
test('deve retornar perfil com sucesso', async () => {
const mockProfile = {
id: 'user-1',
username: 'testuser',
full_name: 'Test User',
}
mockSupabase.from.mockReturnValue({
select: jest.fn().mockReturnValue({
eq: jest.fn().mockReturnValue({
single: jest.fn().mockResolvedValue({
data: mockProfile,
error: null
})
})
})
} as any)
const result = await SupabaseAPI.getProfile('user-1')
expect(result).toEqual(mockProfile)
expect(mockSupabase.from).toHaveBeenCalledWith('profiles')
})
test('deve lançar APIError em caso de erro', async () => {
const mockError = { message: 'Database error' }
mockSupabase.from.mockReturnValue({
select: jest.fn().mockReturnValue({
eq: jest.fn().mockReturnValue({
single: jest.fn().mockResolvedValue({
data: null,
error: mockError
})
})
})
} as any)
await expect(SupabaseAPI.getProfile('user-1')).rejects.toThrow(APIError)
})
})
describe('getPosts', () => {
test('deve retornar posts paginados', async () => {
const mockPosts = [
{ id: 'post-1', title: 'Post 1' },
{ id: 'post-2', title: 'Post 2' }
]
mockSupabase.from.mockReturnValue({
select: jest.fn().mockReturnValue({
eq: jest.fn().mockReturnValue({
order: jest.fn().mockReturnValue({
range: jest.fn().mockResolvedValue({
data: mockPosts,
error: null,
count: 2
})
})
})
})
} as any)
const result = await SupabaseAPI.getPosts({ page: 1, limit: 10 })
expect(result).toEqual({
posts: mockPosts,
total: 2,
page: 1,
totalPages: 1
})
})
})
describe('searchAll', () => {
test('deve buscar em múltiplas tabelas', async () => {
const mockResults = {
posts: [{ id: 'post-1', title: 'Search result' }],
profiles: [{ id: 'user-1', username: 'searchuser' }],
organizations: [{ id: 'org-1', name: 'Search Org' }]
}
// Mock para posts
mockSupabase.from.mockImplementation((table) => {
const mockData = mockResults[table as keyof typeof mockResults] || []
return {
select: jest.fn().mockReturnValue({
textSearch: jest.fn().mockReturnValue({
eq: jest.fn().mockReturnValue({
limit: jest.fn().mockResolvedValue({
data: mockData,
error: null
})
})
}),
or: jest.fn().mockReturnValue({
limit: jest.fn().mockResolvedValue({
data: mockData,
error: null
})
})
})
} as any
})
const result = await SupabaseAPI.searchAll('search query')
expect(result.posts).toEqual(mockResults.posts)
expect(result.profiles).toEqual(mockResults.profiles)
expect(result.organizations).toEqual(mockResults.organizations)
})
})
})
// Performance Tests
describe('SupabaseAPI Performance', () => {
test('deve completar queries em tempo hábil', async () => {
const startTime = Date.now()
// Mock rápido
mockSupabase.from.mockReturnValue({
select: jest.fn().mockReturnValue({
eq: jest.fn().mockReturnValue({
single: jest.fn().mockResolvedValue({
data: { id: 'test' },
error: null
})
})
})
} as any)
await SupabaseAPI.getProfile('test-user')
const duration = Date.now() - startTime
expect(duration).toBeLessThan(100) // Should complete in < 100ms
})
test('deve gerenciar concurrent requests', async () => {
mockSupabase.from.mockReturnValue({
select: jest.fn().mockReturnValue({
eq: jest.fn().mockReturnValue({
single: jest.fn().mockResolvedValue({
data: { id: 'test' },
error: null
})
})
})
} as any)
const promises = Array.from({ length: 10 }, (_, i) =>
SupabaseAPI.getProfile(`user-${i}`)
)
const results = await Promise.all(promises)
expect(results).toHaveLength(10)
expect(results.every(r => r?.id === 'test')).toBe(true)
})
})
Jest + Testing Library
Testes unitários e de integração completos
Playwright
Testes end-to-end para fluxos críticos
Supabase Test Helpers
Utilities para testes com Supabase local
MSW (Mock Service Worker)
Mock de APIs para testes isolados
Segurança Avançada e Compliance
Implementação de práticas rigorosas de segurança, auditoria e conformidade com regulamentações como LGPD e GDPR.
// lib/security.ts
import { supabase } from './supabase'
import crypto from 'crypto'
// Configurações de segurança
export const SECURITY_CONFIG = {
passwordMinLength: 12,
passwordComplexityRegex: /^(?=.*[a-z])(?=.*[A-Z])(?=.*d)(?=.*[@$!%*?&])[A-Za-zd@$!%*?&]/,
maxLoginAttempts: 5,
lockoutDuration: 15 * 60 * 1000, // 15 minutos
sessionTimeout: 24 * 60 * 60 * 1000, // 24 horas
otpExpiration: 5 * 60 * 1000, // 5 minutos
}
// Classe para gerenciamento de segurança
export class SecurityManager {
// Validação robusta de senha
static validatePassword(password: string): {
isValid: boolean
errors: string[]
} {
const errors: string[] = []
if (password.length < SECURITY_CONFIG.passwordMinLength) {
errors.push(`Senha deve ter pelo menos ${SECURITY_CONFIG.passwordMinLength} caracteres`)
}
if (!SECURITY_CONFIG.passwordComplexityRegex.test(password)) {
errors.push('Senha deve conter: maiúscula, minúscula, número e símbolo')
}
// Verificar senhas comuns
const commonPasswords = [
'password123', '123456789', 'qwerty123', 'admin123',
'password1', 'welcome123', 'letmein123'
]
if (commonPasswords.some(common =>
password.toLowerCase().includes(common.toLowerCase())
)) {
errors.push('Senha muito comum, escolha uma mais segura')
}
return {
isValid: errors.length === 0,
errors
}
}
// Rate limiting para tentativas de login
static async checkRateLimit(
identifier: string,
action: string = 'login'
): Promise<{
allowed: boolean
attempts: number
resetTime?: Date
}> {
const key = `rate_limit:${action}:${identifier}`
try {
// Buscar tentativas recentes
const { data: attempts, error } = await supabase
.from('rate_limits')
.select('*')
.eq('identifier', identifier)
.eq('action', action)
.gte('created_at', new Date(Date.now() - SECURITY_CONFIG.lockoutDuration).toISOString())
.order('created_at', { ascending: false })
if (error) throw error
const recentAttempts = attempts?.length || 0
if (recentAttempts >= SECURITY_CONFIG.maxLoginAttempts) {
const oldestAttempt = attempts?.[attempts.length - 1]
const resetTime = new Date(
new Date(oldestAttempt.created_at).getTime() + SECURITY_CONFIG.lockoutDuration
)
return {
allowed: false,
attempts: recentAttempts,
resetTime
}
}
return {
allowed: true,
attempts: recentAttempts
}
} catch (error) {
console.error('Erro ao verificar rate limit:', error)
// Em caso de erro, permitir (fail-open) mas logar
await this.logSecurityEvent('rate_limit_error', identifier, { error })
return { allowed: true, attempts: 0 }
}
}
// Registrar tentativa para rate limiting
static async recordAttempt(
identifier: string,
action: string,
success: boolean,
metadata: any = {}
): Promise<void> {
try {
await supabase
.from('rate_limits')
.insert({
identifier,
action,
success,
metadata,
ip_address: metadata.ip,
user_agent: metadata.userAgent
})
// Log de segurança
await this.logSecurityEvent(
success ? 'auth_success' : 'auth_failure',
identifier,
{ action, ...metadata }
)
} catch (error) {
console.error('Erro ao registrar tentativa:', error)
}
}
// Two-Factor Authentication
static async generateTOTP(userId: string): Promise<{
secret: string
qrCode: string
backupCodes: string[]
}> {
const secret = crypto.randomBytes(20).toString('hex')
// Gerar códigos de backup
const backupCodes = Array.from({ length: 8 }, () =>
crypto.randomBytes(4).toString('hex').toUpperCase()
)
// Salvar configuração 2FA
const { error } = await supabase
.from('user_2fa')
.upsert({
user_id: userId,
secret,
backup_codes: backupCodes,
enabled: false // Usuário deve confirmar primeiro
})
if (error) throw error
// Gerar QR Code (usando biblioteca externa)
const qrCode = `otpauth://totp/SeuApp:${userId}?secret=${secret}&issuer=SeuApp`
await this.logSecurityEvent('2fa_setup_initiated', userId)
return { secret, qrCode, backupCodes }
}
static async verifyTOTP(
userId: string,
token: string,
isBackupCode: boolean = false
): Promise<boolean> {
try {
const { data: config } = await supabase
.from('user_2fa')
.select('*')
.eq('user_id', userId)
.single()
if (!config) return false
if (isBackupCode) {
// Verificar código de backup
if (config.backup_codes.includes(token.toUpperCase())) {
// Remover código usado
const updatedCodes = config.backup_codes.filter(
code => code !== token.toUpperCase()
)
await supabase
.from('user_2fa')
.update({ backup_codes: updatedCodes })
.eq('user_id', userId)
await this.logSecurityEvent('2fa_backup_used', userId, { codesRemaining: updatedCodes.length })
return true
}
return false
}
// Verificar TOTP (implementar verificação real)
const isValid = this.verifyTOTPToken(config.secret, token)
if (isValid) {
await this.logSecurityEvent('2fa_success', userId)
} else {
await this.logSecurityEvent('2fa_failure', userId, { token: token.substring(0, 2) + '****' })
}
return isValid
} catch (error) {
await this.logSecurityEvent('2fa_error', userId, { error: error.message })
return false
}
}
// Verificação de token TOTP (simplificada)
private static verifyTOTPToken(secret: string, token: string): boolean {
// Implementar verificação real do TOTP
// Esta é uma versão simplificada
const timeStep = Math.floor(Date.now() / 30000)
const expectedToken = this.generateTOTPToken(secret, timeStep)
return token === expectedToken
}
private static generateTOTPToken(secret: string, timeStep: number): string {
// Implementação simplificada - use biblioteca como 'otplib' em produção
const hash = crypto
.createHmac('sha1', secret)
.update(timeStep.toString())
.digest('hex')
return hash.substring(0, 6)
}
// Sanitização de dados
static sanitizeInput(input: string, type: 'html' | 'sql' | 'xss' = 'xss'): string {
if (!input) return ''
switch (type) {
case 'html':
return input
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(///g, '/')
case 'sql':
return input
.replace(/'/g, "''")
.replace(/;/g, '')
.replace(/--/g, '')
.replace(//*/g, '')
.replace(/*//g, '')
case 'xss':
default:
return input
.replace(/<script[^<]*(?:(?!</script>)<[^<]*)*</script>/gi, '')
.replace(/javascript:/gi, '')
.replace(/onw+s*=/gi, '')
.trim()
}
}
// Validação de CSRF token
static async validateCSRFToken(token: string, sessionId: string): Promise<boolean> {
try {
const { data } = await supabase
.from('csrf_tokens')
.select('*')
.eq('token', token)
.eq('session_id', sessionId)
.gte('expires_at', new Date().toISOString())
.single()
if (data) {
// Remover token após uso (one-time use)
await supabase
.from('csrf_tokens')
.delete()
.eq('token', token)
return true
}
return false
} catch (error) {
await this.logSecurityEvent('csrf_validation_error', sessionId, { error })
return false
}
}
// Log de eventos de segurança
static async logSecurityEvent(
event: string,
userId: string,
metadata: any = {}
): Promise<void> {
try {
await supabase
.from('security_logs')
.insert({
user_id: userId,
event,
metadata,
ip_address: metadata.ip,
user_agent: metadata.userAgent,
timestamp: new Date().toISOString()
})
// Alertas para eventos críticos
const criticalEvents = [
'multiple_failed_logins',
'suspicious_activity',
'2fa_disabled',
'admin_access',
'data_export'
]
if (criticalEvents.includes(event)) {
await this.triggerSecurityAlert(event, userId, metadata)
}
} catch (error) {
console.error('Erro ao registrar evento de segurança:', error)
}
}
// Sistema de alertas de segurança
private static async triggerSecurityAlert(
event: string,
userId: string,
metadata: any
): Promise<void> {
try {
// Enviar alerta para administradores
await supabase.functions.invoke('security-alert', {
body: {
event,
userId,
metadata,
timestamp: new Date().toISOString()
}
})
} catch (error) {
console.error('Erro ao enviar alerta de segurança:', error)
}
}
// Compliance com LGPD/GDPR
static async exportUserData(userId: string): Promise<any> {
try {
const [profile, posts, logs] = await Promise.all([
supabase.from('profiles').select('*').eq('id', userId),
supabase.from('posts').select('*').eq('author_id', userId),
supabase.from('audit_logs').select('*').eq('user_id', userId)
])
const userData = {
profile: profile.data?.[0],
posts: posts.data || [],
activityLogs: logs.data || [],
exportedAt: new Date().toISOString(),
format: 'JSON'
}
await this.logSecurityEvent('data_export', userId, {
recordsCount: {
profile: 1,
posts: posts.data?.length || 0,
logs: logs.data?.length || 0
}
})
return userData
} catch (error) {
await this.logSecurityEvent('data_export_error', userId, { error })
throw error
}
}
static async deleteUserData(userId: string, reason: string): Promise<void> {
try {
// Soft delete primeiro (manter por período legal)
await supabase
.from('profiles')
.update({
deleted_at: new Date().toISOString(),
deletion_reason: reason,
status: 'deleted'
})
.eq('id', userId)
// Anonimizar posts ao invés de deletar
await supabase
.from('posts')
.update({
author_id: null,
content: '[CONTEÚDO REMOVIDO - USUÁRIO DELETADO]',
title: '[TÍTULO REMOVIDO - USUÁRIO DELETADO]'
})
.eq('author_id', userId)
await this.logSecurityEvent('user_data_deletion', userId, { reason })
} catch (error) {
await this.logSecurityEvent('data_deletion_error', userId, { error, reason })
throw error
}
}
}
// Middleware de segurança para APIs
export function withSecurity(handler: any) {
return async (req: any, res: any) => {
try {
// Rate limiting
const clientIP = req.ip || req.connection.remoteAddress
const rateCheck = await SecurityManager.checkRateLimit(clientIP, 'api')
if (!rateCheck.allowed) {
return res.status(429).json({
error: 'Too many requests',
resetTime: rateCheck.resetTime
})
}
// CSRF protection
if (['POST', 'PUT', 'DELETE'].includes(req.method)) {
const csrfToken = req.headers['x-csrf-token']
const sessionId = req.headers['x-session-id']
if (!csrfToken || !sessionId) {
return res.status(403).json({ error: 'CSRF token required' })
}
const validCSRF = await SecurityManager.validateCSRFToken(csrfToken, sessionId)
if (!validCSRF) {
return res.status(403).json({ error: 'Invalid CSRF token' })
}
}
// Sanitizar inputs
if (req.body) {
req.body = sanitizeObject(req.body)
}
return handler(req, res)
} catch (error) {
console.error('Security middleware error:', error)
return res.status(500).json({ error: 'Security check failed' })
}
}
}
function sanitizeObject(obj: any): any {
if (typeof obj !== 'object' || obj === null) {
return typeof obj === 'string' ? SecurityManager.sanitizeInput(obj) : obj
}
const sanitized: any = Array.isArray(obj) ? [] : {}
for (const key in obj) {
sanitized[key] = sanitizeObject(obj[key])
}
return sanitized
}
⚠️Compliance LGPD/GDPR
Implemente funcionalidades de portabilidade de dados, direito ao esquecimento e consentimento explícito. Mantenha logs de auditoria e processos de resposta a incidentes.