Skip to content

/template recipe-projects-model

Commercial License Required

MakerKit requires a commercial license. While Orchestre can help you build with it, you must obtain a valid license from MakerKit for commercial use.

Access: /template recipe-projects-model or /t recipe-projects-model

Purpose

Transforms your SaaS from a simple team-based model to a sophisticated projects-based architecture. This recipe implements a complete project management layer where teams can create multiple projects, each with its own resources, permissions, and billing context.

How It Actually Works

The recipe:

  1. Analyzes your current team structure
  2. Creates project infrastructure and hierarchy
  3. Implements project-level permissions
  4. Adds resource scoping to projects
  5. Integrates project billing and limits
  6. Sets up project discovery and switching

Use Cases

  • Development Platforms: Multiple apps per team
  • Design Tools: Separate design projects
  • Analytics Platforms: Different websites/apps
  • CI/CD Services: Multiple repositories
  • Content Platforms: Distinct publications
  • API Services: Separate API projects

Examples

Basic Projects Model

bash
/template recipe-projects-model
# Implements standard projects with:
# - Unlimited projects per team
# - Project-level permissions
# - Resource isolation
# - Quick switching UI

Limited Projects Model

bash
/template recipe-projects-model limited
# Creates tiered project limits:
# - Free: 1 project
# - Pro: 5 projects
# - Business: 20 projects
# - Enterprise: Unlimited

Hierarchical Projects

bash
/template recipe-projects-model hierarchical
# Advanced project structure with:
# - Parent/child projects
# - Project templates
# - Inheritance rules
# - Cross-project sharing

What Gets Created

Database Schema

sql
-- Projects table
create table projects (
  id uuid primary key default gen_random_uuid(),
  team_id uuid references teams(id) on delete cascade,
  name text not null,
  slug text not null,
  description text,
  avatar_url text,
  is_active boolean default true,
  settings jsonb default '{}',
  limits jsonb default '{}',
  parent_project_id uuid references projects(id),
  created_by uuid references auth.users(id),
  created_at timestamp with time zone default now(),
  updated_at timestamp with time zone default now(),
  unique(team_id, slug)
);

-- Project members with roles
create table project_members (
  id uuid primary key default gen_random_uuid(),
  project_id uuid references projects(id) on delete cascade,
  user_id uuid references auth.users(id) on delete cascade,
  role text not null check (role in ('owner', 'admin', 'member', 'viewer')),
  permissions jsonb default '{}',
  joined_at timestamp with time zone default now(),
  invited_by uuid references auth.users(id),
  last_accessed_at timestamp with time zone,
  unique(project_id, user_id)
);

-- Project resources (generic)
create table project_resources (
  id uuid primary key default gen_random_uuid(),
  project_id uuid references projects(id) on delete cascade,
  resource_type text not null,
  resource_id text not null,
  metadata jsonb default '{}',
  created_at timestamp with time zone default now(),
  unique(project_id, resource_type, resource_id)
);

-- Project activity log
create table project_activity (
  id uuid primary key default gen_random_uuid(),
  project_id uuid references projects(id) on delete cascade,
  user_id uuid references auth.users(id),
  action text not null,
  resource_type text,
  resource_id text,
  details jsonb default '{}',
  created_at timestamp with time zone default now()
);

-- Project invitations
create table project_invitations (
  id uuid primary key default gen_random_uuid(),
  project_id uuid references projects(id) on delete cascade,
  email text not null,
  role text not null,
  token text unique not null,
  invited_by uuid references auth.users(id),
  expires_at timestamp with time zone not null,
  accepted_at timestamp with time zone,
  created_at timestamp with time zone default now()
);

-- Project API keys
create table project_api_keys (
  id uuid primary key default gen_random_uuid(),
  project_id uuid references projects(id) on delete cascade,
  name text not null,
  key_hash text unique not null,
  permissions jsonb default '{}',
  last_used_at timestamp with time zone,
  expires_at timestamp with time zone,
  created_by uuid references auth.users(id),
  created_at timestamp with time zone default now()
);

