Appearance
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:
- Application-Frontend - Configuration interface where users design their ads
- Creative-Engine - Rendering library that displays the final ads
- 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 = $2Step 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 System2. State → Rendering
Database → API Response → DataStore → Form Store → Component Props → DOM3. Real-time Updates
Configuration Change → Vuex State → iframe postMessage → Creative-Engine → Re-render4. Validation Flow
User Input → Form Store Validation → Error State → UI Feedback → DOM UpdateBenefits 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.