Documentation Index
Fetch the complete documentation index at: https://docs.tuturuuu.com/llms.txt
Use this file to discover all available pages before exploring further.
@tuturuuu/supabase
The @tuturuuu/supabase package provides type-safe Supabase client utilities for the Tuturuuu platform with different privilege levels and execution contexts.
Installation
# Already included in monorepo workspace
import { createClient, createAdminClient } from '@tuturuuu/supabase/next/server';
Client Types
createClient() - User-Scoped Client
Use Case: Standard operations respecting Row-Level Security (RLS) policies.
Context: Server Components, Server Actions, API Routes
Privilege Level: Current authenticated user
import { createClient } from '@tuturuuu/supabase/next/server';
export default async function TaskPage() {
const supabase = createClient();
// Queries respect RLS policies
const { data: tasks } = await supabase
.from('workspace_tasks')
.select('*')
.eq('ws_id', 'workspace-id');
// User can only see tasks they have permission to view
return <div>{tasks?.length} tasks</div>;
}
Important:
- ✅ Respects RLS policies
- ✅ Safe for user-initiated operations
- ❌ Cannot bypass workspace isolation
- ❌ Cannot perform privileged operations
createAdminClient() - Service Role Client
Use Case: Privileged operations bypassing RLS (admin tasks, system operations).
Context: Server-side only (never expose to client)
Privilege Level: Service role (bypasses RLS)
import { createAdminClient } from '@tuturuuu/supabase/next/server';
export async function createWorkspace(name: string, ownerId: string) {
const sbAdmin = await createAdminClient();
// Bypasses RLS - can insert into any workspace
const { data: workspace } = await sbAdmin
.from('workspaces')
.insert({ name })
.select()
.single();
// Add owner as member
await supabase
.from('workspace_members')
.insert({
ws_id: workspace.id,
user_id: ownerId,
role: 'owner',
});
return workspace;
}
⚠️ Critical Security Warning:
- ✅ Only use server-side
- ✅ Only for privileged operations
- ❌ NEVER expose to client
- ❌ NEVER use for user-initiated queries (always prefer
createClient())
createDynamicClient() - Middleware Client
Use Case: Middleware and edge runtime contexts.
Context: Next.js middleware, edge functions
import { createDynamicClient } from '@tuturuuu/supabase/next/server';
import { NextRequest } from 'next/server';
export async function proxy(request: NextRequest) {
const supabase = createDynamicClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
return Response.redirect('/login');
}
}
Common Patterns
Authentication
Get Current User
import { createClient } from '@tuturuuu/supabase/next/server';
export async function getCurrentUser() {
const supabase = createClient();
const { data: { user }, error } = await supabase.auth.getUser();
if (error || !user) {
return null;
}
return user;
}
Server Action with Auth Check
'use server';
import { createClient } from '@tuturuuu/supabase/next/server';
export async function createTask(formData: FormData) {
const supabase = createClient();
// Check authentication
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
throw new Error('Unauthorized');
}
// Perform operation (RLS policies apply)
const { data, error } = await supabase
.from('workspace_tasks')
.insert({
name: formData.get('name') as string,
ws_id: formData.get('wsId') as string,
created_by: user.id,
})
.select()
.single();
if (error) throw error;
return data;
}
Workspace Operations
Get User’s Workspaces
import { createClient } from '@tuturuuu/supabase/next/server';
export async function getUserWorkspaces(userId: string) {
const supabase = createClient();
const { data: memberships } = await supabase
.from('workspace_members')
.select(`
ws_id,
role,
pending,
workspaces (
id,
name,
logo_url
)
`)
.eq('user_id', userId)
.eq('pending', false);
return memberships?.map((m) => m.workspaces) || [];
}
Check Workspace Permission
import { createClient } from '@tuturuuu/supabase/next/server';
import type { Database } from '@tuturuuu/types';
type Permission = Database['public']['Enums']['workspace_role_permission'];
export async function hasPermission(
userId: string,
wsId: string,
permission: Permission
): Promise<boolean> {
const supabase = createClient();
const { data } = await supabase
.from('workspace_members')
.select(`
role,
workspace_role_permissions!inner (
permission
)
`)
.eq('user_id', userId)
.eq('ws_id', wsId)
.eq('workspace_role_permissions.permission', permission)
.limit(1);
return (data?.length || 0) > 0;
}
Data Fetching
Paginated Query
import { createClient } from '@tuturuuu/supabase/next/server';
export async function getTasks(wsId: string, page = 0, pageSize = 20) {
const supabase = createClient();
const from = page * pageSize;
const to = from + pageSize - 1;
const { data: tasks, error, count } = await supabase
.from('workspace_tasks')
.select('*', { count: 'exact' })
.eq('ws_id', wsId)
.order('created_at', { ascending: false })
.range(from, to);
if (error) throw error;
return {
tasks,
totalCount: count,
currentPage: page,
totalPages: Math.ceil((count || 0) / pageSize),
};
}
Filtered Query with Relations
import { createClient } from '@tuturuuu/supabase/next/server';
export async function getTasksWithAssignees(wsId: string, completed?: boolean) {
const supabase = createClient();
let query = supabase
.from('workspace_tasks')
.select(`
*,
task_assignees (
workspace_users (
id,
display_name,
avatar_url
)
),
task_lists (
id,
name
)
`)
.eq('ws_id', wsId);
if (completed !== undefined) {
query = query.eq('completed', completed);
}
const { data, error } = await query;
if (error) throw error;
return data;
}
Realtime Subscriptions
'use client';
import { createClient } from '@tuturuuu/supabase/next/client';
import { useEffect, useState } from 'react';
import type { Database } from '@tuturuuu/types';
type Task = Database['public']['Tables']['workspace_tasks']['Row'];
export function useTaskSubscription(wsId: string) {
const [tasks, setTasks] = useState<Task[]>([]);
const supabase = createClient();
useEffect(() => {
// Initial fetch
supabase
.from('workspace_tasks')
.select('*')
.eq('ws_id', wsId)
.then(({ data }) => setTasks(data || []));
// Subscribe to changes
const channel = supabase
.channel(`workspace-tasks-${wsId}`)
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'workspace_tasks',
filter: `ws_id=eq.${wsId}`,
},
(payload) => {
if (payload.eventType === 'INSERT') {
setTasks((prev) => [...prev, payload.new as Task]);
} else if (payload.eventType === 'UPDATE') {
setTasks((prev) =>
prev.map((task) =>
task.id === payload.new.id ? (payload.new as Task) : task
)
);
} else if (payload.eventType === 'DELETE') {
setTasks((prev) => prev.filter((task) => task.id !== payload.old.id));
}
}
)
.subscribe();
return () => {
channel.unsubscribe();
};
}, [wsId]);
return tasks;
}
Type Safety
Using Generated Types
import type { Database } from '@tuturuuu/types';
import { createClient } from '@tuturuuu/supabase/next/server';
type Task = Database['public']['Tables']['workspace_tasks']['Row'];
type TaskInsert = Database['public']['Tables']['workspace_tasks']['Insert'];
type TaskUpdate = Database['public']['Tables']['workspace_tasks']['Update'];
export async function createTypedTask(task: TaskInsert): Promise<Task> {
const supabase = createClient();
const { data, error } = await supabase
.from('workspace_tasks')
.insert(task)
.select()
.single();
if (error) throw error;
return data;
}
Custom Type Helpers
import type { Database } from '@tuturuuu/types';
// Extract table types
export type Tables<T extends keyof Database['public']['Tables']> =
Database['public']['Tables'][T]['Row'];
export type Enums<T extends keyof Database['public']['Enums']> =
Database['public']['Enums'][T];
// Usage
type Task = Tables<'workspace_tasks'>;
type Permission = Enums<'workspace_role_permission'>;
Error Handling
Standard Pattern
import { createClient } from '@tuturuuu/supabase/next/server';
export async function getTask(taskId: string) {
const supabase = createClient();
const { data, error } = await supabase
.from('workspace_tasks')
.select('*')
.eq('id', taskId)
.single();
if (error) {
console.error('Failed to fetch task:', error);
throw new Error(`Task not found: ${taskId}`);
}
return data;
}
With Try-Catch
import { createClient } from '@tuturuuu/supabase/next/server';
export async function safeGetTask(taskId: string) {
try {
const supabase = createClient();
const { data, error } = await supabase
.from('workspace_tasks')
.select('*')
.eq('id', taskId)
.single();
if (error) throw error;
return { data, error: null };
} catch (error) {
console.error('Error fetching task:', error);
return { data: null, error };
}
}
Storage Operations
Upload File
import { createClient } from '@tuturuuu/supabase/next/server';
export async function uploadAvatar(userId: string, file: File) {
const supabase = createClient();
const fileName = `${userId}-${Date.now()}.${file.name.split('.').pop()}`;
const filePath = `avatars/${fileName}`;
const { data, error } = await supabase.storage
.from('public')
.upload(filePath, file, {
cacheControl: '3600',
upsert: false,
});
if (error) throw error;
const { data: { publicUrl } } = supabase.storage
.from('public')
.getPublicUrl(filePath);
return publicUrl;
}
Download File
import { createClient } from '@tuturuuu/supabase/next/server';
export async function downloadFile(path: string) {
const supabase = createClient();
const { data, error } = await supabase.storage
.from('public')
.download(path);
if (error) throw error;
return data; // Blob
}
Best Practices
✅ DO
-
Use
createClient() by default
const supabase = createClient(); // Respects RLS
-
Check authentication early
const { data: { user } } = await supabase.auth.getUser();
if (!user) throw new Error('Unauthorized');
-
Handle errors explicitly
const { data, error } = await supabase.from('table').select();
if (error) throw error;
-
Use TypeScript types
import type { Database } from '@tuturuuu/types';
-
Limit data fetching
.select('id, name, created_at') // Only needed fields
.limit(20) // Pagination
❌ DON’T
-
Never use
createAdminClient() for user operations
// ❌ Bad: Bypasses RLS
const sbAdmin = await createAdminClient();
const tasks = await sbAdmin.from('workspace_tasks').select();
-
Don’t expose service role key
// ❌ Bad: Never do this
const client = createClient(process.env.SUPABASE_SECRET_KEY);
-
Don’t ignore errors
// ❌ Bad: Silent failure
const { data } = await supabase.from('table').select();
return data; // Could be null!
-
Don’t fetch unnecessary data
// ❌ Bad: Over-fetching
.select('*')
.order('created_at')
// No limit!
Testing
Mock Supabase Client
// __mocks__/@tuturuuu/supabase/next/server.ts
export const createClient = vi.fn(() => ({
from: vi.fn(() => ({
select: vi.fn(() => ({
eq: vi.fn(() => ({
single: vi.fn(() => Promise.resolve({ data: mockData, error: null })),
})),
})),
})),
auth: {
getUser: vi.fn(() =>
Promise.resolve({ data: { user: mockUser }, error: null })
),
},
}));
Test Example
import { describe, it, expect, vi } from 'vitest';
import { getTask } from './tasks';
vi.mock('@tuturuuu/supabase/next/server');
describe('getTask', () => {
it('should fetch task by ID', async () => {
const task = await getTask('task-123');
expect(task).toBeDefined();
expect(task.id).toBe('task-123');
});
});
External Resources