-- Project billing allocation
create table project_billing (
  id uuid primary key default gen_random_uuid(),
  project_id uuid references projects(id) on delete cascade,
  team_id uuid references teams(id) on delete cascade,
  allocation_type text check (allocation_type in ('percentage', 'fixed', 'usage')),
  allocation_value jsonb not null,
  effective_from timestamp with time zone default now(),
  effective_to timestamp with time zone
);

API Routes

typescript
// app/api/projects/create/route.ts
POST /api/projects
- Create new project
- Validate limits
- Set up resources
- Assign creator as owner

// app/api/projects/[id]/route.ts
GET/PATCH/DELETE /api/projects/[id]
- Get project details
- Update settings
- Archive/delete project
- Check permissions

// app/api/projects/[id]/members/route.ts
GET/POST/DELETE /api/projects/[id]/members
- List project members
- Add/invite members
- Update roles
- Remove members

// app/api/projects/[id]/switch/route.ts
POST /api/projects/[id]/switch
- Switch active project
- Update session
- Load project context
- Track usage

// app/api/projects/[id]/resources/route.ts
GET/POST /api/projects/[id]/resources
- List project resources
- Create resources
- Scope queries
- Manage limits

// app/api/projects/[id]/activity/route.ts
GET /api/projects/[id]/activity
- Project activity feed
- Filter by type
- Pagination
- Real-time updates

React Components

typescript
// components/projects/project-switcher.tsx
- Dropdown selector
- Recent projects
- Quick search
- Create new option

// components/projects/project-create-modal.tsx
- Project name/slug
- Initial settings
- Team selection
- Template choice

// components/projects/project-settings.tsx
- General settings
- Member management
- Permissions grid
- Danger zone

// components/projects/project-members-table.tsx
- Member list
- Role management
- Activity status
- Bulk operations

// components/projects/project-dashboard.tsx
- Project overview
- Resource usage
- Activity feed
- Quick actions

// components/projects/project-resources.tsx
- Resource listing
- Type filtering
- Usage metrics
- Management actions

Context and Hooks

typescript
// contexts/project-context.tsx
- Current project state
- Project switching
- Permission checking
- Resource scoping

// hooks/use-project.ts
- useCurrentProject()
- useProjectMembers()
- useProjectResources()
- useProjectActivity()

// hooks/use-projects.ts
- useProjects()
- useCreateProject()
- useProjectLimits()
- useProjectBilling()

// lib/projects/permissions.ts
- Check project access
- Validate actions
- Role hierarchies
- Custom permissions

// lib/projects/resources.ts
- Scope resources
- Check limits
- Track usage
- Resource CRUD

Technical Details

Project Context Implementation

typescript
// Robust project context system
export const ProjectProvider: React.FC = ({ children }) => {
  const [currentProject, setCurrentProject] = useState<Project | null>(null);
  const { user } = useAuth();
  const router = useRouter();

  // Load project from URL or session
  useEffect(() => {
    const loadProject = async () => {
      const projectId = router.query.projectId || 
                       sessionStorage.getItem('currentProjectId');
      
      if (projectId) {
        const project = await fetchProject(projectId);
        if (await canAccessProject(user.id, project.id)) {
          setCurrentProject(project);
        } else {
          router.push('/projects');
        }
      }
    };
    
    loadProject();
  }, [router.query.projectId, user]);

  // Switch project handler
  const switchProject = async (projectId: string) => {
    const project = await fetchProject(projectId);
    
    // Update session
    sessionStorage.setItem('currentProjectId', projectId);
    
    // Update URL without navigation
    window.history.pushState(
      null, 
      '', 
      `${window.location.pathname}?projectId=${projectId}`
    );
    
    // Update context
    setCurrentProject(project);
    
    // Track switch
    await trackProjectSwitch(user.id, projectId);
  };

  return (
    <ProjectContext.Provider value={{
      currentProject,
      switchProject,
      isLoading: !currentProject
    }}>
      {children}
    </ProjectContext.Provider>
  );
};

