Guides
Adding a New Feature

Adding a New Feature

Step-by-step guide to add a new feature to HiveForge.

1. Plan the Feature

Before coding, define:

  • User requirements
  • Database schema changes
  • API endpoints needed
  • UI components required
  • Permissions/access control

2. Database Schema

Create migration:

cd supabase/migrations
touch $(date +%Y%m%d%H%M%S)_add_projects.sql

Add schema:

-- Create projects table
CREATE TABLE projects (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
  name TEXT NOT NULL,
  description TEXT,
  created_by UUID NOT NULL REFERENCES profiles(id),
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);
 
-- RLS policies
CREATE POLICY "Users can view org projects"
  ON projects FOR SELECT
  USING (
    organization_id IN (
      SELECT organization_id FROM organization_members
      WHERE user_id = auth.uid()
    )
  );
 
CREATE POLICY "Members can create projects"
  ON projects FOR INSERT
  WITH CHECK (
    organization_id IN (
      SELECT organization_id FROM organization_members
      WHERE user_id = auth.uid()
    )
  );
 
-- Indexes
CREATE INDEX idx_projects_org ON projects(organization_id);

Apply migration:

pnpm db:migrate

3. Backend API

Create router:

# apps/api/app/routers/projects.py
from fastapi import APIRouter, Depends, HTTPException
from app.schemas.project import ProjectCreate, ProjectUpdate, ProjectResponse
from app.services.projects import ProjectService
from app.core.dependencies import get_current_user
 
router = APIRouter(prefix="/api/projects", tags=["projects"])
 
@router.get("/", response_model=list[ProjectResponse])
async def list_projects(
    organization_id: str,
    user = Depends(get_current_user)
):
    return await ProjectService.list(organization_id, user.id)
 
@router.post("/", response_model=ProjectResponse)
async def create_project(
    data: ProjectCreate,
    user = Depends(get_current_user)
):
    return await ProjectService.create(data, user.id)
 
@router.put("/{project_id}", response_model=ProjectResponse)
async def update_project(
    project_id: str,
    data: ProjectUpdate,
    user = Depends(get_current_user)
):
    return await ProjectService.update(project_id, data, user.id)
 
@router.delete("/{project_id}")
async def delete_project(
    project_id: str,
    user = Depends(get_current_user)
):
    await ProjectService.delete(project_id, user.id)
    return {"status": "deleted"}

Create service:

# apps/api/app/services/projects.py
from app.core.database import get_db
 
class ProjectService:
    @staticmethod
    async def create(data: ProjectCreate, user_id: str):
        db = get_db()
        result = await db.from_("projects").insert({
            "organization_id": data.organization_id,
            "name": data.name,
            "description": data.description,
            "created_by": user_id,
        }).execute()
        return result.data[0]
 
    @staticmethod
    async def list(organization_id: str, user_id: str):
        db = get_db()
        result = await db.from_("projects")\
            .select("*")\
            .eq("organization_id", organization_id)\
            .execute()
        return result.data

Register router:

# apps/api/app/main.py
from app.routers import projects
 
app.include_router(projects.router)

4. Frontend Components

Create types:

// packages/types/src/project.ts
export interface Project {
  id: string
  organization_id: string
  name: string
  description: string | null
  created_by: string
  created_at: string
  updated_at: string
}
 
export interface CreateProjectInput {
  organization_id: string
  name: string
  description?: string
}

Create API client:

// apps/web/src/lib/api/projects.ts
import { apiClient } from './client'
import type { Project, CreateProjectInput } from '@hiveforge/types'
 
export async function getProjects(organizationId: string): Promise<Project[]> {
  return apiClient.get(`/api/projects?organization_id=${organizationId}`)
}
 
export async function createProject(data: CreateProjectInput): Promise<Project> {
  return apiClient.post('/api/projects', data)
}
 
export async function updateProject(id: string, data: Partial<Project>): Promise<Project> {
  return apiClient.put(`/api/projects/${id}`, data)
}
 
export async function deleteProject(id: string): Promise<void> {
  return apiClient.delete(`/api/projects/${id}`)
}

Create components:

// apps/web/src/components/projects/ProjectList.tsx
'use client'
 
import { useEffect, useState } from 'react'
import { getProjects } from '@/lib/api/projects'
import { useOrganization } from '@/hooks/useOrganization'
import type { Project } from '@hiveforge/types'
 
export function ProjectList() {
  const { organization } = useOrganization()
  const [projects, setProjects] = useState<Project[]>([])
  const [loading, setLoading] = useState(true)
 
  useEffect(() => {
    if (organization) {
      loadProjects()
    }
  }, [organization])
 
  async function loadProjects() {
    try {
      const data = await getProjects(organization.id)
      setProjects(data)
    } catch (error) {
      console.error('Failed to load projects:', error)
    } finally {
      setLoading(false)
    }
  }
 
  if (loading) return <div>Loading...</div>
 
  return (
    <div>
      <h2>Projects</h2>
      <ul>
        {projects.map(project => (
          <li key={project.id}>{project.name}</li>
        ))}
      </ul>
    </div>
  )
}

5. Add Routes

// apps/web/src/app/(dashboard)/projects/page.tsx
import { ProjectList } from '@/components/projects/ProjectList'
 
export default function ProjectsPage() {
  return (
    <div>
      <h1>Projects</h1>
      <ProjectList />
    </div>
  )
}

6. Add Tests

Frontend tests:

// apps/web/src/components/projects/__tests__/ProjectList.test.tsx
import { render, screen, waitFor } from '@testing-library/react'
import { ProjectList } from '../ProjectList'
 
jest.mock('@/lib/api/projects', () => ({
  getProjects: jest.fn(() => Promise.resolve([
    { id: '1', name: 'Test Project', organization_id: 'org1' }
  ])),
}))
 
test('renders project list', async () => {
  render(<ProjectList />)
 
  await waitFor(() => {
    expect(screen.getByText('Test Project')).toBeInTheDocument()
  })
})

Backend tests:

# apps/api/tests/test_projects.py
import pytest
from httpx import AsyncClient
 
@pytest.mark.asyncio
async def test_create_project(client: AsyncClient, auth_headers):
    response = await client.post(
        "/api/projects",
        headers=auth_headers,
        json={
            "organization_id": "org-id",
            "name": "Test Project",
            "description": "A test project"
        }
    )
 
    assert response.status_code == 200
    data = response.json()
    assert data["name"] == "Test Project"

7. Add Documentation

Update docs:

# docs/features/projects.mdx
 
# Projects
 
Create and manage projects within organizations...

8. Deploy

# Build and test
pnpm build
pnpm test
 
# Deploy
git add .
git commit -m "feat: Add projects feature"
git push origin main

Best Practices

  1. Type Safety: Use TypeScript/Pydantic types
  2. Testing: Add unit and integration tests
  3. Documentation: Document API endpoints
  4. Permissions: Check user permissions
  5. Error Handling: Handle all error cases
  6. Validation: Validate input data
  7. Migrations: Version control database changes

Next Steps