Skip to main content
8Space uses Supabase PostgREST and RPC functions to manage projects, tenants, and memberships.

Types

packages/app/src/domain/types.ts
export type ProjectRole = 'owner' | 'editor' | 'viewer';
export type TenantRole = 'owner' | 'admin' | 'member';

export interface Project {
  id: string;
  tenantId: string;
  name: string;
  description?: string | null;
  createdBy: string;
  createdAt: string;
  archivedAt?: string | null;
  role: ProjectRole;
}

export interface Tenant {
  id: string;
  name: string;
  slug: string;
  role: TenantRole;
}

export interface ProjectMember {
  projectId: string;
  userId: string;
  role: ProjectRole;
  profile?: UserProfile;
}

export interface UserProfile {
  id: string;
  displayName: string;
  avatarUrl?: string | null;
}

List Tenants

Query tenants for the current user.
packages/app/src/domain/repositories/supabase.ts
async listTenants(userId: string): Promise<Tenant[]> {
  const { data, error } = await supabase
    .from('tenant_members')
    .select('role,tenant:tenants!inner(id,name,slug,archived_at)')
    .eq('user_id', userId)
    .is('tenant.archived_at', null);

  if (error) throw error;
  return data.map(row => mapTenant(unwrapOne(row.tenant), row.role));
}

Query Parameters

user_id
string
required
Filter by user UUID using eq.<uuid>
select
string
PostgREST select clause. Use embedded resources with !inner for joins.

Response

role
TenantRole
User’s role in the tenant (owner, admin, member)
tenant
object
Embedded tenant object

Create Tenant with Owner

Create a new tenant and assign the current user as owner using RPC.
packages/app/src/domain/repositories/supabase.ts
async createTenantWithOwner(name: string, preferredSlug?: string): Promise<Tenant> {
  const { data, error } = await supabase.rpc('create_tenant_with_owner', {
    p_name: name,
    p_slug: preferredSlug ?? null,
  });

  if (error) throw error;
  return mapTenant(data as TenantRow, 'owner');
}

Request Body

p_name
string
required
Tenant name
p_slug
string | null
Preferred URL slug (auto-generated if null)

Response

Returns the created tenant row with the current user as owner.

List Projects

Query projects for the current user within a tenant.
packages/app/src/domain/repositories/supabase.ts
async listProjects(userId: string, tenantSlug: string): Promise<Project[]> {
  // First, get tenant by slug
  const { data: membershipData, error: membershipError } = await supabase
    .from('tenant_members')
    .select('role,tenant:tenants!inner(id,name,slug,archived_at)')
    .eq('user_id', userId)
    .eq('tenant.slug', tenantSlug)
    .is('tenant.archived_at', null)
    .maybeSingle();

  if (membershipError) throw membershipError;
  const tenant = unwrapOne(membershipData?.tenant);
  if (!tenant) return [];

  // Then, get projects
  const { data, error } = await supabase
    .from('project_members')
    .select('role,project:projects!inner(id,tenant_id,name,description,created_by,created_at,archived_at)')
    .eq('user_id', userId)
    .eq('project.tenant_id', tenant.id)
    .is('project.archived_at', null);

  if (error) throw error;
  return data.map(row => mapProject(unwrapOne(row.project), row.role));
}

Query Parameters

user_id
string
required
User UUID filter: eq.<uuid>
project_id
string
Project UUID filter: eq.<uuid>
select
string
PostgREST select clause with embedded resources

Response

role
ProjectRole
User’s role in the project
project
object
Embedded project object

Create Project with Defaults

Create a new project with default workflow columns using RPC.
packages/app/src/domain/repositories/supabase.ts
async createProjectWithDefaults(tenantSlug: string, input: CreateProjectInput): Promise<Project> {
  const { data: userResult, error: userError } = await supabase.auth.getUser();
  if (userError) throw userError;

  const userId = userResult.user?.id;
  if (!userId) throw new Error('Not authenticated');

  const { data, error } = await supabase.rpc('create_project_with_defaults', {
    p_tenant_slug: tenantSlug,
    p_name: input.name,
    p_description: input.description ?? null,
  });

  if (error) throw error;

  const projectId = data as string;
  const projects = await this.listProjects(userId, tenantSlug);
  return projects.find(p => p.id === projectId)!;
}

Request Body

