• Skip to main content
  • Skip to navigation
  • Skip to search
    Petunia™
    FeaturesPricingIntegrationsAboutContact
    Log inStart free trialSign up
    Loading
    Petunia™

    Reimagining customer communication for the modern business.

    Product

    • Features
    • Pricing
    • Integrations
    • Roadmap
    • What's New

    Resources

    • Help Center
    • Documentation
    • Guides
    • API Reference
    • Community
    • Support

    Company

    • About Us
    • Careers
    • Blog
    • Press
    • Contact

    © 2026 Gray Group International LLC. All rights reserved.·
    Made by gardenpatch 🌱

    Privacy PolicyTerms of ServiceCookie Policy

    Petunia™ is a trademark of Gray Group International LLC. The Petunia name, brand, product design, and content are proprietary. Unauthorized use, imitation, or copying is prohibited.

    Documentation

    ARCHITECTURE

    docs/ARCHITECTURE.md
    Docs homeGuidesSupport
    Quick links
    Start here
    How the docs are organized.
    Environment setup
    Configure env + run locally.
    Unified Inbox
    Inbox concepts & behavior.
    Voice AI setup
    Providers, Twilio, testing.
    Pricing model
    Source-of-truth pricing.
    Operations runbook
    How to operate safely.

    Petunia Platform Architecture

    Version: 2.0.0 Last Updated: 2025-12-12 Status: Living Document - Ideal State Definition

    This document defines the ideal architectural state for Petunia - the "north star" that guides all development decisions. It describes both the current state and the target architecture, serving as the authoritative reference for system design.


    Table of Contents

    1. Executive Summary
    2. Entity Hierarchy & Relationships
    3. Multi-Tenant Architecture
    4. Customer Journey: Contact → Lead → Customer
    5. Database Schema (133+ Models)
    6. Knowledge Architecture (4-Tier)
    7. Async/Queue Architecture
    8. Webhook Idempotency
    9. LLM Cost Tracking
    10. Voice AI Architecture
    11. Billing & Commission System
    12. Authentication & Security
    13. Observability Stack
    14. API Architecture
    15. Data Retention & GDPR
    16. Disaster Recovery
    17. Demo Portal Configuration
    18. Cross-Portal Deduplication
    19. Design Decisions
    20. Migration Roadmap

    Executive Summary

    Petunia is a multi-tenant customer engagement platform providing unified inbox management, AI-powered autoresponders, lead management, voice AI, and review aggregation across 6+ platforms (Yelp, Google, Facebook, Apple Maps, Nextdoor, BBB).

    Core Architecture Principles

    1. Portal-First Multi-Tenancy: Portal is the primary tenant boundary where all stakeholders meet
    2. Company Persistence: Business data (Products, Services) persists across portal lifecycles
    3. Defense-in-Depth Security: 4-layer isolation (Middleware → Service → Query → RLS)
    4. Event-Driven Processing: BullMQ + Database-backed queues with automatic failover
    5. AI Provider Abstraction: Unified interface across Anthropic, OpenAI, Cartesia, ElevenLabs

    Technology Stack

    LayerTechnology
    FrontendNext.js 15 App Router, React 19, TailwindCSS
    BackendNext.js API Routes (415+ routes), Edge Runtime
    DatabasePostgreSQL (Supabase), Prisma ORM
    QueueBullMQ (Redis) + Database fallback
    Vector StorePinecone (Platform), Upstash Vector (Tenant)
    AuthSupabase Auth, TOTP 2FA, Session Rotation
    PaymentsStripe (Subscriptions, Connect, Checkout)
    Voice AICartesia (primary), ElevenLabs, Retell
    ObservabilitySentry, Prometheus, Structured Logging
    Real-timeWebSocket Server, Supabase Realtime

    Entity Hierarchy & Relationships

    Core Entity Chain

    User (Authentication)
      ├── UserCompany ──→ Company (Business Entity)
      │                      ├── Client ──→ Portal (Tenant Workspace)
      │                      │               ├── Business (PortalAIConfig) ← Voice AI settings
      │                      │               ├── Conversations/Messages
      │                      │               ├── Leads/Contacts
      │                      │               └── Reviews/Integrations
      │                      ├── Products (persist across portals)
      │                      └── Services (persist across portals)
      ├── UserPortalAccess ──→ Portal (Role-based access)
      └── UserClient ──→ Client (Legacy relationship)
    

    Key Entity Definitions

    EntityPurposeLifecycle
    UserAuthenticated identity with MFA, preferencesPermanent
    CompanyBusiness organization, billing entityPermanent
    ClientOrganization representation, legacyPermanent
    PortalWorkspace container (petuniaID), tenant boundaryCan be deactivated
    BusinessPortal AI config (voice settings, greeting)Tied to Portal
    ContactCustomer record with purchase historyPortal-scoped
    LeadSales opportunity with pipeline stagesPortal-scoped

    Ideal Entity Relationships (Target State)

    // Target: Clean foreign key relationships
    model Company {
      id        String    @id
      clients   Client[]  // One Company → Many Clients
      products  Product[] // Products persist at Company level
      services  Service[] // Services persist at Company level
    }
    
    model Client {
      id        String   @id
      companyId String   // Required FK (currently broken: default "default")
      Company   Company  @relation(fields: [companyId], references: [id])
      portals   Portal[] // One Client → Many Portals (typically 1 active)
    }
    
    model Portal {
      id          String   @id
      petuniaID   String   @unique // Human-readable identifier
      clientId    String   // Required FK to Client
      Client      Client   @relation(fields: [clientId], references: [id])
      AIConfig    PortalAIConfig? // 1:1 with voice AI settings
      isDemoPortal Boolean @default(false)
    }
    

    Join Table Schemas

    // User ↔ Company (many-to-many with role)
    model UserCompany {
      id        String   @id @default(cuid())
      userId    String
      companyId String
      role      String   @default("member") // owner, admin, member
      createdAt DateTime @default(now())
    
      User      User     @relation(fields: [userId], references: [id], onDelete: Cascade)
      Company   Company  @relation(fields: [companyId], references: [id], onDelete: Cascade)
    
      @@unique([userId, companyId])
      @@index([companyId])
    }
    
    // User ↔ Portal (many-to-many with role and permissions)
    model UserPortalAccess {
      id        String   @id @default(cuid())
      userId    String
      portalId  String
      role      String   @default("viewer") // admin, editor, viewer
      createdAt DateTime @default(now())
    
      User      User     @relation(fields: [userId], references: [id], onDelete: Cascade)
      Portal    Portal   @relation(fields: [portalId], references: [id], onDelete: Cascade)
    
      @@unique([userId, portalId])
      @@index([portalId])
    }
    
    // Conversation ↔ Contact/Lead (many-to-many participants)
    model ConversationParticipant {
      id             String   @id @default(cuid())
      conversationId String
      contactId      String?  // Either contact or lead
      leadId         String?
      joinedAt       DateTime @default(now())
      leftAt         DateTime?
    
      Conversation   Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
      Contact        Contact?     @relation(fields: [contactId], references: [id])
      Lead           Lead?        @relation(fields: [leadId], references: [id])
    
      @@index([conversationId])
      @@index([contactId])
      @@index([leadId])
    }
    

    Source of Truth: See /prisma/schema.prisma for complete, authoritative schema definitions.

    Current Issues (See ARCHITECTURE_PLAN.md)

    • P0-1: Client.companyId has no FK constraint, defaults to "default"
    • P0-8: Portal.clientId FK missing for direct linkage
    • P0-9: Portal→Client→Company chain broken

    Multi-Tenant Architecture

    Tenant Isolation Model

    ┌─────────────────────────────────────────────────────────────┐
    │                    ISOLATION LAYERS                         │
    ├─────────────────────────────────────────────────────────────┤
    │ Layer 1: MIDDLEWARE                                         │
    │   • Session validation (Supabase Auth)                      │
    │   • Rate limiting (Upstash Redis)                           │
    │   • CSRF protection (constant-time comparison)              │
    ├─────────────────────────────────────────────────────────────┤
    │ Layer 2: SERVICE (TenantAccessService)                      │
    │   • getTenantContext() validates portal access              │
    │   • buildPortalScope() adds WHERE clauses                   │
    │   • validateMessageAccess() per-resource checks             │
    ├─────────────────────────────────────────────────────────────┤
    │ Layer 3: QUERY (Prisma)                                     │
    │   • All queries include portalId filter                     │
    │   • No cross-portal data leakage                            │
    ├─────────────────────────────────────────────────────────────┤
    │ Layer 4: DATABASE (PostgreSQL RLS)                          │
    │   • Row-Level Security policies on sensitive tables         │
    │   • Service role bypass for background jobs                 │
    └─────────────────────────────────────────────────────────────┘
    

    Key Files

    • /proxy.ts - 1060 lines, session validation
    • /lib/services/tenant-access.ts - Core tenant context
    • /lib/utils/portalAccess.ts - Portal listing and roles
    • /supabase/migrations/20251205_apply_core_rls_policies.sql - RLS policies

    Access Validation Flow

    // Every API request follows this pattern:
    1. Middleware validates session
    2. getTenantContext(user, petuniaID) validates access
    3. buildPortalScope(context) returns { portalId: string }
    4. Query includes portalId filter
    5. RLS provides database-level backup
    

    Platform Admin Bypass

    • Stored in Admin table with isPlatformAdmin: true
    • Bypasses UserPortalAccess validation
    • 5-minute cache TTL for performance
    • No email-based hardcoding (database-driven only)

    Customer Journey: Contact → Lead → Customer

    Journey Model

    CONTACT                    LEAD                      CUSTOMER
    ┌─────────────┐           ┌─────────────┐           ┌─────────────┐
    │ name        │           │ contactId   │──1:1────→ │ totalValue  │
    │ email       │◄──────────│ score       │           │ customerSince│
    │ phone       │  optional │ journeyStage│           │ lastPurchase │
    │ clientId    │           │ pipelineId  │           │ (calculated) │
    │ companyId   │           │ stageId     │           │              │
    └─────────────┘           └─────────────┘           └─────────────┘
          │                         │
          │                         ▼
          │                  ┌─────────────┐
          │                  │ Pipeline    │
          │                  │ Stages      │
          │                  │ (kanban)    │
          │                  └─────────────┘
          │
          ▼
    ┌─────────────┐
    │Conversation │
    │ Messages    │
    │ (history)   │
    └─────────────┘
    

    Schema Definition

    model Contact {
      id               String    @id
      name             String
      email            String?
      phone            String?
      clientId         String    // Required: belongs to Client
      companyId        String?   // Optional: Company scope
    
      // Customer graduation fields
      totalValue       Float     @default(0)      // Lifetime value
      lastPurchaseDate DateTime?                   // Most recent purchase
      customerSince    DateTime?                   // When became customer
    
      // Relationships
      Lead             Lead?     // Optional 1:1 to Lead
      Conversations    Conversation[]
      CallRecords      CallRecord[]
    }
    
    model Lead {
      id              String    @id
      contactId       String?   @unique          // Links to Contact
      Contact         Contact?  @relation(...)
    
      // Scoring
      score           Int       @default(0)
      journeyStage    String?   // new, engaged, qualified, customer
    
      // Pipeline
      pipelineId      String?
      pipelineStageId String?
      Pipeline        LeadPipeline?
      PipelineStage   LeadPipelineStage?
    
      // Soft delete for merging
      isDeleted       Boolean   @default(false)
    }
    

    Pipeline Stages

    New → Contacted → Qualified → Proposal → Negotiation → Won/Lost
    

    Customer Graduation

    A Contact becomes a Customer when:

    1. totalValue > 0 (has made a purchase)
    2. customerSince is set (graduation date)
    3. Journey stage updated to "customer"

    Database Schema (133+ Models)

    Model Categories

    CategoryCountKey Models
    Auth & Identity5User, Account, Session, Admin, VerificationToken
    User Relationships13UserCompany, UserPortalAccess, UserPreference, OnboardingData
    Client & Company3Client, Company, CompanySettings
    Portal & Business3Portal, PortalLocation, Business
    Contacts & Leads14Contact, Lead, LeadPipeline, LeadPipelineStage, LeadScore
    Conversations9Conversation, Message, CallRecord, ConversationParticipant
    Facebook8FacebookConnection, FacebookMessage, FacebookReview
    Google/Apple6GoogleConnection, GoogleReview, AppleMapsReview
    Yelp8YelpConnection, YelpMessage, YelpReview, YelpCompetitor
    Nextdoor/BBB6NextdoorConnection, BbbConnection, reviews
    Integrations5Connection, ConnectionIntegration, CallLogSyncJob
    Autoresponder7AutoresponderSettings, KnowledgeBaseEntry, sequences
    Voice3VoiceTouchpoint, PhoneNumberMapping, MessagingProvider
    Products/Sales5Product, Service, Quote, ConversionLink
    Commission8CommissionRule, CommissionTransaction, CommissionEvent
    Billing6CreditWallet, Subscription, Invoice, WalletTransaction
    Analytics6AnalyticsRollupDaily, PortalAnalyticsSummary, TTFV
    Audit5AuditLog, ErrorLog, SettingsAuditLog
    Feature Flags3FeatureFlag, FeatureFlagClientOverride
    Platform4PlatformTrialConfig, Template, DemoTelemetry

    Critical Model Issues

    ModelIssuePriority
    Client.companyIdString default "default", no FKP0
    Lead.clientIdOptional, no FK relationP4
    BusinessShould be renamed to PortalAIConfigP2
    Product/ServicebusinessId should be companyIdP1

    Knowledge Architecture (4-Tier)

    Tier Overview

    ┌─────────────────────────────────────────────────────────────┐
    │ TIER 1: PLATFORM KNOWLEDGE (Pinecone)                       │
    │ • Petunia's brain - how the platform works                  │
    │ • Documentation, help articles, features                    │
    │ • Index: platform-knowledge                                 │
    │ • Shared across all tenants                                 │
    └─────────────────────────────────────────────────────────────┘
                                  ▼
    ┌─────────────────────────────────────────────────────────────┐
    │ TIER 2: COMPANY KNOWLEDGE (Upstash Vector)                  │
    │ • Business offerings, products, services                    │
    │ • Persists when client gets new portal                      │
    │ • Namespace: company-{companyId}                            │
    │ • Shared across portals within company                      │
    └─────────────────────────────────────────────────────────────┘
                                  ▼
    ┌─────────────────────────────────────────────────────────────┐
    │ TIER 3: PORTAL KNOWLEDGE (Upstash Vector)                   │
    │ • Workspace-specific context                                │
    │ • Team members, integrations, workflows                     │
    │ • Namespace: portal-{portalId}                              │
    │ • Portal-scoped, resets with new portal                     │
    └─────────────────────────────────────────────────────────────┘
                                  ▼
    ┌─────────────────────────────────────────────────────────────┐
    │ TIER 4: CONVERSATION KNOWLEDGE (In-Memory/Redis)            │
    │ • Real-time conversation context                            │
    │ • Recent messages, current topic, sentiment                 │
    │ • Ephemeral, session-scoped                                 │
    └─────────────────────────────────────────────────────────────┘
    

    Retrieval Priority

    async function retrieveKnowledge(query: string, context: TenantContext) {
      // 1. Check conversation cache (fastest)
      const conversationContext = await getConversationContext(context.conversationId);
    
      // 2. Query portal-specific knowledge
      const portalKnowledge = await upstash.query({
        namespace: `portal-${context.portalId}`,
        topK: 3
      });
    
      // 3. Query company knowledge (persists across portals)
      const companyKnowledge = await upstash.query({
        namespace: `company-${context.companyId}`,
        topK: 3
      });
    
      // 4. Query platform knowledge (Petunia docs)
      const platformKnowledge = await pinecone.query({
        index: 'platform-knowledge',
        topK: 2
      });
    
      return mergeAndRank([conversationContext, portalKnowledge, companyKnowledge, platformKnowledge]);
    }
    

    Failure Handling

    ScenarioBehaviorFallback
    Pinecone down/timeout (>3s)Skip platform tier, continue with tenant tiersLog to Sentry with knowledge.retrieval.failed tag
    Upstash Vector timeout (>3s)Skip that tier (company or portal), continueUse cached results if available, log failure
    LLM returns garbage/invalidValidate response structure, retry onceReturn cached response or escalate to human agent
    All vector stores failProceed with conversation context onlyAlert ops, degrade to keyword matching
    Complete knowledge failureReturn graceful error to userEscalate to human immediately, notify on-call

    All failures logged to Sentry with knowledge.retrieval.failed tag and include tier, latency, and error details.

    Key Files

    • /lib/services/ai/knowledge-ai.ts - Knowledge retrieval
    • /lib/services/voice/voiceKnowledgeConnector.ts - Voice AI integration

    Async/Queue Architecture

    Queue System Overview

    ┌─────────────────────────────────────────────────────────────┐
    │ PRIMARY: BullMQ (Redis)                                     │
    │ • ttfv-calculation, analytics-rollup, message-processing    │
    │ • notification, competitor-analysis, email/sms-delivery     │
    │ • webhook-delivery                                          │
    │ • Default: 3 retries, exponential backoff (1s base)         │
    │ • Retention: completed 1hr, failed 24hr                     │
    └─────────────────────────────────────────────────────────────┘
                                  │
                        (if Redis unavailable)
                                  ▼
    ┌─────────────────────────────────────────────────────────────┐
    │ FALLBACK: Database-Backed Queues (Prisma)                   │
    │ • YelpReviewSyncJob, GoogleReviewSyncJob, CallLogSyncJob    │
    │ • States: pending → running → completed | dead_letter       │
    │ • 5 retries, exponential backoff (60s base, 12hr max)       │
    │ • Stuck job recovery: 15-minute threshold                   │
    └─────────────────────────────────────────────────────────────┘
    

    Cron Jobs (Vercel)

    EndpointSchedulePurpose
    /api/health/edgeEvery 15 minHealth check
    /api/cron/yelp-reviewsEvery 15 minYelp review sync
    /api/cron/google-reviewsEvery 15 minGoogle review sync
    /api/cron/message-statusEvery 5 minMessage delivery status
    /api/cron/orphan-cleanupEvery 6 hoursOrphaned record cleanup
    /api/cron/dunningDaily 8 AMPayment retry
    /api/cron/usage-warningsEvery 15 minUsage threshold alerts
    /api/cron/lead-settlementDaily 6 AMLead settlement
    /api/cron/presence-cleanupEvery 5 minStale presence cleanup
    /api/cron/data-retentionDaily 3 AMGDPR data retention

    Retry Policies

    SystemMax AttemptsBackoffMax Delay
    BullMQ (default)3Exponential (1s)N/A
    Review Sync Jobs5Exponential (60s)12 hours
    Webhook Delivery2Exponential (2x)30 seconds
    withRetry utility3Exponential (2x)Configurable

    Key Files

    • /lib/queue/bullmq-queue.ts - BullMQ implementation
    • /lib/jobs/yelpReviewSyncJobService.ts - Database job pattern
    • /vercel.json - Cron configuration

    Webhook Idempotency

    Signature Verification by Provider

    ProviderHeaderAlgorithm
    Yelpx-yelp-signatureHMAC-SHA256
    Facebook/Instagramx-hub-signature-256HMAC-SHA256
    Google (Pub/Sub)Authorization: BearerJWT
    Twiliox-twilio-signatureHMAC-SHA1
    MailgunProvider-specificHMAC
    Sentrysentry-hook-signatureHMAC-SHA256 + timestamp

    Deduplication Strategy

    // Current: Database-based deduplication
    const existingMessage = await prisma.message.findFirst({
      where: { externalId: messageId }
    });
    if (existingMessage) {
      return { success: true, isDuplicate: true };
    }
    
    // Target: Add unique constraint
    @@unique([externalId, providerId, portalId])
    

    Critical Gaps

    • No Redis/cache-based idempotency - relies on database lookup
    • Race condition vulnerability - no atomic check+insert
    • No unique constraint on (externalId, providerId)
    • Missing idempotency key header support

    Key Files

    • /app/api/webhooks/*/route.ts - Provider handlers
    • /tests/app/api/webhooks/*-signature.test.ts - Signature tests

    LLM Cost Tracking

    Platform Usage Tracker

    // File: /lib/services/platform-usage-tracker.ts (1,860 lines)
    
    interface UsageTracking {
      clientId: string;
      provider: 'anthropic' | 'openai' | 'elevenlabs' | 'cartesia';
      keySource: 'platform' | 'client';
      operationalContext: 'demo' | 'client-facing' | 'internal';
      callCount: number;
      estimatedCost: number;
      metadata: {
        inputTokens?: number;
        outputTokens?: number;
        characters?: number;
        audioMinutes?: number;
      };
    }
    

    Provider Pricing (Nov 2025)

    ProviderTypeCost
    Anthropic (Claude)Tokens$3/1M input, $15/1M output
    OpenAI (GPT-4 Turbo)Tokens$10/1M input, $30/1M output
    ElevenLabs (Voice)Characters$0.30/1k characters
    CartesiaMinutes$0.05/minute
    RetellMinutes$0.10/minute
    Twilio (SMS)Messages$0.0075/message

    Budget Alerts

    LevelThresholdAction
    Safe0-79%No alert
    Warning80-89%Email notification
    Critical90-99%Email + SMS + auto-pause option
    Exceeded100%+Services paused

    Model Selection & Fallback

    // Primary: Claude 3 Haiku
    // Fallback chain:
    1. Anthropic (Claude) if available
    2. OpenAI (GPT-4 Turbo) if Anthropic fails
    3. Regex/pattern matching if both fail
    // Confidence: 0.95 (AI) → 0.60 (regex)
    

    Voice AI Architecture

    Provider Hierarchy

    ┌─────────────────────────────────────────────────────────────┐
    │ CARTESIA (Primary)                                          │
    │ • Sonic 3.0 TTS - ultra-realistic voices                    │
    │ • Cartesia Line - real-time conversational AI               │
    │ • Cartesia Ink - STT (Ink-Whisper model)                    │
    └─────────────────────────────────────────────────────────────┘
                        (automatic failover)
                                  ▼
    ┌─────────────────────────────────────────────────────────────┐
    │ ELEVENLABS (Fallback)                                       │
    │ • v3 TTS and Conversational AI                              │
    │ • Multiple voice options (Rachel, Bella, Dorothy)           │
    │ • 30-minute cache for frequent phrases                      │
    └─────────────────────────────────────────────────────────────┘
                                  │
                        (client integration only)
                                  ▼
    ┌─────────────────────────────────────────────────────────────┐
    │ RETELL (Client Integration)                                 │
    │ • Full telephony support                                    │
    │ • Agent creation and management                             │
    │ • Not platform default                                      │
    └─────────────────────────────────────────────────────────────┘
    

    Inbound Call Flow

    1. Incoming call → handleInboundCall()
    2. Agent config resolution (5-min cache):
       a. Business.voiceAIAgentId (custom)
       b. Platform env var (CARTESIA_AGENT_ID)
       c. Default: 'default-agent'
    3. Transfer number resolution:
       a. Business.voiceAITransferNumber
       b. RINGCENTRAL/TWILIO_PHONE_NUMBER
    4. Greeting generation:
       a. Business.voiceAIGreeting (custom)
       b. voiceKnowledgeConnector.generateInboundGreeting()
       c. Default: "Thank you for calling..."
    5. Response: TwiML with greeting + action
    

    Business Model Voice Config (Target: PortalAIConfig)

    model Business {  // TODO: Rename to PortalAIConfig
      portalId            String?  @unique
      voiceAIEnabled      Boolean  @default(true)
      voiceAIAgentId      String?
      voiceAITransferNumber String?
      voiceAIGreeting     String?
      voiceAIProvider     String?  @default("cartesia")
    
      // Move to Company:
      products            Product[]  // Should be Company.products
      services            Service[]  // Should be Company.services
    }
    

    Key Files

    • /lib/services/voice/index.ts - Provider initialization
    • /lib/services/voice/unifiedVoiceService.ts - Unified interface
    • /lib/services/voice/inboundCallHandler.ts - Call flow
    • /lib/services/voice/voiceKnowledgeConnector.ts - Knowledge integration

    Billing & Commission System

    Wallet Model

    ┌─────────────────────────────────────────────────────────────┐
    │ CREDIT WALLET (Prepaid)                                     │
    │ • leadCredits - Prepaid lead purchases                      │
    │ • smsCredits - SMS message credits                          │
    │ • voiceCredits - Voice AI minutes                           │
    │ • aiCredits - AI/LLM token credits                          │
    │ • Trust ladder: immediate → monthly billing                 │
    └─────────────────────────────────────────────────────────────┘
    

    Commission System

    Lead → Appointment → Closure → Commission
      │         │           │           │
      ▼         ▼           ▼           ▼
     2%        5%         7%      Revenue-based
    

    Stripe Integration

    • Subscriptions: Base monthly fee
    • Stripe Connect: OAuth payment attribution
    • Webhooks: Wallet top-ups, payment failures
    • Customer Portal: Self-service billing management

    Key Files

    • /docs/features/BILLING_SYSTEM.md - Comprehensive documentation
    • /lib/services/platform-usage-tracker.ts - Usage tracking
    • /app/api/payments/* - Payment API routes

    Authentication & Security

    Auth Stack

    ┌─────────────────────────────────────────────────────────────┐
    │ SUPABASE AUTH                                               │
    │ • SSR-compatible cookie-based authentication                │
    │ • OAuth PKCE flow (Google, Facebook)                        │
    │ • Email verification required                               │
    └─────────────────────────────────────────────────────────────┘
                                  +
    ┌─────────────────────────────────────────────────────────────┐
    │ SESSION MANAGEMENT                                          │
    │ • JWT-based with HMAC-SHA256 signing                        │
    │ • Automatic rotation (24hr interval)                        │
    │ • Force rotation on privilege change                        │
    │ • Max session age: 7 days                                   │
    └─────────────────────────────────────────────────────────────┘
                                  +
    ┌─────────────────────────────────────────────────────────────┐
    │ 2FA (TOTP)                                                  │
    │ • HMAC-SHA1, 6-digit codes, 30-second period                │
    │ • ±30 second time window tolerance                          │
    │ • 20-byte (160-bit) secrets                                 │
    └─────────────────────────────────────────────────────────────┘
    

    Rate Limiting

    TierLimitPurpose
    auth50/15minLogin/signup
    otp3/15min2FA attempts
    api100/minGeneral API
    heavy10/minResource-intensive
    webhook1000/minWebhook handlers

    Security Headers

    • CSP: Restrictive default-src, specific allowlists
    • HSTS: 1 year, includeSubDomains, preload
    • X-Frame-Options: DENY
    • Permissions-Policy: Restrictive camera/mic/geo

    Encryption

    • At-rest: AES-256-GCM with PBKDF2 (100K iterations)
    • Tenant isolation: HKDF-derived keys per tenant
    • Sensitive fields: Auto-detection and encryption

    Key Files

    • /proxy.ts - Auth validation, rate limiting
    • /lib/auth/sessionRotation.ts - Session management
    • /lib/utils/totp.ts - TOTP implementation
    • /docs/features/ENCRYPTION.md - Encryption documentation

    Observability Stack

    Three Pillars

    ┌─────────────────────────────────────────────────────────────┐
    │ LOGGING (Structured)                                        │
    │ • 19 pre-configured module loggers                          │
    │ • JSON context formatting                                   │
    │ • Performance timers                                        │
    │ • File: /lib/utils/logging.ts                               │
    └─────────────────────────────────────────────────────────────┘
    
    ┌─────────────────────────────────────────────────────────────┐
    │ METRICS (Prometheus)                                        │
    │ • HTTP: request_duration, requests_total, active_connections│
    │ • Database: query_duration, connection_pool                 │
    │ • Queue: jobs_total, job_duration, jobs_active/waiting/failed│
    │ • Business: leads_created, reviews_processed                │
    │ • SLO: webhook/payment request duration                     │
    │ • File: /lib/services/monitoring/prometheusMetrics.ts       │
    └─────────────────────────────────────────────────────────────┘
    
    ┌─────────────────────────────────────────────────────────────┐
    │ TRACING (Sentry)                                            │
    │ • 10% sample rate (production)                              │
    │ • Prisma database query tracking                            │
    │ • Data scrubbing (passwords, tokens, API keys)              │
    │ • Profiling: 10% sample rate                                │
    │ • Files: /sentry.*.config.ts                                │
    └─────────────────────────────────────────────────────────────┘
    

    Error Tracking

    • Server-side: Queue-based batch processing (50 errors/5 seconds)
    • Client-side: Browser capture with rate limiting
    • Alerting: Slack, Discord, Email with threshold-based triggers
    • Storage: ErrorLog table with fingerprint deduplication

    Health Endpoints

    EndpointPurpose
    /api/healthFull component health
    /api/metricsPrometheus metrics
    /api/diagnosticsSystem diagnostics
    /api/pingLiveness probe
    /api/readyReadiness probe

    API Architecture

    Route Structure

    /app/api/
    ├── auth/              # Authentication (signin, signup, 2fa)
    ├── users/             # User management
    ├── inbox/             # Conversations, messages
    ├── leads/             # Lead management
    ├── contacts/          # Contact management
    ├── reviews/           # Review aggregation
    ├── connections/       # Integration management
    ├── voice/             # Voice AI endpoints
    ├── communications/    # Voice calls, SMS
    ├── payments/          # Billing, subscriptions
    ├── platform/          # Platform admin
    ├── settings/          # User/portal settings
    ├── webhooks/          # External webhooks
    ├── cron/              # Scheduled jobs
    └── health/            # Health checks
    

    API Response Standard

    interface ApiResponse<T> {
      success: boolean;
      data?: T;
      error?: {
        code: string;
        message: string;
        details?: unknown;
      };
      meta?: {
        timestamp: string;
        requestId: string;
        pagination?: {
          page: number;
          pageSize: number;
          total: number;
          hasMore: boolean;
        };
      };
    }
    

    Versioning Strategy

    • Current: Per-route version metadata (@version JSDoc)
    • OpenAPI: Static specs at /docs/openapi.yaml
    • Target: URL-based versioning (/api/v1/) for major changes
    • Deprecation: Not yet implemented (needs headers)

    Key Files

    • /docs/openapi.yaml - API specification
    • /lib/types/api.ts - Response types

    Data Retention & GDPR

    Retention Policies

    Data TypeRetentionAction
    Behavior analytics90 daysHard delete
    Session data30 daysHard delete
    ML training data180 daysHard delete
    Audit logs365 daysArchive
    Profile responsesIndefiniteAnonymize
    Aggregated metricsIndefiniteKeep

    GDPR Compliance

    // Implemented: /lib/services/gdpr-compliance.ts
    - exportUserData()      // Article 20 - Data Portability
    - deleteUserData()      // Article 17 - Right to Erasure
    - anonymizeUserData()   // Alternative to deletion
    - trackConsent()        // Consent management
    - generatePrivacyReport()
    

    Audit Trail

    • AuditLog table: 85+ event types
    • Auth, data access, security, admin actions
    • IP address, user agent, metadata
    • 365-day retention

    Key Files

    • /lib/compliance/gdpr.ts - GDPR/CCPA/HIPAA classes
    • /lib/services/gdpr-compliance.ts - Implementation
    • /app/api/cron/data-retention/route.ts - Cleanup job

    Disaster Recovery

    Current State

    AspectStatus
    Database backupsSupabase automatic
    RTO/RPO targetsNOT DOCUMENTED
    Restore proceduresNOT DOCUMENTED
    Cross-region failoverNOT IMPLEMENTED
    Backup testingNOT SCHEDULED

    Target State

    AspectTarget
    RTO4 hours
    RPO1 hour
    Backup testingQuarterly drills
    Cross-regionSecondary region for failover

    Rollback Procedures

    Documented in /docs/PRE_DEPLOYMENT_CHECKLIST.md:

    1. Vercel rollback via CLI/dashboard
    2. Git revert to previous commit
    3. Emergency database fix procedures

    Key Files

    • /docs/PRE_DEPLOYMENT_CHECKLIST.md - Deployment procedures
    • /scripts/db-safety-check.sh - Migration safety
    • /docs/security/incident-log.md - Incident tracking

    Demo Portal Configuration

    Canonical Identifier

    export const DEMO_PETUNIA_ID = 'PID-DEMO-001';
    export const DEMO_COMPANY_ID = 'demo-company';
    export const DEMO_PORTAL_ID = 'demo-portal';
    

    Detection Methods

    // Method 1: PetuniaID check (preferred)
    isDemoPortalByPetuniaID(petuniaID) // petuniaID === 'PID-DEMO-001'
    
    // Method 2: Boolean flag
    portal.isDemoPortal === true
    
    // Method 3: ID check
    isDemoPortal(id) // matches demo-company, demo-portal, or PID-DEMO-001
    

    Demo Data Isolation

    • All metrics queries exclude isDemoPortal: false
    • Demo portals get simulated data from /lib/services/monitoring/demoMetrics.ts
    • Storage isolated with demo_ prefix
    • Features restricted: data export, integration setup, billing

    Demo User

    • Email: demo@example.com
    • Password: password123
    • Flag: isDemoAdmin: true
    • Session: 4 hours
    • Data reset: 24 hours

    Key Files

    • /docs/DEMO.md - Demo documentation
    • /lib/demo/DemoDataIsolation.ts - Isolation logic
    • /lib/services/monitoring/demoMetrics.ts - Demo metrics

    Cross-Portal Deduplication

    Current Implementation

    Scope: Single portal/client only (NO cross-portal deduplication)

    // Detection algorithm (basic matching)
    function isDuplicate(a, b): boolean {
      // Email matching (case-insensitive)
      if (normalizeEmail(a.email) === normalizeEmail(b.email)) return true;
    
      // Phone matching (digits-only)
      if (normalizePhone(a.phone) === normalizePhone(b.phone)) return true;
    
      // Name matching (when contact info missing)
      if (namesMatch(a.name, b.name) && (!a.email || !a.phone)) return true;
    
      return false;
    }
    

    Merge Operation

    // File: /lib/services/lead/leadService.prisma.ts
    async mergeLeads(businessId, primaryLeadId, duplicateLeadIds) {
      // 1. Move activities to primary
      // 2. Move conversation participants
      // 3. Soft delete duplicates (isDeleted = true)
      // 4. Log merge activity
    }
    

    Limitations

    • No database-level unique constraints on email/phone
    • No cross-portal contact linking
    • Race condition possible during concurrent creation
    • No automatic duplicate detection triggers

    Target State

    • Add @@unique([email, clientId]) constraint
    • Add @@unique([phone, clientId]) constraint
    • Consider cross-company contact linking for enterprise

    Design Decisions

    Why Portal-First Architecture?

    Decision: Portal is the primary tenant boundary, not Company.

    Rationale:

    1. Fresh Start Capability: If a company needs a fresh start, they get a new Portal (old deactivated)
    2. Workspace Isolation: Each Portal is a complete workspace with its own integrations, team, settings
    3. AI Config Binding: Voice AI configuration is Portal-specific (different numbers, greetings, agents)
    4. Typical Usage: 1 Client = 1 active Portal (never 2 active simultaneously)

    Consequence: Products/Services should persist at Company level, AI config at Portal level.

    Why Business Model Split?

    Decision: Split Business into PortalAIConfig (stays at Portal) and move Products/Services to Company.

    Rationale:

    1. Products/Services persist: When client gets new portal, their product catalog shouldn't reset
    2. AI config is workspace-specific: Different portals might have different voice greetings, agents
    3. Clean separation: Business data vs. operational configuration

    Why BullMQ + Database Fallback?

    Decision: Use BullMQ as primary queue with database-backed fallback.

    Rationale:

    1. Resilience: System continues without Redis using database as queue
    2. Feature parity: Both systems support retry, backoff, dead letter
    3. Observability: Both have metrics and health checks
    4. Graceful degradation: Production continues even if Redis fails

    Why 4-Tier Knowledge Architecture?

    Decision: Separate knowledge into Platform, Company, Portal, and Conversation tiers.

    Rationale:

    1. Platform knowledge: Petunia's capabilities, shared across all
    2. Company knowledge: Products/services persist across portals
    3. Portal knowledge: Workspace-specific context, team, integrations
    4. Conversation: Real-time context, ephemeral

    Why Prepaid Wallet for Billing?

    Decision: Prepaid wallet model instead of post-paid.

    Rationale:

    1. No bill shock: Customers know exactly what they'll spend
    2. Trust ladder: New customers start immediate billing, graduate to monthly
    3. Cash flow: Revenue collected upfront
    4. Simplicity: Single wallet for all usage types

    Affiliate Partner Model

    Decision: Deferred to Phase 2.

    Target Implementation: UserAffiliate join table with commission tracking, linking Users to affiliate program with referral codes, commission rates, and payout tracking.

    Rationale: Core entity relationships (User→Company→Client→Portal) must be stabilized before adding affiliate layer. Will be implemented after P0-P1 migrations complete.


    Migration Roadmap

    See ARCHITECTURE_PLAN.md for the complete 50+ item migration plan with priorities P0-P5.

    Quick Reference

    PriorityCountFocus
    P09Critical: FK constraints, data integrity, encryption
    P110High: Rate limiting, model migration, indexes
    P28Medium: Type safety, model renaming, access control
    P38Standard: Cleanup, standardization, observability
    P48Lower: Schema refinement, validation, optimization
    P55Documentation: Webhook, monitoring, voice AI docs

    Appendix: File Reference

    Core Architecture Files

    CategoryKey Files
    Schema/prisma/schema.prisma
    Auth/proxy.ts, /lib/auth/*
    Tenant/lib/services/tenant-access.ts
    Queue/lib/queue/bullmq-queue.ts, /lib/jobs/*
    Voice/lib/services/voice/*
    Billing/lib/services/platform-usage-tracker.ts
    GDPR/lib/services/gdpr-compliance.ts
    Demo/lib/demo/DemoDataIsolation.ts
    Observability/lib/utils/logging.ts, /lib/services/monitoring/*

    Documentation

    DocumentPurpose
    /docs/ARCHITECTURE.mdThis document (ideal state)
    /docs/ARCHITECTURE_PLAN.mdMigration todos
    /docs/DEMO.mdDemo portal
    /docs/features/BILLING_SYSTEM.mdBilling
    /docs/features/ENCRYPTION.mdSecurity
    /TESTING.mdTest guidelines

    This document is the authoritative reference for Petunia's architecture. All development decisions should align with the patterns and principles documented here.

    On this page
    Table of ContentsExecutive SummaryCore Architecture PrinciplesTechnology StackEntity Hierarchy & RelationshipsCore Entity ChainKey Entity DefinitionsIdeal Entity Relationships (Target State)Join Table SchemasCurrent Issues (See ARCHITECTURE_PLAN.md)Multi-Tenant ArchitectureTenant Isolation ModelKey FilesAccess Validation FlowPlatform Admin BypassCustomer Journey: Contact → Lead → CustomerJourney ModelSchema DefinitionPipeline StagesCustomer GraduationDatabase Schema (133+ Models)Model CategoriesCritical Model IssuesKnowledge Architecture (4-Tier)Tier OverviewRetrieval PriorityFailure HandlingKey FilesAsync/Queue ArchitectureQueue System OverviewCron Jobs (Vercel)Retry Policies