/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:
- Analyzes your current team structure
- Creates project infrastructure and hierarchy
- Implements project-level permissions
- Adds resource scoping to projects
- Integrates project billing and limits
- 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 UILimited Projects Model
bash
/template recipe-projects-model limited
# Creates tiered project limits:
# - Free: 1 project
# - Pro: 5 projects
# - Business: 20 projects
# - Enterprise: UnlimitedHierarchical Projects
bash
/template recipe-projects-model hierarchical
# Advanced project structure with:
# - Parent/child projects
# - Project templates
# - Inheritance rules
# - Cross-project sharingWhat 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 updatesReact 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 actionsContext 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 CRUDTechnical 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-scopedBest 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;
},
};Related Commands
/template add-team-management- Team structure/template add-rbac- Role-based access/template add-api-keys- API authentication/template add-resource-limits- Usage limits
