Skip to content

Cavai Platform Data Flow: From Configuration to Rendering

Executive Summary

The Cavai platform consists of three main components that work together to create interactive advertising experiences:

  1. Application-Frontend - Configuration interface where users design their ads
  2. Creative-Engine - Rendering library that displays the final ads
  3. Application-Backend - Data persistence and API layer

This document traces the complete journey of data from initial configuration to final rendering, using a concrete example of styling a phone input field.

Architecture Overview

┌─────────────────────┐    Configuration    ┌─────────────────────┐    Payload     ┌─────────────────────┐
│                     │    Data Flow        │                     │    Transfer    │                     │
│ Application-Frontend│ ─────────────────→  │ Application-Backend │ ────────────→  │  Creative-Engine    │
│                     │                     │                     │                │                     │
│ • Vue.js + Vuex     │                     │ • AdonisJS + Postgres│               │ • Vue.js Library    │
│ • Configuration UI  │                     │ • JSON Storage      │               │ • Component Rendering│
│ • Type Definitions  │                     │ • API Endpoints     │               │ • Style Processing  │
└─────────────────────┘                     └─────────────────────┘                └─────────────────────┘

Concrete Example: Phone Input Field Styling

Let's trace how a phone input field gets configured, stored, and rendered:

Step 1: Component Definition (Application-Frontend)

Type Definition (types.ts)

typescript
export interface FormInputProperties extends BlockBase {
  blockType: 'formInputProperties'
  type: 'tel' | 'text' | 'email' | 'number' | 'date' | 'select' | 'checkbox' | 'radio'
  label: string
  placeholder?: string
  required?: boolean
  validation?: {
    minLength?: number
    maxLength?: number
    pattern?: string
    phoneFormat?: 'international' | 'national' | 'e164' | 'custom'
    country?: Country
    customErrorMessage?: string
  }
  style?: FontStyle & SizeStyle & PaddingStyle & MarginStyle
  countryCodeStyle?: {
    showIcon?: boolean
    backgroundSettings?: BackgroundSettings
    style?: FontStyle
  }
}

export interface Country {
  code: string    // '+47'
  flag: string    // '🇳🇴'
  name: string    // 'Norway'
}

Default Values (defaults.ts)

typescript
export const phoneInputDefaults = (): FormInputProperties => ({
  blockType: 'formInputProperties',
  blockName: 'formInputProperties-phone-1',
  type: 'tel',
  label: 'Phone Number',
  placeholder: 'Enter your phone number',
  required: false,
  order: 1,
  validation: {
    phoneFormat: 'international',
    country: { code: '+47', flag: '🇳🇴', name: 'Norway' },
    minLength: 8,
    maxLength: 15,
    customErrorMessage: 'Please enter a valid phone number'
  },
  style: {
    fontSize: '14px',
    fontFamily: 'Arial, sans-serif',
    color: '#333333',
    width: '100%',
    height: '40px',
    paddingLeft: '12px',
    paddingRight: '12px',
    paddingTop: '8px',
    paddingBottom: '8px'
  },
  countryCodeStyle: {
    showIcon: true,
    backgroundSettings: { mode: 'color', color: '#f8f9fa' },
    style: {
      fontSize: '14px',
      fontFamily: 'Arial, sans-serif',
      color: '#495057'
    }
  }
})

Step 2: Configuration UI (Application-Frontend)

Configuration Component (PhoneInputConfiguration.vue)

vue
<template>
  <BaseInputConfiguration
    :block-data="blockData"
    :path="path"
    v-bind="$attrs"
    v-on="$listeners"
  >
    <template #validation-section>
      <ValidationSection
        :validation="blockData?.validation || {}"
        input-type="tel"
        :block-name="blockData?.blockName || 'phone'"
        @update:validation="updateValue($event, 'validation')"
      />
      
      <!-- Phone Format Selection -->
      <InputSection>
        <InputLabel label="Phone Format" />
        <select
          :value="blockData.validation?.phoneFormat || 'international'"
          @change="updateValue($event.target.value, 'validation.phoneFormat')"
          class="form-select"
        >
          <option value="international">International (+47 123 45 678)</option>
          <option value="national">National (123 45 678)</option>
          <option value="e164">E.164 (+4712345678)</option>
          <option value="custom">Custom Pattern</option>
        </select>
      </InputSection>
    </template>

    <template #additional-properties>
      <Card title="Phone Configuration">
        <!-- Country Code Selection -->
        <InputSection>
          <InputLabel label="Country Code" />
          <CountryCodeSelector
            :selected-country="blockData.validation?.country"
            @update:country="onCountryUpdate"
          />
        </InputSection>
        
        <!-- Country Code Styling -->
        <BackgroundStyleSection
          :background-settings="blockData.countryCodeStyle?.backgroundSettings || {}"
          @update:color="updateValue($event, 'countryCodeStyle.backgroundSettings.color')"
        />
      </Card>
    </template>
  </BaseInputConfiguration>
