> ## 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

> Supabase client creation patterns and utilities

# @tuturuuu/supabase

The `@tuturuuu/supabase` package provides type-safe Supabase client utilities for the Tuturuuu platform with different privilege levels and execution contexts.

## Installation

```bash theme={null}
# 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

```typescript theme={null}
import { createClient } from '@tuturuuu/supabase/next/server';

export default async function TaskPage() {
  const supabase = await 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)

```typescript theme={null}
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 sbAdmin
    .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()`)

> **Return type note:** `createAdminClient()` has the signature
> `({ noCookie }?: { noCookie?: boolean }) => SupabaseClient | Promise<SupabaseClient>`.
> The default call (`createAdminClient()` with `noCookie: false`) resolves cookies
> via `next/headers`, so it returns a `Promise` — `await` it as shown above. The
> `createAdminClient({ noCookie: true })` variant returns a client synchronously.
> Awaiting either form is safe (`await` on a non-Promise is a no-op), so prefer
> `await createAdminClient()` everywhere for consistency.

### `createDynamicClient()` - Middleware Client

**Use Case:** Middleware and edge runtime contexts.

**Context:** Next.js middleware, edge functions

```typescript theme={null}
import { createDynamicClient } from '@tuturuuu/supabase/next/server';
import { NextRequest } from 'next/server';

export async function proxy(request: NextRequest) {
  const supabase = await createDynamicClient();

  const { data: { user } } = await supabase.auth.getUser();

  if (!user) {
    return Response.redirect('/login');
  }
}
```

## Common Patterns

### Authentication

#### Revalidated Session User

Use `resolveAuthenticatedSessionUser()` from
`@tuturuuu/supabase/next/auth-session-user` when shared server code needs the
current Supabase session user. The helper may probe `auth.getClaims()` for
compatibility or timing context, but it must revalidate the session with
`auth.getUser()` before returning a user.

Treat JWT claims as verified token contents, not current account state. Do not
authorize privileged routes, staff-only checks, or `createAdminClient()` flows
from claim-derived `email`, `role`, or metadata alone. For those paths, make the
authorization decision from the Auth-server user returned by `getUser()` so
revoked, deleted, or offboarded sessions are rejected before any service-role
operation runs.

#### Get Current User

```typescript theme={null}
import { createClient } from '@tuturuuu/supabase/next/server';

export async function getCurrentUser() {
  const supabase = await createClient();
  const { data: { user }, error } = await supabase.auth.getUser();

  if (error || !user) {
    return null;
  }

  return user;
}
```

#### Server Action with Auth Check

```typescript theme={null}
'use server';

import { createClient } from '@tuturuuu/supabase/next/server';

export async function createTask(formData: FormData) {
  const supabase = await 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

```typescript theme={null}
import { createClient } from '@tuturuuu/supabase/next/server';