Resource Scoping System

typescript
// Automatic resource scoping
class ProjectResourceManager {
  constructor(private projectId: string) {}

  // Scope any query to project
  async scopedQuery<T>(
    table: string,
    query: QueryBuilder<T>
  ): Promise<T[]> {
    // Check if table has project scoping
    const isScoped = await this.isTableScoped(table);
    
    if (isScoped) {
      query.where('project_id', this.projectId);
    }
    
    return query.execute();
  }

  // Create resource with project
  async createResource<T>(
    table: string,
    data: Partial<T>
  ): Promise<T> {
    // Auto-add project_id
    const scopedData = {
      ...data,
      project_id: this.projectId,
    };
    
    // Check limits
    await this.checkResourceLimits(table);
    
    // Create resource
    const resource = await db(table).insert(scopedData);
    
    // Track in project_resources
    await this.trackResource(table, resource.id);
    
    return resource;
  }

  // Check project limits
  private async checkResourceLimits(resourceType: string) {
    const project = await getProject(this.projectId);
    const limits = project.limits[resourceType];
    
    if (limits) {
      const count = await this.getResourceCount(resourceType);
      if (count >= limits.max) {
        throw new Error(`Project limit reached for ${resourceType}`);
      }
    }
  }
}

Permission System

typescript
// Granular project permissions
class ProjectPermissionSystem {
  // Permission hierarchy
  private roleHierarchy = {
    owner: ['admin', 'member', 'viewer'],
    admin: ['member', 'viewer'],
    member: ['viewer'],
    viewer: [],
  };

  // Check permission
  async can(
    userId: string,
    projectId: string,
    action: string
  ): Promise<boolean> {
    // Get user's role in project
    const member = await getProjectMember(projectId, userId);
    if (!member) return false;
    
    // Check role permissions
    const rolePerms = this.getRolePermissions(member.role);
    if (rolePerms.includes(action)) return true;
    
    // Check custom permissions
    const customPerms = member.permissions || {};
    if (customPerms[action] === true) return true;
    
    // Check inherited permissions
    if (this.canInherit(member.role, action)) return true;
    
    return false;
  }

  // Batch permission check
  async canMany(
    userId: string,
    projectId: string,
    actions: string[]
  ): Promise<Record<string, boolean>> {
    const results: Record<string, boolean> = {};
    
    // Optimize with single query
    const member = await getProjectMember(projectId, userId);
    
    for (const action of actions) {
      results[action] = member ? 
        await this.checkMemberPermission(member, action) : 
        false;
    }
    
    return results;
  }
}

Memory Evolution

The recipe creates comprehensive project system memory:

markdown
## Projects Model Configuration

### Project Structure
- Projects per team: Unlimited (Pro plan)
- Nesting: Single level
- Templates: 5 starter templates
- Default role: Member

### Permission Model
- Owner: Full control, billing, deletion
- Admin: Settings, members, resources
- Member: Create/edit resources
- Viewer: Read-only access

### Resource Limits (per project)
- Free plan: 100 resources
- Pro plan: 10,000 resources
- Business: 100,000 resources
- Enterprise: Unlimited

### Current Implementation
- URL-based project context
- Session persistence
- Quick switcher in navbar
- Project-scoped API routes

### Migration Status
- Existing resources: Assigned to default project
- Team members: Auto-added to all projects
- Permissions: Migrated from team roles
- API keys: Now project-scoped

Best Practices

Project Organization

  • Use clear, descriptive names
  • Implement project templates
  • Set default permissions
  • Archive inactive projects
  • Regular cleanup policies

Performance Optimization

  • Index project_id columns
  • Cache current project
  • Lazy load project lists
  • Implement pagination
  • Use project-specific caches

Security Considerations

  • Validate project access
  • Scope all queries
  • Audit project actions
  • Implement rate limits
  • Regular permission reviews

