NudgeCampaign Coding Standards: AI-First Email Marketing Platform
Generated: 2025-01-29 20:30 UTC
Version: 1.0 - Comprehensive Development Standards
Scope: Complete tech stack coding guidelines
Purpose: Maintain code quality, consistency, and best practices across the entire platform
Document Overview
This document establishes comprehensive coding standards for the NudgeCampaign AI-first email marketing platform. These standards ensure:
- Maintainable Code: Clear patterns and consistent structure
- Scalable Architecture: Designed for growth from startup to enterprise
- AI-First Principles: Optimized for conversational intelligence integration
- Production Quality: Enterprise-grade reliability and performance
Tech Stack Covered
| Component | Technology | Standards Covered |
|---|---|---|
| AI Layer | OpenAI GPT-4, Custom middleware | Intent analysis, safety validation |
| Frontend | Next.js 14, TypeScript, Tailwind | App Router, components, styling |
| Backend | Node.js 20, n8n, PostgreSQL | APIs, workflows, database design |
| Infrastructure | Google Cloud, Docker, Redis | Deployment, caching, monitoring |
| Postmark, Webhooks | Delivery, event handling | |
| Testing | Vitest, Playwright, Storybook | Unit, E2E, component testing |
Section 1: AI-First Development Standards
Core Principles
AI-first development puts conversational intelligence at the center of every feature, not as an add-on. This fundamental approach shapes how we design, build, and maintain the platform.
1.1 AI Service Integration Patterns
Provider-Agnostic Architecture
// β
Good: Abstracted AI service interface
interface AIProvider {
generateContent(prompt: string, context: AIContext): Promise<AIResponse>;
analyzeIntent(input: string): Promise<Intent>;
validateContent(content: string): Promise<ValidationResult>;
}
class OpenAIProvider implements AIProvider {
private client: OpenAI;
constructor(config: OpenAIConfig) {
this.client = new OpenAI({
apiKey: config.apiKey,
timeout: 30000,
maxRetries: 3,
});
}
async generateContent(prompt: string, context: AIContext): Promise<AIResponse> {
const response = await this.client.chat.completions.create({
model: 'gpt-4',
messages: [
{ role: 'system', content: this.buildSystemPrompt(context) },
{ role: 'user', content: prompt }
],
temperature: 0.7,
max_tokens: 1000,
});
return this.parseResponse(response);
}
}
// β Bad: Direct API calls scattered throughout codebase
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const result = await openai.chat.completions.create({...}); // Tightly coupled
AI Context Management
interface AIContext {
businessProfile: {
industry: string;
size: 'startup' | 'smb' | 'enterprise';
goals: string[];
};
userContext: {
experienceLevel: 'beginner' | 'intermediate' | 'expert';
previousCampaigns: CampaignHistory[];
preferences: UserPreferences;
};
sessionContext: {
conversationHistory: ConversationTurn[];
currentIntent: Intent;
activeCampaign?: Campaign;
};
}
class AIContextManager {
private context: Map<string, AIContext> = new Map();
enrichContext(userId: string, newData: Partial<AIContext>): AIContext {
const existing = this.context.get(userId) || this.getDefaultContext();
const enriched = deepMerge(existing, newData);
this.context.set(userId, enriched);
return enriched;
}
// Context should persist across conversation turns
persistContext(userId: string): Promise<void> {
return this.contextStore.save(userId, this.context.get(userId));
}
}
1.2 Intent Analysis & Classification
Structured Intent Processing
enum IntentType {
CREATE_CAMPAIGN = 'create_campaign',
MODIFY_CAMPAIGN = 'modify_campaign',
ANALYZE_PERFORMANCE = 'analyze_performance',
TROUBLESHOOT = 'troubleshoot',
LEARN_FEATURE = 'learn_feature',
}
interface Intent {
type: IntentType;
confidence: number;
entities: Record<string, any>;
requiresClarification: boolean;
suggestedActions: string[];
}
class IntentAnalyzer {
async analyzeUserInput(input: string, context: AIContext): Promise<Intent> {
// Multi-stage intent analysis
const [primaryIntent, entities, confidence] = await Promise.all([
this.classifyIntent(input, context),
this.extractEntities(input),
this.calculateConfidence(input, context),
]);
return {
type: primaryIntent,
confidence,
entities,
requiresClarification: confidence < 0.85,
suggestedActions: this.generateSuggestions(primaryIntent, entities),
};
}
private async classifyIntent(input: string, context: AIContext): Promise<IntentType> {
const prompt = `
Classify the user's intent for email marketing automation.
User input: "${input}"
Business context: ${JSON.stringify(context.businessProfile)}
Respond with exactly one of: ${Object.values(IntentType).join(', ')}
`;
const response = await this.aiProvider.generateContent(prompt, context);
return this.parseIntentResponse(response);
}
}
1.3 Safety & Validation Systems
Multi-Layer Validation Pipeline
interface ValidationRule {
name: string;
validate(content: string, context: AIContext): Promise<ValidationResult>;
severity: 'error' | 'warning' | 'info';
}
class ContentSafetyValidator {
private rules: ValidationRule[] = [
new SpamScoreValidator(),
new BrandVoiceValidator(),
new LegalComplianceValidator(),
new AccessibilityValidator(),
];
async validateContent(content: string, context: AIContext): Promise<ValidationSummary> {
const results = await Promise.all(
this.rules.map(rule => rule.validate(content, context))
);
const errors = results.filter(r => r.severity === 'error');
const warnings = results.filter(r => r.severity === 'warning');
return {
isValid: errors.length === 0,
canProceedWithWarnings: warnings.length < 3,
results,
suggestions: this.generateImprovementSuggestions(results),
};
}
}
// Example validation rule implementation
class SpamScoreValidator implements ValidationRule {
name = 'spam_score';
severity = 'warning' as const;
async validate(content: string): Promise<ValidationResult> {
const spamTriggers = [
/FREE!/gi, /URGENT!/gi, /ACT NOW!/gi, /LIMITED TIME!/gi
];
const triggerCount = spamTriggers.reduce((count, trigger) =>
count + (content.match(trigger)?.length || 0), 0
);
const score = Math.min(triggerCount * 0.2, 1.0);
return {
rule: this.name,
passed: score < 0.5,
score,
message: score > 0.5
? 'Content may trigger spam filters. Consider softening language.'
: 'Spam score is acceptable.',
suggestions: score > 0.5
? ['Replace urgent language with value-focused messaging']
: [],
};
}
}
Section 2: Frontend Standards (Next.js 14 + TypeScript)
Core Architecture Principles
NudgeCampaign uses Next.js 14 with App Router for optimal performance, SEO, and developer experience. Our frontend architecture prioritizes conversational interfaces and real-time AI interactions.
2.1 App Router Structure & Organization
Recommended Folder Structure
src/
βββ app/ # App Router pages
β βββ (dashboard)/ # Route groups for layout
β β βββ campaigns/
β β βββ analytics/
β β βββ settings/
β βββ (auth)/
β β βββ login/
β β βββ signup/
β βββ api/ # API routes
β β βββ ai/
β β βββ campaigns/
β β βββ webhooks/
β βββ globals.css
β βββ layout.tsx # Root layout
β βββ page.tsx # Home page
βββ components/ # Reusable components
β βββ ui/ # Base UI components
β βββ forms/ # Form components
β βββ ai/ # AI interaction components
β βββ dashboard/ # Dashboard-specific components
βββ lib/ # Utilities and configurations
β βββ ai/ # AI service integrations
β βββ db/ # Database utilities
β βββ email/ # Email service integrations
β βββ utils.ts # General utilities
βββ types/ # TypeScript type definitions
βββ ai.ts
βββ campaign.ts
βββ user.ts
Layout Pattern Implementation
// β
Good: Proper layout hierarchy with TypeScript
// app/layout.tsx - Root layout
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import { Providers } from './providers';
import './globals.css';
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: {
template: '%s | NudgeCampaign',
default: 'NudgeCampaign - AI-First Email Marketing',
},
description: 'Conversational email marketing automation platform',
keywords: ['email marketing', 'AI automation', 'campaigns'],
};
interface RootLayoutProps {
children: React.ReactNode;
}
export default function RootLayout({ children }: RootLayoutProps) {
return (
<html lang="en" className={inter.className}>
<body className="min-h-screen bg-background antialiased">
<Providers>
{children}
</Providers>
</body>
</html>
);
}
// app/(dashboard)/layout.tsx - Dashboard layout
import { DashboardNav } from '@/components/dashboard/nav';
import { ConversationPanel } from '@/components/ai/conversation-panel';
interface DashboardLayoutProps {
children: React.ReactNode;
}
export default function DashboardLayout({ children }: DashboardLayoutProps) {
return (
<div className="flex h-screen bg-gray-50">
<DashboardNav />
<main className="flex-1 flex">
<div className="flex-1 p-6">
{children}
</div>
<ConversationPanel /> {/* AI chat always available */}
</main>
</div>
);
}
2.2 Server vs Client Components Strategy
Server Components (Default Choice)
// β
Good: Server component for data fetching
// app/(dashboard)/campaigns/page.tsx
import { getCampaigns } from '@/lib/db/campaigns';
import { CampaignsList } from '@/components/dashboard/campaigns-list';
import { CreateCampaignButton } from '@/components/dashboard/create-campaign-button';
interface CampaignsPageProps {
searchParams: {
status?: 'active' | 'draft' | 'completed';
page?: string;
};
}
export default async function CampaignsPage({ searchParams }: CampaignsPageProps) {
// Server-side data fetching - no loading state needed
const campaigns = await getCampaigns({
status: searchParams.status,
page: Number(searchParams.page) || 1,
});
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
<h1 className="text-2xl font-bold">Your Campaigns</h1>
<CreateCampaignButton />
</div>
<CampaignsList
campaigns={campaigns}
initialStatus={searchParams.status}
/>
</div>
);
}
// Generate metadata for SEO
export async function generateMetadata({ searchParams }: CampaignsPageProps): Promise<Metadata> {
const status = searchParams.status || 'all';
return {
title: `${status.charAt(0).toUpperCase() + status.slice(1)} Campaigns`,
description: `Manage your ${status} email marketing campaigns`,
};
}
Client Components (Interactive Only)
// β
Good: Client component for interactivity
'use client';
import { useState, useTransition } from 'react';
import { useRouter } from 'next/navigation';
import { Button } from '@/components/ui/button';
import { ConversationInterface } from '@/components/ai/conversation-interface';
import { createCampaignFromConversation } from '@/lib/actions/campaigns';
export function CreateCampaignButton() {
const [isOpen, setIsOpen] = useState(false);
const [isPending, startTransition] = useTransition();
const router = useRouter();
const handleCampaignCreated = (campaignId: string) => {
startTransition(() => {
router.push(`/campaigns/${campaignId}`);
setIsOpen(false);
});
};
return (
<>
<Button
onClick={() => setIsOpen(true)}
disabled={isPending}
>
Create Campaign
</Button>
{isOpen && (
<ConversationInterface
onClose={() => setIsOpen(false)}
onCampaignCreated={handleCampaignCreated}
initialPrompt="What kind of email campaign would you like to create?"
/>
)}
</>
);
}
2.3 TypeScript Configuration & Patterns
Strict TypeScript Configuration
// tsconfig.json
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "es6"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@/components/*": ["./src/components/*"],
"@/lib/*": ["./src/lib/*"],
"@/types/*": ["./src/types/*"]
},
// Strict type checking options
"noUnusedLocals": true,
"noUnusedParameters": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
Type Definition Patterns
// types/campaign.ts - Domain types
export interface Campaign {
readonly id: string;
readonly userId: string;
name: string;
status: CampaignStatus;
type: CampaignType;
configuration: CampaignConfig;
metrics: CampaignMetrics;
readonly createdAt: Date;
readonly updatedAt: Date;
}
export type CampaignStatus = 'draft' | 'active' | 'paused' | 'completed';
export type CampaignType = 'welcome' | 'nurture' | 'promotional' | 'transactional';
export interface CampaignConfig {
trigger: CampaignTrigger;
emails: EmailTemplate[];
settings: CampaignSettings;
}
// Use branded types for IDs to prevent mixing
export type CampaignId = string & { readonly brand: unique symbol };
export type UserId = string & { readonly brand: unique symbol };
// Utility types for forms and API responses
export type CreateCampaignRequest = Omit<Campaign, 'id' | 'createdAt' | 'updatedAt'>;
export type UpdateCampaignRequest = Partial<Pick<Campaign, 'name' | 'status' | 'configuration'>>;
// β
Good: Discriminated unions for type safety
export type AIResponse =
| { success: true; data: string; confidence: number }
| { success: false; error: string; retryable: boolean };
// β
Good: Generic types for reusable patterns
export interface APIResponse<T> {
data?: T;
error?: string;
meta?: {
page: number;
limit: number;
total: number;
};
}
2.4 Tailwind CSS Design System
Design Tokens Configuration
// tailwind.config.js
const { fontFamily } = require('tailwindcss/defaultTheme');
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ['class'],
content: [
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {
colors: {
// Brand colors
brand: {
primary: 'hsl(var(--brand-primary))',
secondary: 'hsl(var(--brand-secondary))',
accent: 'hsl(var(--brand-accent))',
},
// AI interaction colors
ai: {
thinking: 'hsl(var(--ai-thinking))',
success: 'hsl(var(--ai-success))',
warning: 'hsl(var(--ai-warning))',
error: 'hsl(var(--ai-error))',
},
// Semantic colors
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))',
},
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
},
fontFamily: {
sans: ['var(--font-inter)', ...fontFamily.sans],
mono: ['var(--font-mono)', ...fontFamily.mono],
},
animation: {
'ai-thinking': 'pulse 2s infinite',
'slide-up': 'slideUp 0.3s ease-out',
'fade-in': 'fadeIn 0.2s ease-in',
},
},
},
plugins: [require('tailwindcss-animate')],
};
Component Styling Patterns
// β
Good: Consistent component styling with variants
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
default: 'bg-brand-primary text-white hover:bg-brand-primary/90',
secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200',
outline: 'border border-gray-300 bg-transparent hover:bg-gray-50',
ghost: 'hover:bg-gray-100',
ai: 'bg-ai-success text-white hover:bg-ai-success/90 animate-pulse',
},
size: {
sm: 'h-8 px-3 text-xs',
default: 'h-10 px-4',
lg: 'h-11 px-8',
icon: 'h-10 w-10',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
);
interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
export function Button({
className,
variant,
size,
asChild = false,
...props
}: ButtonProps) {
return (
<button
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
}
2.5 State Management & Data Fetching
Server Actions for Mutations
// lib/actions/campaigns.ts
'use server';
import { revalidateTag, revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import { z } from 'zod';
import { createCampaign, updateCampaign } from '@/lib/db/campaigns';
import { getCurrentUser } from '@/lib/auth';
const CreateCampaignSchema = z.object({
name: z.string().min(1).max(100),
type: z.enum(['welcome', 'nurture', 'promotional', 'transactional']),
aiPrompt: z.string().min(10).max(1000),
});
export async function createCampaignAction(formData: FormData) {
const user = await getCurrentUser();
if (!user) {
redirect('/login');
}
const validatedFields = CreateCampaignSchema.safeParse({
name: formData.get('name'),
type: formData.get('type'),
aiPrompt: formData.get('aiPrompt'),
});
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
};
}
try {
const campaign = await createCampaign({
...validatedFields.data,
userId: user.id,
});
revalidateTag('campaigns');
revalidatePath('/campaigns');
return { success: true, campaignId: campaign.id };
} catch (error) {
return {
errors: { _form: ['Failed to create campaign. Please try again.'] },
};
}
}
Client-Side State with React Query
// lib/queries/campaigns.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { getCampaigns, updateCampaignStatus } from '@/lib/api/campaigns';
export function useCampaigns(filters: CampaignFilters = {}) {
return useQuery({
queryKey: ['campaigns', filters],
queryFn: () => getCampaigns(filters),
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 30, // 30 minutes
});
}
export function useUpdateCampaignStatus() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ campaignId, status }: { campaignId: string; status: CampaignStatus }) =>
updateCampaignStatus(campaignId, status),
onSuccess: (_, { campaignId }) => {
// Optimistically update the cache
queryClient.setQueryData(['campaigns'], (old: Campaign[] | undefined) =>
old?.map(campaign =>
campaign.id === campaignId
? { ...campaign, status }
: campaign
)
);
// Revalidate to ensure consistency
queryClient.invalidateQueries({ queryKey: ['campaigns'] });
},
});
}
Section 2 (Frontend Standards) completed. Continue with Section 3 (Backend & API Standards)?