Last updated: Aug 1, 2025, 02:00 PM UTC

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
Email 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)?