</template>

<script>
export default {
  methods: {
    updateValue(value, path) {
      // This triggers Vuex store update
      this.$store.commit('blocks/updateBlockValue', {
        blockName: this.blockData.blockName,
        path: path,
        value: value
      })
    },
    
    onCountryUpdate(country) {
      this.updateValue(country, 'validation.country')
    }
  }
}
</script>

Step 3: State Management (Vuex Store)

Store Mutation

javascript
// In Application-Frontend/src/store/modules/blocks.ts
mutations: {
  updateBlockValue(state, { blockName, path, value }) {
    const block = state.creativeBlocks[blockName]
    if (block) {
      // Update nested property using lodash set
      set(block, path, value)
      
      // Trigger reactivity
      Vue.set(state.creativeBlocks, blockName, { ...block })
      
      // Auto-save to backend
      this.dispatch('saveCreativeToBackend')
    }
  }
}

Resulting State Structure

javascript
// State after user configures phone input
state.creativeBlocks = {
  'formInputProperties-phone-1': {
    blockType: 'formInputProperties',
    blockName: 'formInputProperties-phone-1',
    type: 'tel',
    label: 'Phone Number',
    placeholder: 'Enter your phone number',
    required: true,
    validation: {
      phoneFormat: 'international',
      country: { code: '+47', flag: '🇳🇴', name: 'Norway' },
      minLength: 8,
      maxLength: 15,
      customErrorMessage: 'Please enter a valid phone number'
    },
    style: {
      fontSize: '16px',        // User changed from 14px to 16px
      fontFamily: 'Helvetica', // User changed from Arial
      color: '#2c3e50',        // User changed color
      width: '100%',
      height: '45px',          // User increased height
      paddingLeft: '15px',     // User increased padding
      paddingRight: '15px',
      paddingTop: '10px',
      paddingBottom: '10px'
    },
    countryCodeStyle: {
      showIcon: true,
      backgroundSettings: { mode: 'color', color: '#e9ecef' }, // User changed color
      style: {
        fontSize: '16px',      // Matches input font size
        fontFamily: 'Helvetica',
        color: '#495057'
      }
    }
  }
}

Step 4: Data Persistence (Application-Backend)

API Endpoint

javascript
// Application-Backend saves to file system
POST /api/creatives/{id}/save

// Saves complete creative JSON to:
// /tmp/creatives/{creative-id}/assets/stub.js

const creativeData = {
  creativeSettings: {
    creativeBlocks: {
      'formInputProperties-phone-1': {
        // ... complete phone input configuration
      }
    }
  }
}

Database Storage

sql
-- Also saved to PostgreSQL database
UPDATE creatives 
SET creative_json = $1, updated_at = NOW()
WHERE id = $2

Step 5: Payload Transfer to Creative-Engine

Data Structure Sent to Engine

javascript
// Complete payload sent to Creative-Engine iframe
window.postMessage({
  type: 'CREATIVE_DATA_UPDATE',
  payload: {
    creativeSettings: {
      creativeBlocks: {
        formProperties: {
          blockType: 'formProperties',
          inputs: ['formInputProperties-phone-1'],
          submitButtonProperties: { /* ... */ }
        },
        'formInputProperties-phone-1': {
          blockType: 'formInputProperties',
          type: 'tel',
          label: 'Phone Number',
          placeholder: 'Enter your phone number',
          required: true,
          validation: {
            phoneFormat: 'international',
            country: { code: '+47', flag: '🇳🇴', name: 'Norway' },
            minLength: 8,
            maxLength: 15,
            customErrorMessage: 'Please enter a valid phone number'
          },
          style: {
            fontSize: '16px',
            fontFamily: 'Helvetica',
            color: '#2c3e50',
            width: '100%',
            height: '45px',
            paddingLeft: '15px',
            paddingRight: '15px',
            paddingTop: '10px',
            paddingBottom: '10px'
          },
          countryCodeStyle: {
            showIcon: true,
            backgroundSettings: { mode: 'color', color: '#e9ecef' },
            style: {
              fontSize: '16px',
              fontFamily: 'Helvetica',
              color: '#495057'
            }
          }
        }
      }
    }
  }
}, '*')

