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
Filter by user UUID using eq.<uuid>
PostgREST select clause. Use embedded resources with !inner for joins.
Response
User’s role in the tenant (owner, admin, member)
Embedded tenant object
URL-safe tenant identifier
Timestamp when tenant was archived
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
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 UUID filter: eq.<uuid>
Project UUID filter: eq.<uuid>
PostgREST select clause with embedded resources
Response
User’s role in the project
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
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
Filter by project UUID: eq.<uuid>
Include embedded profile data
Response
Returns array of project member objects.
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
Project UUID filter: eq.<uuid>
Request Body
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
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]
});
},
});
}