p_tenant_slug
string
required
Tenant slug identifier
p_name
string
required
Project name
p_description
string | null
Project description

Response

Returns the created project UUID as a string.

Get Project Members

List all members of a project with their profiles.
packages/app/src/domain/repositories/supabase.ts
async getProjectMembers(projectId: string): Promise<ProjectMember[]> {
  const { data, error } = await supabase
    .from('project_members')
    .select('project_id,user_id,role,profile:profiles(id,display_name,avatar_url)')
    .eq('project_id', projectId);

  if (error) throw error;

  return data.map(row => ({
    projectId: row.project_id,
    userId: row.user_id,
    role: row.role,
    profile: mapProfile(unwrapOne(row.profile)),
  }));
}

Query Parameters

project_id
string
required
Filter by project UUID: eq.<uuid>
select
string
Include embedded profile data

Response

Returns array of project member objects.
project_id
string
Project UUID
user_id
string
User UUID
role
ProjectRole
Member role in project
profile
object
User profile data

Update Project Settings

Update project name and description.
packages/app/src/domain/repositories/supabase.ts
async updateProjectSettings(projectId: string, input: Pick<Project, 'name' | 'description'>): Promise<Project> {
  const { data: updateData, error } = await supabase
    .from('projects')
    .update({
      name: input.name,
      description: input.description ?? null,
    })
    .eq('id', projectId)
    .select('id,tenant_id,name,description,created_by,created_at,archived_at')
    .single();

  if (error) throw error;

  const updatedProject = updateData as ProjectRow;

  // Get current user's role
  const { data: roleData, error: roleError } = await supabase.rpc('current_project_role', {
    p_project_id: projectId,
  });

  if (roleError) throw roleError;

  const role = (roleData ?? 'viewer') as ProjectRole;
  return mapProject(updatedProject, role);
}

Query Parameters

id
string
required
Project UUID filter: eq.<uuid>

Request Body

name
string
Updated project name
description
string | null
Updated project description

Response

Returns array with updated project row(s).

Get Current Project Role

Query the current user’s role in a project using RPC.
packages/app/src/domain/repositories/supabase.ts
const { data: roleData, error: roleError } = await supabase.rpc('current_project_role', {
  p_project_id: projectId,
});

const role = (roleData ?? 'viewer') as ProjectRole;

Request Body

p_project_id
string
required
Project UUID

Response

Returns the user’s role as a string: owner, editor, or viewer.

React Hook Usage

packages/app/src/hooks/use-project-data.ts
import { useQuery, useMutation } from '@tanstack/react-query';
import { projectRepository } from '@/domain/repositories';

// List projects for a tenant
export function useProjects(tenantSlug: string | undefined) {
  const { user } = useAuth();

  return useQuery({
    queryKey: ['tenants', tenantSlug, 'projects', user?.id],
    queryFn: async () => {
      if (!user?.id || !tenantSlug) return [];
      return projectRepository.listProjects(user.id, tenantSlug);
    },
    enabled: Boolean(user?.id && tenantSlug),
  });
}

// Create a project
export function useCreateProject(tenantSlug: string | undefined) {
  const queryClient = useQueryClient();
  const { user } = useAuth();

  return useMutation({
    mutationFn: async (input: CreateProjectInput) => {
      if (!tenantSlug) throw new Error('Tenant is required');
      return projectRepository.createProjectWithDefaults(tenantSlug, input);
    },
    onSuccess: async () => {
      if (!user?.id || !tenantSlug) return;
      await queryClient.invalidateQueries({
        queryKey: ['tenants', tenantSlug, 'projects', user.id]
      });
    },
  });
}

// Update project settings
export function useUpdateProjectSettings(projectId: string | undefined, tenantSlug: string | undefined) {
  const queryClient = useQueryClient();
  const { user } = useAuth();

  return useMutation({
    mutationFn: async (input: { name: string; description?: string | null }) => {
      if (!projectId) throw new Error('Project is required');
      return projectRepository.updateProjectSettings(projectId, {
        name: input.name,
        description: input.description ?? null,
      });
    },
    onSuccess: async () => {
      if (!projectId || !user?.id || !tenantSlug) return;
      await queryClient.invalidateQueries({
        queryKey: ['tenants', tenantSlug, 'projects', user.id]
      });
    },
  });
}

Build docs developers (and LLMs) love