Petunia Developer Guide
Version: 1.0.0 Last Updated: 2025-12-13 Status: Complete Onboarding Guide
Welcome to Petunia! This guide will help you set up your development environment, understand the project structure, and become productive quickly.
Table of Contents
- Quick Start
- Development Environment Setup
- Project Structure
- Key Architectural Patterns
- Common Development Workflows
- Testing Guidelines
- Code Style and Conventions
- Troubleshooting
- Additional Resources
Quick Start
Get up and running in 5 minutes:
# 1. Clone the repository
git clone https://github.com/gardenpatch-xyz/petunia.git
cd petunia
# 2. Install dependencies (pnpm is required)
pnpm install
# 3. Set up environment variables
cp .env.example .env.local
# Edit .env.local with your configuration (see Environment Setup below)
# 4. Validate environment
pnpm prestart
# 5. Generate Prisma client and start dev server
pnpm dev
# 6. Open http://localhost:3000
Prerequisites
- Node.js: 22.x (check with
node --version) - pnpm: 10.12.1+ (install with
npm install -g pnpm) - PostgreSQL: Access to a Supabase project or local PostgreSQL instance
- Git: For version control
Development Environment Setup
1. Node.js and pnpm
# Install Node.js 22.x via nvm (recommended)
nvm install 22
nvm use 22
# Install pnpm globally
npm install -g pnpm@10.12.1
2. Environment Variables
Petunia uses environment variables for configuration. Follow these steps:
# Copy the example file
cp .env.example .env.local
Required Variables (minimum for local development):
# Supabase Configuration
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
# Database
DATABASE_URL=postgresql://postgres:[PASSWORD]@db.[PROJECT].supabase.co:5432/postgres?pgbouncer=true
DIRECT_DATABASE_URL=postgresql://postgres:[PASSWORD]@db.[PROJECT].supabase.co:5432/postgres
# Application
NEXT_PUBLIC_SITE_URL=http://localhost:3000
# Security Keys (generate with: openssl rand -base64 32)
SESSION_SECRET=your-session-secret
CSRF_SECRET=your-csrf-secret
CREDENTIALS_ENCRYPTION_KEY=your-encryption-key
ADMIN_API_KEY=your-admin-api-key
INTERNAL_API_KEY=your-internal-api-key
CRON_SECRET=your-cron-secret
Where to Get Keys:
- Supabase Keys: Supabase Dashboard → Settings → API
- Database URLs: Supabase Dashboard → Settings → Database → Connection String
- Security Keys: Generate with
openssl rand -base64 32
For a complete list of environment variables, see docs/env/ENV_GUIDE.md.
IMPORTANT: NEVER commit .env.local or .env.production to git. Only .env.example should be committed.
3. Database Setup
# Run database migrations (creates tables)
pnpm prisma migrate dev
# Generate Prisma client (required after schema changes)
pnpm prisma generate
# Seed demo data (optional, takes ~4 minutes)
pnpm prisma db seed
4. Verify Installation
# Run type checking
pnpm typecheck
# Run tests
pnpm test
# Build the application
pnpm build:prod
If all commands succeed, you're ready to develop!
Project Structure
Petunia follows a strict directory structure for maintainability. Here's the layout:
/Users/tiago/Documents/Projects/petunia/
├── app/ # Next.js App Router
│ ├── api/ # ALL API routes (single source of truth)
│ │ ├── auth/ # Authentication endpoints
│ │ ├── onboarding/ # User onboarding
│ │ ├── inbox/ # Unified inbox
│ │ ├── voice/ # Voice AI
│ │ └── webhooks/ # Third-party webhooks
│ ├── (client)/ # Client-facing pages (dashboard, inbox, etc.)
│ ├── (public)/ # Public pages (landing, pricing, etc.)
│ ├── (admin)/ # Admin pages
│ ├── (system)/ # System pages (onboarding, settings)
│ └── layout.tsx # Root layout
│
├── components/ # React components
│ ├── ui/ # shadcn/ui components
│ └── [feature]/ # Feature-specific components
│
├── lib/ # Core application logic
│ ├── hooks/ # Custom React hooks (FLAT structure preferred)
│ ├── providers/ # React Context providers
│ ├── utils/ # Utility functions
│ ├── types/ # TypeScript type definitions
│ ├── services/ # Business logic services
│ ├── auth/ # Authentication utilities
│ ├── database/ # Database utilities
│ └── [domain]/ # Domain-specific code
│
├── tests/ # ALL test files (mirrors source structure)
│ ├── lib/ # Tests for /lib code
│ ├── components/ # Tests for /components code
│ ├── app/ # Tests for /app routes
│ ├── utils/ # Test utilities
│ └── mocks/ # Test mocks
│
├── prisma/ # Database schema and migrations
│ ├── schema.prisma # Database schema (133+ models)
│ ├── migrations/ # Database migrations
│ └── seed.ts # Database seeding script
│
├── docs/ # Documentation
│ ├── features/ # Feature-specific docs
│ ├── env/ # Environment variable docs
│ ├── security/ # Security docs
│ └── [topic]/ # Topic-specific docs
│
├── scripts/ # Utility scripts
│ ├── verify-env.ts # Environment validation
│ ├── auth-init.mjs # Auth initialization
│ └── [script].ts # Other scripts
│
├── public/ # Static assets
├── cypress/ # E2E tests (Cypress)
├── e2e/ # E2E tests (Playwright)
└── realtime-server/ # WebSocket server
Key Directories Explained
| Directory | Purpose | Rules |
|---|---|---|
/app/api/ | ALL API routes | NEVER create API routes elsewhere (no route groups) |
/tests/ | ALL test files | Mirror source paths (e.g., /lib/auth/ → /tests/lib/auth/) |
/lib/hooks/ | Custom React hooks | Keep FLAT unless part of feature module |
/lib/providers/ | React Context providers | ALL providers go here (no /contexts/) |
/lib/utils/ | Utility functions | Single source of truth for utilities |
/lib/types/ | TypeScript types | Single source of truth for types |
/docs/ | Documentation | Persistent docs only (use .tmp/ for work-in-progress) |
Path Aliases (tsconfig.json)
// Use these aliases in imports
import { Button } from '@/components/ui/button';
import { logger } from '@/lib/utils/logging';
import { createClient } from '@/lib/utils/supabase/server';
import type { AuthUser } from '@/lib/types/auth';
import { useAuth } from '@/hooks/auth/useAuth';
import { AuthProvider } from '@/lib/providers/AuthContext';
Key Architectural Patterns
1. Multi-Tenant Architecture (Portal-First)
Petunia is a multi-tenant platform where Portal is the primary tenant boundary.
Entity Hierarchy:
User → Company → Client → Portal
├── Conversations
├── Leads
├── Contacts
└── Reviews
Key Concepts:
- Portal: Workspace container (identified by
petuniaID) - Company: Business organization (persists across portals)
- Client: Organization representation (legacy, 1:1 with Company)
- User: Authenticated identity with role-based access
Row-Level Security (RLS): All database queries are protected by 4 layers:
- Middleware (session verification)
- Service layer (authorization checks)
- Query layer (portal filtering)
- RLS policies (database-level enforcement)
See docs/ARCHITECTURE.md for complete details.
2. Next.js App Router
Petunia uses Next.js 15 with the App Router:
Route Groups:
(client)- Client-facing pages (dashboard, inbox)(public)- Public pages (landing, pricing)(admin)- Admin pages (user management)(system)- System pages (onboarding, settings)
Important: Route groups are for PAGES ONLY. API routes MUST be in /app/api/.
Navigation:
// ✅ CORRECT - Client-side navigation
import { useRouter } from 'next/navigation';
const router = useRouter();
router.push('/dashboard');
// ❌ WRONG - Full page reload
window.location.href = '/dashboard';
Exceptions (when to use window.location.href):
- Logout (intentional full reload)
- OAuth flows (preserves user gesture chain)
- Post-auth redirects (ensures clean React tree)
3. Authentication & Security
Stack:
- Supabase Auth: Email/password, OAuth (Google, Facebook)
- TOTP 2FA: Two-factor authentication via
otplib - Session Management: Encrypted sessions with rotation
Auth Patterns:
// Server Components
import { createClient } from '@/lib/utils/supabase/server';
export default async function Page() {
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) redirect('/auth/signin');
// ...
}
// Client Components
'use client';
import { useAuth } from '@/hooks/auth/useAuth';
export function Component() {
const { user, isLoading } = useAuth();
if (isLoading) return <Spinner />;
if (!user) return <SignInPrompt />;
// ...
}
See docs/features/AUTHENTICATION.md.
4. Database Access (Prisma)
Prisma Client:
import { db } from '@/lib/database';
// Always filter by portal for tenant isolation
const conversations = await db.conversation.findMany({
where: { portalId: userPortal.id },
orderBy: { updatedAt: 'desc' }
});
Service Layer Pattern:
// lib/services/conversation.ts
export async function getConversations(portalId: string) {
// Authorization checks
if (!portalId) throw new Error('Portal ID required');
// Query with RLS
return db.conversation.findMany({
where: { portalId },
include: { messages: true }
});
}
5. API Routes
Standard Pattern:
// app/api/example/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/utils/supabase/server';
export async function GET(request: NextRequest) {
try {
// 1. Authenticate
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// 2. Get portal context
const portalId = request.headers.get('x-portal-id');
if (!portalId) {
return NextResponse.json({ error: 'Portal ID required' }, { status: 400 });
}
// 3. Authorize access
// ... authorization logic
// 4. Query data
const data = await getDataForPortal(portalId);
// 5. Return response
return NextResponse.json({ data });
} catch (error) {
console.error('API error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
6. Real-Time Updates
WebSocket Server:
// Realtime server runs separately
pnpm realtime:dev
// Client usage
import { useRealtimeConnection } from '@/hooks/useRealtimeConnection';
export function InboxPage() {
const { isConnected, subscribe } = useRealtimeConnection();
useEffect(() => {
const unsubscribe = subscribe('conversations', (message) => {
// Handle real-time message
});
return unsubscribe;
}, [subscribe]);
}
See docs/features/REAL_TIME_WEBSOCKET.md.
Common Development Workflows
Creating a New Feature
-
Check for existing implementations first:
# Search for similar files find . -name "*keyword*" # Search for similar functionality grep -r "keyword" lib/ -
Create files in correct locations:
# Component touch components/feature/MyComponent.tsx # Hook (FLAT preferred) touch lib/hooks/useMyFeature.ts # Service touch lib/services/myFeature.ts # API route touch app/api/my-feature/route.ts # Test (mirrors source) touch tests/components/feature/MyComponent.test.tsx -
Follow existing patterns:
- Look at similar features in the codebase
- Maintain consistency with existing code
- Use TypeScript strictly
-
Write tests:
# Run tests in watch mode pnpm test -- --watch -
Verify before committing:
# Type check pnpm typecheck # Run tests pnpm test # Build pnpm build:prod
Making API Changes
-
Update Prisma schema (if needed):
# Edit prisma/schema.prisma # Create migration pnpm prisma migrate dev --name add_my_field # Generate client pnpm prisma generate -
Create/update API route:
// app/api/my-endpoint/route.ts export async function POST(request: NextRequest) { // Implementation } -
Write API tests:
// tests/app/api/my-endpoint/route.test.ts import { POST } from '@/app/api/my-endpoint/route'; describe('POST /api/my-endpoint', () => { it('should create resource', async () => { // Test implementation }); }); -
Test manually:
# Start dev server pnpm dev # Test with curl curl -X POST http://localhost:3000/api/my-endpoint \ -H "Content-Type: application/json" \ -d '{"data": "value"}'
Running the Application
# Development mode (Next.js + Prisma generation)
pnpm dev
# Development mode with real-time server
pnpm dev:all
# Production build
pnpm build:prod
# Start production server
pnpm start
Database Operations
# Run migrations
pnpm prisma migrate dev
# Reset database (CAUTION: deletes all data)
pnpm prisma migrate reset
# Open Prisma Studio (database GUI)
pnpm prisma:studio
# Seed demo data
pnpm prisma db seed
# Check database health
pnpm db:health
Git Workflow
# Create feature branch
git checkout -b feature/my-feature
# Make changes and commit
git add .
git commit -m "feat: Add my feature"
# Pre-push hooks automatically run:
# - pnpm typecheck:strict (must pass with 0 errors)
# - pnpm build:prod (must succeed)
# Push to remote
git push origin feature/my-feature
# Create pull request on GitHub
Commit Message Conventions:
feat: Add new feature
fix: Fix bug in authentication
chore: Update dependencies
docs: Update developer guide
test: Add tests for inbox
refactor: Improve database queries
Testing Guidelines
Petunia has comprehensive testing infrastructure. See /TESTING.md for complete details.
Test Structure
ALL tests go in /tests/ directory (mirrors source structure):
/lib/services/conversation.ts
→ /tests/lib/services/conversation.test.ts
/components/ui/button.tsx
→ /tests/components/ui/button.test.tsx
/app/api/inbox/route.ts
→ /tests/app/api/inbox/route.test.ts
Running Tests
# Run all tests
pnpm test
# Run tests in watch mode
pnpm test -- --watch
# Run specific test file
pnpm test -- tests/lib/auth/useAuth.test.ts
# Run tests with coverage
pnpm test:coverage
# Run E2E tests (Cypress)
pnpm cypress:open
# Run E2E tests (Playwright)
pnpm playwright:test
Writing Unit Tests
// tests/lib/services/myService.test.ts
import { myFunction } from '@/lib/services/myService';
describe('myFunction', () => {
it('should return expected result', () => {
const result = myFunction('input');
expect(result).toBe('expected');
});
it('should handle errors', () => {
expect(() => myFunction(null)).toThrow();
});
});
Writing API Tests
// tests/app/api/my-endpoint/route.test.ts
import { POST } from '@/app/api/my-endpoint/route';
import { NextRequest } from 'next/server';
describe('POST /api/my-endpoint', () => {
it('should require authentication', async () => {
const request = new NextRequest('http://localhost:3000/api/my-endpoint');
const response = await POST(request);
expect(response.status).toBe(401);
});
});
Writing E2E Tests
// cypress/e2e/my-feature/flow.cy.ts
describe('My Feature Flow', () => {
it('should complete user journey', () => {
cy.visit('/my-feature');
cy.get('[data-testid="submit-button"]').click();
cy.url().should('include', '/success');
});
});
Test Coverage Goals
| Component | Target Coverage |
|---|---|
| Critical paths (auth, billing) | 95%+ |
| Business logic (services) | 90%+ |
| API routes | 85%+ |
| UI components | 80%+ |
| Utilities | 95%+ |
Code Style and Conventions
Petunia follows strict coding standards. See /CLAUDE.md for complete guidelines.
Directory Structure Rules
MANDATORY Rules (enforced by ESLint and pre-commit hooks):
- API Routes: ALL in
/app/api/(no route groups) - Tests: ALL in
/tests/(mirrors source paths) - Hooks: ALL in
/lib/hooks/(FLAT preferred) - Providers: ALL in
/lib/providers/ - Utils: ALL in
/lib/utils/ - Types: ALL in
/lib/types/
FORBIDDEN Patterns:
❌ /app/(system)/api/ # API routes in route groups
❌ /contexts/ # Use /lib/providers/
❌ /utils/ # Use /lib/utils/
❌ /types/ # Use /lib/types/
❌ /hooks/ # Use /lib/hooks/
❌ /components/*/hooks/ # Use /lib/hooks/
❌ *.backup.ts # No backup files
❌ *-v2.ts # No version suffixes
❌ enhanced-*.ts # No "improved" variants
File Naming Conventions
# Components (PascalCase)
Button.tsx
ConversationList.tsx
# Hooks (camelCase, start with "use")
useAuth.ts
useConversation.ts
# Providers (PascalCase, end with "Context" or "Provider")
AuthContext.tsx
ThemeProvider.tsx
# Utils (camelCase or kebab-case)
logging.ts
api-client.ts
# Types (camelCase or PascalCase)
auth.ts
ApiResponse.ts
# Tests (match source + .test)
Button.test.tsx
useAuth.test.ts
route.test.ts
TypeScript Standards
// ✅ Use strict types
interface User {
id: string;
email: string;
name: string | null;
}
// ✅ Avoid "any" (use "unknown" if needed)
function parse(data: unknown): User {
// Type guard
if (!isUser(data)) throw new Error('Invalid user');
return data;
}
// ✅ Use optional chaining and nullish coalescing
const userName = user?.profile?.name ?? 'Anonymous';
// ✅ Define return types explicitly
function getUser(id: string): Promise<User | null> {
return db.user.findUnique({ where: { id } });
}
Import Organization
// 1. External dependencies
import React from 'react';
import { NextRequest, NextResponse } from 'next/server';
// 2. Internal imports (grouped by alias)
import { Button } from '@/components/ui/button';
import { useAuth } from '@/hooks/auth/useAuth';
import { logger } from '@/lib/utils/logging';
import type { User } from '@/lib/types/auth';
// 3. Relative imports (if needed)
import { helper } from './helper';
Error Handling
// ✅ Structured error handling
try {
const result = await riskyOperation();
return NextResponse.json({ data: result });
} catch (error) {
// Log with context
console.error('Operation failed:', {
error: error instanceof Error ? error.message : 'Unknown error',
stack: error instanceof Error ? error.stack : undefined,
context: { userId, portalId }
});
// Return user-friendly error
return NextResponse.json(
{ error: 'Operation failed', details: 'Please try again' },
{ status: 500 }
);
}
Logging
import { logger } from '@/lib/utils/logging';
// ✅ Structured logging with context
logger.info('User action', {
action: 'create_conversation',
userId: user.id,
portalId: portal.id,
conversationId: conversation.id
});
// ✅ Error logging with stack traces
logger.error('Database error', {
error: error.message,
stack: error.stack,
query: 'findMany',
model: 'Conversation'
});
Pre-commit and Pre-push Hooks
Petunia uses Git hooks to prevent common issues:
Pre-commit Hook:
- Blocks forbidden files (backups, enhanced-*, -v2, etc.)
- Blocks tests outside
/tests/directory - Runs
pnpm typecheck:strict(must pass with 0 errors)
Pre-push Hook:
- Runs
pnpm typecheck:strict - Runs
pnpm build:prod(catches syntax/build errors)
To bypass (for urgent fixes only):
git push --no-verify
Troubleshooting
Common Issues and Solutions
"Module not found" errors
# Regenerate Prisma client
pnpm prisma generate
# Clear Next.js cache
rm -rf .next
# Reinstall dependencies
rm -rf node_modules pnpm-lock.yaml
pnpm install
TypeScript errors
# Run type check to see all errors
pnpm typecheck
# Check specific file
pnpm exec tsc --noEmit path/to/file.ts
# Update TypeScript (if needed)
pnpm update typescript
Database connection issues
# Verify DATABASE_URL is correct
echo $DATABASE_URL
# Test database connection
pnpm db:health
# Check Prisma can connect
pnpm prisma db pull
Build failures
# Clear all caches
pnpm clean
# Run production build
pnpm build:prod
# Check for syntax errors
pnpm typecheck
Tests failing
# Run tests in watch mode to debug
pnpm test -- --watch
# Run specific test file
pnpm test -- tests/path/to/test.test.ts
# Check test environment
NODE_ENV=test pnpm test
Hydration errors (React)
Petunia has comprehensive hydration error prevention using the ClientOnly component.
Quick debugging:
// Wrap client-only code
import { ClientOnly } from '@/components/ClientOnly';
export function MyComponent() {
return (
<ClientOnly>
{/* Client-only content */}
</ClientOnly>
);
}
WebSocket connection issues
# Start realtime server separately
pnpm realtime:dev
# Check WebSocket URL in .env.local
NEXT_PUBLIC_REALTIME_URL=ws://localhost:3001
NEXT_PUBLIC_WEBSOCKET_URL=ws://localhost:3001
Pre-push hook failures
# Run checks manually
pnpm typecheck:strict
pnpm build:prod
# Fix errors before pushing
# Or bypass for urgent fixes (not recommended)
git push --no-verify
Getting Help
-
Check existing documentation:
- README.md - Project overview
- TESTING.md - Testing guide
- CONTRIBUTING.md - Contributing guidelines
- docs/ARCHITECTURE.md - Architecture details
- docs/features/ - Feature-specific docs
-
Search the codebase:
# Find similar implementations grep -r "pattern" lib/ # Find files by name find . -name "*keyword*" -
Ask the team:
- Open an issue on GitHub
- Check team communication channels
Additional Resources
Documentation
- README.md - Project overview and quick start
- TESTING.md - Comprehensive testing guide
- CONTRIBUTING.md - Contributing guidelines
- CLAUDE.md - Code organization and standards
- docs/ARCHITECTURE.md - System architecture
- docs/env/ENV_GUIDE.md - Environment variables
- docs/features/ - Feature documentation
- docs/security/ - Security documentation
Key Features Documentation
- Authentication - Auth system and patterns
- Unified Inbox - Inbox architecture
- Real-Time WebSocket - WebSocket server
- Voice Providers - Voice AI integration
- Billing System - Stripe integration
- Demo Mode - Demo portal configuration
External Resources
- Next.js Documentation
- Prisma Documentation
- Supabase Documentation
- React Documentation
- TypeScript Handbook
- Tailwind CSS Documentation
Development Tools
# Code quality
pnpm lint # Run ESLint
pnpm format # Format with Prettier
pnpm typecheck # TypeScript type checking
# Testing
pnpm test # Run unit tests
pnpm test:coverage # Run tests with coverage
pnpm cypress:open # Open Cypress E2E tests
pnpm playwright:test # Run Playwright tests
# Database
pnpm prisma:studio # Open database GUI
pnpm prisma migrate dev # Run migrations
pnpm db:health # Check database health
# Analysis
pnpm analyze:bundle # Analyze bundle size
pnpm analyze:deps # Find unused dependencies
Next Steps
Now that you've set up your development environment:
-
Explore the codebase:
- Browse
/app/api/to understand API structure - Look at
/lib/services/for business logic - Check
/components/for UI patterns
- Browse
-
Run the demo:
pnpm prisma db seed pnpm dev # Visit http://localhost:3000 -
Pick a task:
- Check open issues on GitHub
- Look for
// TODOcomments in code - Improve test coverage
-
Make your first contribution:
- Follow the Contributing Guide
- Write tests for your changes
- Open a pull request
Welcome to the team! We're excited to have you contribute to Petunia.
Maintained by: Petunia Engineering Team Last Review: 2025-12-13 Next Review: 2026-01-13