Coding Standards & Style Guide
This document defines the coding standards, design system, and style guidelines for the Vertical Farm project.
🎨 Design System Overview
Our design system provides consistency across the entire application through: - Design Tokens: Standardized spacing, colors, typography - Utility Classes: Reusable styling patterns - Component Library: Type-safe, accessible components - Mobile-First: Responsive design approach
📐 Design Tokens
Core Tokens
Located in frontend/src/app/globals.css
:
/* Farm-specific Design Tokens */
--spacing-plant: 0.75rem; /* Plant-level spacing */
--spacing-row: 1.5rem; /* Row-level spacing */
--spacing-rack: 2rem; /* Rack-level spacing */
--spacing-shelf: 1rem; /* Shelf-level spacing */
--spacing-sensor: 0.5rem; /* Sensor spacing */
/* Component Sizes */
--size-plant-icon: 1.5rem;
--size-sensor-icon: 1.25rem;
--size-control-btn: 2.5rem;
--size-status-indicator: 0.5rem;
/* Animation Timing */
--duration-fast: 150ms;
--duration-normal: 250ms;
--duration-slow: 350ms;
--duration-pulse: 2s;
/* Easing Functions */
--ease-farm: cubic-bezier(0.4, 0, 0.2, 1);
--ease-gentle: cubic-bezier(0.25, 0.46, 0.45, 0.94);
--ease-bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55);
/* Typography */
--font-size-plant-label: 0.75rem;
--font-size-sensor-value: 1.125rem;
--font-size-farm-title: 1.5rem;
--font-size-control-label: 0.875rem;
/* Z-Index Layers */
--z-index-farm-overlay: 10;
--z-index-sensor-popup: 20;
--z-index-control-panel: 30;
--z-index-modal: 40;
/* Form & Input Tokens */
--input-height-sm: 2rem;
--input-height-md: 2.5rem;
--input-height-lg: 3rem;
--validation-success: #10b981;
--validation-warning: #f59e0b;
--validation-error: #ef4444;
/* Mobile Touch Targets */
--touch-target-min: 44px;
--touch-spacing: 12px;
🎯 Frontend Standards (Next.js 15 / React 19)
Component Architecture
Server vs Client Components
// DEFAULT: Server Component (no 'use client')
export default async function FarmDashboard({ farmId }: Props) {
// Can fetch data directly
const farms = await farmService.getFarmsByUser(userId)
return <FarmList farms={farms} />
}
// Client Component (interactive features)
'use client'
export function FarmControls({ farm }: Props) {
const [optimisticState, setOptimisticState] = useOptimistic(farm.state)
// Handle user interactions
}
Component Organization
components/
├── features/ # Domain-specific components
│ ├── agriculture/ # Farm management
│ │ ├── FarmCard.tsx
│ │ ├── PlantMonitor.tsx
│ │ └── HarvestSchedule.tsx
│ ├── automation/ # Device control
│ │ ├── DeviceControl.tsx
│ │ ├── SensorPanel.tsx
│ │ └── AutomationRules.tsx
│ └── business/ # Analytics
│ ├── YieldChart.tsx
│ └── CostAnalysis.tsx
├── ui/ # Reusable UI components
│ ├── Button.tsx
│ ├── Card.tsx
│ └── Modal.tsx
└── layout/ # Layout components
├── Header.tsx
├── Sidebar.tsx
└── Footer.tsx
Styling Patterns
Utility Classes
// Farm Hierarchy Gradients
<div className="gradient-farm"> {/* Blue gradient for farms */}
<div className="gradient-row"> {/* Green gradient for rows */}
<div className="gradient-rack"> {/* Yellow gradient for racks */}
<div className="gradient-shelf"> {/* Purple gradient for shelves */}
// Typography Utilities
<h1 className="text-farm-title">Farm Dashboard</h1>
<span className="text-sensor-value">24.5°C</span>
<label className="text-control-label">Enable Automation</label>
<small className="text-plant-label">Planted: 2024-01-15</small>
// State Patterns
<div className="state-active"> {/* Green ring, active state */}
<div className="state-maintenance"> {/* Amber ring, maintenance */}
<div className="state-offline"> {/* Red ring, offline */}
<div className="state-growing"> {/* Pulsing animation */}
// Component Utilities
<button className="farm-control-btn">Control</button>
<div className="plant-card">Plant Information</div>
<div className="sensor-panel">Sensor Data</div>
<div className="farm-grid">Grid Layout</div>
<div className="rack-layout">Rack Structure</div>
Component Styling with CVA
import { cva, type VariantProps } from 'class-variance-authority'
const buttonVariants = cva(
'farm-control-btn transition-all duration-normal ease-farm',
{
variants: {
variant: {
default: 'bg-white text-gray-900',
primary: 'gradient-farm text-white',
maintenance: 'state-maintenance',
offline: 'state-offline',
growing: 'state-growing animate-pulse-slow'
},
size: {
sm: 'h-8 px-3 text-sm',
default: 'h-10 px-4',
lg: 'h-12 px-6 text-lg touch-target'
}
},
defaultVariants: {
variant: 'default',
size: 'default'
}
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
icon?: React.ReactNode
}
export const FarmControlButton = forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, icon, children, ...props }, ref) => {
return (
<button
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
>
{icon && <span className="mr-2">{icon}</span>}
{children}
</button>
)
}
)
Form Components
Input Component
<FarmInput
label="Plant Name"
placeholder="Enter plant name..."
icon={<Leaf className="h-4 w-4" />}
helpText="Unique identifier for this plant"
errorText="This field is required"
validation="error"
inputSize="lg"
/>
Select Component
<FarmSelect
label="Plant Species"
options={[
{ value: "lettuce", label: "Lettuce (Lactuca sativa)" },
{ value: "spinach", label: "Spinach (Spinacia oleracea)" }
]}
placeholder="Select species..."
helpText="Choose from available plant species"
/>
Range Slider
<FarmRangeSlider
label="pH Level"
min={5.0}
max={7.0}
step={0.1}
value={6.2}
unit=""
markPoints={[
{ value: 5.5, label: "5.5" },
{ value: 6.0, label: "6.0" },
{ value: 6.5, label: "6.5" }
]}
helpText="Optimal pH for nutrient uptake"
/>
Performance Optimization
// Use React 19's "use cache" directive
import { cache } from 'react'
const getCachedFarmData = cache(async (farmId: string) => {
'use cache'
cacheLife: 3600 // Cache for 1 hour
const farms = await farmService.getFarmById(farmId)
return farms
})
// Optimistic updates with useOptimistic
function DeviceToggle({ device }) {
const [optimisticState, setOptimisticState] = useOptimistic(
device.isOn,
(currentState, newState) => newState
)
async function toggle() {
setOptimisticState(!optimisticState)
await deviceService.toggle(device.id)
}
}
🐍 Backend Standards (FastAPI / Python 3.13)
Project Structure
backend/app/
├── api/v1/ # API endpoints
│ ├── farms.py
│ ├── devices.py
│ └── analytics.py
├── core/ # Core functionality
│ ├── config.py
│ ├── security.py
│ └── exceptions.py
├── crud/ # Database operations
│ ├── base.py
│ └── farm.py
├── models/ # SQLAlchemy models
│ └── farm.py
├── schemas/ # Pydantic schemas
│ └── farm.py
├── services/ # Business logic
│ ├── base.py
│ └── farm_service.py
└── tests/ # Test files
├── unit/
└── integration/
Code Style
Type Hints (Required)
from typing import Optional, List, Dict, Any
from datetime import datetime
from uuid import UUID
async def get_farm_by_id(
farm_id: UUID,
user_id: UUID,
include_devices: bool = False
) -> Optional[Farm]:
"""
Retrieve a farm by ID with optional device information.
Args:
farm_id: The unique identifier of the farm
user_id: The ID of the requesting user
include_devices: Whether to include device data
Returns:
Farm object if found and user has access, None otherwise
Raises:
PermissionError: If user lacks access to the farm
"""
pass
Async Patterns
# Always use async/await for I/O operations
async def create_farm(farm_data: FarmCreate) -> Farm:
async with get_db() as db:
farm = Farm(**farm_data.dict())
db.add(farm)
await db.commit()
await db.refresh(farm)
return farm
# Use asyncio for concurrent operations
async def process_sensor_data(sensor_ids: List[UUID]):
tasks = [
fetch_sensor_data(sensor_id)
for sensor_id in sensor_ids
]
results = await asyncio.gather(*tasks)
return results
Error Handling
from fastapi import HTTPException, status
class FarmService:
async def update_farm(
self,
farm_id: UUID,
farm_update: FarmUpdate,
user_id: UUID
) -> Farm:
try:
farm = await self.get_farm(farm_id, user_id)
if not farm:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Farm {farm_id} not found"
)
# Update logic
return updated_farm
except IntegrityError as e:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Farm name already exists"
)
except Exception as e:
logger.error(f"Error updating farm {farm_id}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update farm"
)
Pydantic Models
from pydantic import BaseModel, Field, validator
from typing import Optional
from datetime import datetime
class FarmBase(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
location: Optional[str] = Field(None, max_length=200)
size_sqm: float = Field(..., gt=0, le=10000)
@validator('name')
def validate_name(cls, v):
if not v.strip():
raise ValueError('Farm name cannot be empty')
return v.strip()
class FarmCreate(FarmBase):
pass
class Farm(FarmBase):
id: UUID
user_id: UUID
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True # Python 3.13 Pydantic v2
API Design
from fastapi import APIRouter, Depends, Query
from typing import List, Optional
router = APIRouter(prefix="/api/v1/farms", tags=["farms"])
@router.get("/", response_model=List[FarmResponse])
async def list_farms(
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=100),
search: Optional[str] = None,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
) -> List[Farm]:
"""
List all farms accessible to the current user.
- **skip**: Number of records to skip (pagination)
- **limit**: Maximum number of records to return
- **search**: Optional search term for farm name
"""
service = FarmService(db)
return await service.list_user_farms(
user_id=current_user.id,
skip=skip,
limit=limit,
search=search
)
🗄️ Database Standards (Supabase/PostgreSQL)
Migration Naming
-- Format: YYYYMMDDHHMMSS_descriptive_name.sql
-- Example: 20240115143000_add_sensor_calibration_table.sql
-- Always include up and down migrations
-- UP Migration
CREATE TABLE sensor_calibration (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
sensor_id UUID NOT NULL REFERENCES sensors(id) ON DELETE CASCADE,
calibrated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
calibration_data JSONB NOT NULL,
created_by UUID NOT NULL REFERENCES auth.users(id)
);
-- Create indexes for foreign keys
CREATE INDEX idx_sensor_calibration_sensor_id ON sensor_calibration(sensor_id);
CREATE INDEX idx_sensor_calibration_created_by ON sensor_calibration(created_by);
-- DOWN Migration (commented)
-- DROP TABLE IF EXISTS sensor_calibration;
Row Level Security (RLS)
-- Enable RLS on all tables
ALTER TABLE farms ENABLE ROW LEVEL SECURITY;
-- Policy: Users can only see their own farms
CREATE POLICY "Users can view own farms" ON farms
FOR SELECT
TO authenticated
USING (user_id = auth.uid());
-- Policy: Users can create farms for themselves
CREATE POLICY "Users can create own farms" ON farms
FOR INSERT
TO authenticated
WITH CHECK (user_id = auth.uid());
-- Policy: Users can update their own farms
CREATE POLICY "Users can update own farms" ON farms
FOR UPDATE
TO authenticated
USING (user_id = auth.uid())
WITH CHECK (user_id = auth.uid());
-- Policy: Users can delete their own farms
CREATE POLICY "Users can delete own farms" ON farms
FOR DELETE
TO authenticated
USING (user_id = auth.uid());
📱 Mobile-First Design
Responsive Utilities
/* Mobile-first breakpoints */
/* Default: Mobile (<640px) */
/* sm: 640px+ */
/* md: 768px+ */
/* lg: 1024px+ */
/* xl: 1280px+ */
/* Mobile container with appropriate padding */
.mobile-container {
padding: var(--spacing-plant);
max-width: 100%;
}
/* Touch-friendly targets */
.touch-target {
min-height: var(--touch-target-min);
min-width: var(--touch-target-min);
padding: var(--touch-spacing);
}
/* Mobile stack layout */
.mobile-stack {
display: flex;
flex-direction: column;
gap: var(--spacing-row);
}
/* Responsive grid */
.mobile-grid {
display: grid;
grid-template-columns: 1fr;
gap: var(--spacing-plant);
}
@media (min-width: 640px) {
.mobile-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 1024px) {
.mobile-grid {
grid-template-columns: repeat(3, 1fr);
}
}
Touch Optimization
// Large touch targets for mobile
<FarmControlButton
size="lg" // Uses touch-target class
variant="primary"
onClick={handleAction}
>
Control Device
</FarmControlButton>
// Mobile-optimized forms
<form className="mobile-stack">
<FarmInput
inputSize="lg"
label="Temperature"
type="number"
inputMode="decimal" // Mobile keyboard optimization
/>
<FarmSelect
inputSize="lg"
options={options}
/>
<FarmControlButton size="lg" type="submit">
Save Settings
</FarmControlButton>
</form>
✅ Code Review Checklist
Frontend
- [ ] Server Components used by default
- [ ] Service layer pattern followed (no direct Supabase calls)
- [ ] Proper error handling with user-friendly messages
- [ ] TypeScript types for all props and returns
- [ ] Accessibility: ARIA labels, keyboard navigation
- [ ] Mobile responsive design tested
- [ ] Performance: No unnecessary re-renders
- [ ] Design tokens used instead of hardcoded values
Backend
- [ ] Type hints on all functions
- [ ] Async/await used consistently
- [ ] Proper error handling with appropriate HTTP status codes
- [ ] Pydantic models for request/response validation
- [ ] Database queries optimized with proper indexes
- [ ] RLS policies implemented for new tables
- [ ] API documentation updated
- [ ] Unit tests for business logic
General
- [ ] No sensitive data in code
- [ ] Environment variables used for configuration
- [ ] Code follows established patterns
- [ ] Tests written and passing
- [ ] Documentation updated
- [ ] No console.log or print statements
- [ ] Imports properly organized
🚀 Performance Guidelines
Frontend Performance
- Lighthouse score >90 for all metrics
- First Contentful Paint <1.5s
- Time to Interactive <3.5s
- Bundle size monitored and optimized
- Images optimized with Next.js Image component
- Code splitting with dynamic imports
Backend Performance
- API response time <200ms (p95)
- Database queries <50ms for indexed queries
- Proper caching strategy implemented
- Connection pooling configured
- Background tasks for heavy operations
- Rate limiting on public endpoints
Database Performance
- Indexes on all foreign keys
- Composite indexes for common query patterns
- EXPLAIN ANALYZE on complex queries
- Vacuum and analyze scheduled regularly
- Connection pooling via Supavisor
- Query result caching where appropriate
This style guide is a living document. Propose changes via pull request with justification for the modification.