Step 6: Creative-Engine Processing

Data Store Update

javascript
// Creative-Engine/src/services/dataStore.ts
export const DataStore = reactive({
  creativeSettings: {
    creativeBlocks: {}
  }
})

// Receives payload and updates store
function updateCreativeData(payload) {
  DataStore.creativeSettings = payload.creativeSettings
}

Form Store Integration

javascript
// Creative-Engine/src/stores/formStore.ts
const actions = {
  getFormProperties(): any {
    const blocks = DataStore.creativeSettings.creativeBlocks
    const formProps = blocks.formProperties || {}
    
    // Include nested form input blocks
    Object.keys(blocks).forEach(key => {
      if (key.startsWith('formInputProperties-')) {
        formProps[key] = blocks[key]
      }
    })
    
    return formProps
  },
  
  getFormInputs(formProps: any): any[] {
    const inputs = []
    const inputKeys = formProps.inputs || []
    
    inputKeys.forEach(key => {
      const inputBlock = formProps[key]
      if (inputBlock && inputBlock.blockType === 'formInputProperties') {
        inputs.push(inputBlock)
      }
    })
    
    return inputs.sort((a, b) => (a.order || 0) - (b.order || 0))
  }
}

Step 7: Component Rendering

Form Block Component

vue
<!-- Creative-Engine/src/components/creative/CreativeFormBlock.vue -->
<template>
  <div class="form-block" :style="formStyles">
    <FormPhoneInput
      v-for="(input, index) in phoneInputs"
      :key="input.blockName"
      :input-data="input"
      :index="index"
      :value="formData[index]"
      @input="updateField(index, $event)"
    />
  </div>
</template>

<script>
import { useFormStore } from '@/stores/formStore'

export default {
  setup() {
    const formStore = useFormStore()
    return { formStore }
  },
  
  computed: {
    formProps() {
      return this.formStore.getFormProperties()
    },
    
    formInputs() {
      return this.formStore.getFormInputs(this.formProps)
    },
    
    phoneInputs() {
      return this.formInputs.filter(input => input.type === 'tel')
    },
    
    formData() {
      return this.formStore.state.formData
    }
  },
  
  methods: {
    updateField(index, value) {
      this.formStore.updateField(index, value)
    }
  }
}
</script>

Phone Input Component

vue
<!-- Creative-Engine/src/components/creative/CreativeFormBlock/components/FormPhoneInput.vue -->
<template>
  <div class="phone-input-wrapper" :style="wrapperStyles">
    <label :style="labelStyles">{{ inputData.label }}</label>
    
    <div class="phone-input-container" :style="containerStyles">
      <!-- Country Code Display -->
      <div 
        v-if="showCountryCode" 
        class="country-code"
        :style="countryCodeStyles"
      >
        <span v-if="showCountryIcon">{{ countryFlag }}</span>
        {{ countryCode }}
      </div>
      
      <!-- Phone Input Field -->
      <input
        type="tel"
        :value="value"
        :placeholder="inputData.placeholder"
        :style="inputStyles"
        :class="{ 'error': hasValidationError }"
        @input="handleInput"
        @blur="validateInput"
      />
    </div>
    
    <!-- Error Display -->
    <div v-if="hasValidationError" class="error-message" :style="errorStyles">
      {{ validationError }}
    </div>
  </div>
</template>