User Experience

  • Remember last project
  • Quick project switching
  • Clear project indicators
  • Bulk resource operations
  • Project-wide search

Integration Points

With Authentication

typescript
// Project-aware authentication
const projectAuth = {
  async validateRequest(req: Request) {
    const user = await authenticateUser(req);
    const projectId = req.headers['x-project-id'];
    
    if (projectId) {
      const canAccess = await canAccessProject(user.id, projectId);
      if (!canAccess) {
        throw new ForbiddenError('No access to project');
      }
      
      req.project = await getProject(projectId);
    }
    
    return { user, project: req.project };
  },
};

With Billing

typescript
// Project-based usage tracking
const projectBilling = {
  async trackUsage(projectId: string, metric: string, quantity: number) {
    const project = await getProject(projectId);
    
    // Track at project level
    await trackProjectUsage(projectId, metric, quantity);
    
    // Roll up to team billing
    await aggregateTeamUsage(project.teamId, metric, quantity);
    
    // Check project limits
    await checkProjectLimits(projectId, metric);
  },
};

With API Keys

typescript
// Project-scoped API keys
const projectApiKeys = {
  async createKey(projectId: string, name: string, permissions: string[]) {
    const key = generateSecureKey();
    const hash = await hashKey(key);
    
    await db.project_api_keys.create({
      project_id: projectId,
      name,
      key_hash: hash,
      permissions,
    });
    
    return {
      key, // Only returned once
      id: key.id,
      prefix: key.substring(0, 8),
    };
  },
};

Troubleshooting

Common Issues

Project Not Loading

  • Check URL parameters
  • Verify session storage
  • Confirm permissions
  • Review project status

Resource Scoping Errors

  • Ensure project_id in queries
  • Check table schemas
  • Verify middleware
  • Review error logs

Permission Denied

  • Check user's project role
  • Verify permission logic
  • Review custom permissions
  • Test role hierarchy

Migration Helpers

typescript
// Migrate to projects model
const projectMigration = {
  async migrateTeamResources(teamId: string) {
    // Create default project
    const project = await createProject({
      teamId,
      name: 'Default Project',
      slug: 'default',
    });
    
    // Migrate resources
    const resources = await getTeamResources(teamId);
    for (const resource of resources) {
      await assignResourceToProject(resource.id, project.id);
    }
    
    // Add team members to project
    const members = await getTeamMembers(teamId);
    for (const member of members) {
      await addProjectMember(project.id, member.userId, member.role);
    }
    
    return project;
  },
};

Advanced Features

Project Templates

typescript
// Starter project templates
const projectTemplates = {
  async createFromTemplate(teamId: string, templateId: string, name: string) {
    const template = await getTemplate(templateId);
    
    // Create project
    const project = await createProject({
      teamId,
      name,
      settings: template.settings,
      limits: template.limits,
    });
    
    // Copy template resources
    for (const resource of template.resources) {
      await copyResourceToProject(resource, project.id);
    }
    
    // Set up default structure
    await template.setup(project);
    
    return project;
  },
};

Cross-Project Sharing

typescript
// Share resources between projects
const projectSharing = {
  async shareResource(
    resourceId: string,
    fromProjectId: string,
    toProjectId: string,
    permissions: string[]
  ) {
    // Verify ownership
    await verifyResourceOwnership(resourceId, fromProjectId);
    
    // Check sharing permissions
    await checkSharingPermissions(fromProjectId, toProjectId);
    
    // Create sharing link
    const share = await createResourceShare({
      resourceId,
      fromProjectId,
      toProjectId,
      permissions,
    });
    
    // Notify target project
    await notifyProjectOfShare(toProjectId, share);
    
    return share;
  },
};
  • /template add-team-management - Team structure
  • /template add-rbac - Role-based access
  • /template add-api-keys - API authentication
  • /template add-resource-limits - Usage limits

Built with ❤️ for the AI Coding community, by Praney Behl