export async function getUserWorkspaces(userId: string) {
  const supabase = await 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

```typescript theme={null}
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 = await 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

```typescript theme={null}
import { createClient } from '@tuturuuu/supabase/next/server';

export async function getTasks(wsId: string, page = 0, pageSize = 20) {
  const supabase = await 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

```typescript theme={null}
import { createClient } from '@tuturuuu/supabase/next/server';

export async function getTasksWithAssignees(wsId: string, completed?: boolean) {
  const supabase = await 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

The browser `createClient()` from `@tuturuuu/supabase/next/client` is
**synchronous** (returns a `SupabaseClient`, not a `Promise`), so it is used
without `await`. Note this client is deprecated for general CRUD/storage — route
those through `@tuturuuu/internal-api` — but it remains valid for realtime
channel subscriptions, which are not covered by the internal API client.

```typescript theme={null}
'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

```typescript theme={null}
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 = await createClient();

  const { data, error } = await supabase
    .from('workspace_tasks')
    .insert(task)
    .select()
    .single();

  if (error) throw error;
  return data;
}
```

### Custom Type Helpers

```typescript theme={null}
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

```typescript theme={null}
import { createClient } from '@tuturuuu/supabase/next/server';

export async function getTask(taskId: string) {
  const supabase = await 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

```typescript theme={null}
import { createClient } from '@tuturuuu/supabase/next/server';

export async function safeGetTask(taskId: string) {
  try {
    const supabase = await 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

```typescript theme={null}
import { createClient } from '@tuturuuu/supabase/next/server';

export async function uploadAvatar(userId: string, file: File) {
  const supabase = await 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

```typescript theme={null}
import { createClient } from '@tuturuuu/supabase/next/server';

export async function downloadFile(path: string) {
  const supabase = await createClient();

  const { data, error } = await supabase.storage
    .from('public')
    .download(path);

  if (error) throw error;
  return data; // Blob
}
```

## Best Practices

### ✅ DO

1. **Use `createClient()` by default**
   ```typescript theme={null}
   const supabase = await createClient(); // Respects RLS
   ```

2. **Check authentication early**
   ```typescript theme={null}
   const { data: { user } } = await supabase.auth.getUser();
   if (!user) throw new Error('Unauthorized');
   ```

3. **Handle errors explicitly**
   ```typescript theme={null}
   const { data, error } = await supabase.from('table').select();
   if (error) throw error;
   ```

4. **Use TypeScript types**
   ```typescript theme={null}
   import type { Database } from '@tuturuuu/types';
   ```

5. **Limit data fetching**
   ```typescript theme={null}
   .select('id, name, created_at') // Only needed fields
   .limit(20) // Pagination
   ```

### ❌ DON'T

1. **Never use `createAdminClient()` for user operations**
   ```typescript theme={null}
   // ❌ Bad: Bypasses RLS
   const sbAdmin = await createAdminClient();
   const tasks = await sbAdmin.from('workspace_tasks').select();
   ```

2. **If you must use `createAdminClient()`, enforce membership and RBAC before the admin-backed operation**
   ```typescript theme={null}
   const member = await verifyWorkspaceMembershipType({
     wsId,
     userId: user.id,
     supabase: requestScopedSupabase,
   });
   if (!member.ok) {
     return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
   }

   const permissions = await getPermissions({ wsId, user });
   if (!permissions?.containsPermission('manage_projects')) {
     return NextResponse.json(
       { error: "You don't have permission" },
       { status: 403 }
     );
   }
   ```

3. **Keep workspace-scoped admin mutations filtered by workspace ID**
   ```typescript theme={null}
   await sbAdmin
     .from('workspace_boards')
     .update({ deleted_at: new Date().toISOString() })
     .eq('id', boardId)
     .eq('ws_id', wsId);
   ```

4. **Don't expose service role key**
   ```typescript theme={null}
   // ❌ Bad: Never do this
   const client = createClient(process.env.SUPABASE_SECRET_KEY);
   ```

5. **Don't ignore errors**
   ```typescript theme={null}
   // ❌ Bad: Silent failure
   const { data } = await supabase.from('table').select();
   return data; // Could be null!
   ```

6. **Don't fetch unnecessary data**
   ```typescript theme={null}
   // ❌ Bad: Over-fetching
   .select('*')
   .order('created_at')
   // No limit!
   ```

## Testing

### Mock Supabase Client

```typescript theme={null}
// __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

```typescript theme={null}
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');
  });
});
```

## Related Documentation

* [Database Schema](/reference/database/schema-overview)
* [RLS Policies](/reference/database/rls-policies)
* [Authentication](/platform/architecture/authentication)
* [Authorization](/platform/architecture/authorization)

## External Resources

* [Supabase JavaScript Client Docs](https://supabase.com/docs/reference/javascript/introduction)
* [Supabase Auth Helpers](https://supabase.com/docs/guides/auth/auth-helpers/nextjs)
