Type Safety
No any Types
Use strict TypeScript throughout. Define proper interfaces for all data structures.
Type Locations
| File | Convention | Purpose |
|---|
types/database.ts | snake_case | Match Supabase schema |
types/api.ts | camelCase | API request/response contracts |
types/forms.ts | camelCase | React Hook Form types |
Type Conversion
Database fields use snake_case, API responses use camelCase. Convert with utilities:
import { snakeToCamel, camelToSnake } from '@/lib/utils/typeConversion';
Component Patterns
shadcn/ui Imports
Always import from @/componentlibrary:
import { Button } from '@/componentlibrary/button';
import { Card } from '@/componentlibrary/card';
Feature Components
Location: components/features/{domain}/ where domain is auth/, recipe/, user/.
Each component gets its own folder:
components/features/recipe/
├── RecipeCard/
│ ├── RecipeCard.tsx
│ └── index.ts
Icons
Use Lucide React exclusively:
import { Heart, Camera, User, Settings } from 'lucide-react';
Styling
Tailwind CSS only — no CSS Modules. Use cn() for conditional classes:
import { cn } from '@/lib/utils';
<div className={cn("p-4", isActive && "bg-primary")} />
API Route Patterns (Phase 2 — apps/web)
These patterns apply to the web client API routes in apps/web, which is Phase 2. The mobile app calls Supabase directly — it does not use any web API routes.
Authentication
const authResult = await requireAuth(supabase);
if (!authResult.success) return authResult.response;
const { user } = authResult;
Error Responses
Use standardized utilities from @/lib/utils/errors:
return validationError('Invalid data', details); // 400
return unauthorizedError(); // 401
return forbiddenError('Not your resource'); // 403
return notFoundError('Recipe'); // 404
return conflictError('DUPLICATE', 'Already exists'); // 409
return internalError('Something went wrong'); // 500
Validation
Always validate with Zod schemas from @/lib/utils/validation:
const result = recipeSchema.safeParse(body);
if (!result.success) {
return validationError('Invalid data', result.error.flatten().fieldErrors);
}
const validation = validatePaginationParams(pageParam, limitParam);
if (!validation.valid) return validationError(validation.error);
const { page, limit, offset } = parsePagination({ page: pageParam, limit: limitParam });
State Management
| Layer | Tool | Location |
|---|
| Server state | React Query | hooks/api/ |
| Global state | Context providers | AuthContext, ModalContext, ToastContext |
| Form state | React Hook Form + Zod | components/forms/ |
Database Conventions
- Soft deletes:
is_deleted flag on recipes, posts, collections
- Always filter:
WHERE is_deleted = FALSE
- Note:
delete_post is a soft delete (is_deleted = true), but delete_recipe is a hard delete (cascading deletion of all related data). Both posts and recipes have the is_deleted column, but the recipe RPC bypasses it.
- Denormalized counters on
profiles table (recipe_count, post_count, etc.) maintained by triggers
- No database views — all complex queries use RPC functions (see RPC Functions)
Don’ts
- Use
getServerSideProps or getStaticProps (use App Router)
- Store sensitive data in localStorage
- Skip confirmation modals for destructive actions
- Hardcode URLs (use environment variables)
- Use CSS Modules
- Create duplicate components
- Use
any type
- Expose service role key to client