<script>
export default {
  props: {
    inputData: Object,
    value: String,
    index: Number
  },
  
  computed: {
    showCountryCode() {
      return this.inputData.validation?.phoneFormat === 'international'
    },
    
    showCountryIcon() {
      return this.inputData.countryCodeStyle?.showIcon !== false
    },
    
    countryCode() {
      return this.inputData.validation?.country?.code || '+47'
    },
    
    countryFlag() {
      return this.inputData.validation?.country?.flag || '🇳🇴'
    },
    
    // Style computations based on configuration
    inputStyles() {
      const style = this.inputData.style || {}
      return {
        fontSize: style.fontSize || '14px',
        fontFamily: style.fontFamily || 'Arial, sans-serif',
        color: style.color || '#333333',
        width: style.width || '100%',
        height: style.height || '40px',
        paddingLeft: style.paddingLeft || '12px',
        paddingRight: style.paddingRight || '12px',
        paddingTop: style.paddingTop || '8px',
        paddingBottom: style.paddingBottom || '8px',
        border: this.hasValidationError ? '2px solid #dc3545' : '1px solid #ced4da',
        borderRadius: '4px',
        outline: 'none'
      }
    },
    
    countryCodeStyles() {
      const bgSettings = this.inputData.countryCodeStyle?.backgroundSettings || {}
      const textStyle = this.inputData.countryCodeStyle?.style || {}
      
      return {
        backgroundColor: bgSettings.mode === 'color' ? bgSettings.color : 'transparent',
        fontSize: textStyle.fontSize || '14px',
        fontFamily: textStyle.fontFamily || 'Arial, sans-serif',
        color: textStyle.color || '#495057',
        padding: '8px 12px',
        borderRight: '1px solid #ced4da',
        display: 'flex',
        alignItems: 'center',
        gap: '4px'
      }
    },
    
    labelStyles() {
      return {
        display: 'block',
        marginBottom: '4px',
        fontSize: '14px',
        fontWeight: '500',
        color: '#212529'
      }
    },
    
    hasValidationError() {
      return this.formStore.state.validationErrors[this.index]?.length > 0
    },
    
    validationError() {
      const errors = this.formStore.state.validationErrors[this.index]
      return errors && errors.length > 0 ? errors[0] : ''
    }
  },
  
  methods: {
    handleInput(event) {
      this.$emit('input', event.target.value)
    },
    
    validateInput() {
      this.formStore.validateField(this.index, this.value)
    }
  }
}
</script>

Step 8: Final Rendered Output

Generated HTML/CSS

html
<div class="phone-input-wrapper" style="margin-bottom: 16px;">
  <label style="display: block; margin-bottom: 4px; font-size: 14px; font-weight: 500; color: #212529;">
    Phone Number
  </label>
  
  <div class="phone-input-container" style="display: flex; border-radius: 4px; overflow: hidden;">
    <div class="country-code" style="
      background-color: #e9ecef;
      font-size: 16px;
      font-family: Helvetica;
      color: #495057;
      padding: 8px 12px;
      border-right: 1px solid #ced4da;
      display: flex;
      align-items: center;
      gap: 4px;
    ">
      <span>🇳🇴</span>
      +47
    </div>
    
    <input 
      type="tel" 
      placeholder="Enter your phone number"
      style="
        font-size: 16px;
        font-family: Helvetica;
        color: #2c3e50;
        width: 100%;
        height: 45px;
        padding-left: 15px;
        padding-right: 15px;
        padding-top: 10px;
        padding-bottom: 10px;
        border: 1px solid #ced4da;
        border-left: none;
        border-radius: 0;
        outline: none;
      "
    />
  </div>
</div>

Key Data Flow Patterns

1. Configuration → State → Persistence

User Input → Vue Component → Vuex Mutation → Backend API → Database/File System

2. State → Rendering

Database → API Response → DataStore → Form Store → Component Props → DOM

3. Real-time Updates

Configuration Change → Vuex State → iframe postMessage → Creative-Engine → Re-render

4. Validation Flow

User Input → Form Store Validation → Error State → UI Feedback → DOM Update

Benefits of This Architecture

1. Separation of Concerns

  • Frontend: Focuses on configuration UI and user experience
  • Backend: Handles data persistence and API management
  • Engine: Specialized in rendering and performance

2. Type Safety

  • Shared TypeScript interfaces ensure consistency
  • Compile-time error detection
  • IntelliSense support across codebases

3. Real-time Preview

  • Immediate visual feedback during configuration
  • iframe isolation prevents conflicts
  • Reactive data flow ensures synchronization

4. Scalability

  • Modular component architecture
  • Easy to add new input types and styling options
  • Independent deployment of each service

5. Maintainability

  • Clear data flow patterns
  • Centralized state management
  • Comprehensive type definitions

Current Challenges and Solutions

1. State Synchronization

Challenge: Keeping Frontend state in sync with Creative-Engine Solution: Reactive data stores with automatic iframe communication

2. Type Consistency

Challenge: Maintaining matching interfaces across codebases Solution: Shared type definitions and automated build processes

3. Performance

Challenge: Real-time updates can be expensive Solution: Debounced updates and selective re-rendering

4. Validation Complexity

Challenge: Complex validation rules for different input types Solution: Centralized validation system with type-specific rules

Future Enhancements

1. Pinia Migration

Replace current formStore with Pinia for better TypeScript support and developer experience

2. Real-time Collaboration

WebSocket integration for multi-user editing

3. Component Library

Shared component library between Frontend and Engine

4. Advanced Validation

Server-side validation and real-time feedback

This architecture provides a robust, scalable foundation for creating complex interactive advertising experiences while maintaining clear separation of concerns and type safety throughout the entire data flow.

Internal documentation