Appearance
Cavai Architecture Blueprint
Cross-checked against actual codebase March 2026. Docs marked as plans/unimplemented are noted.
MAINTENANCE: This document must be kept in sync with the codebase. After significant PRs or architectural changes, update the relevant sections. Check recent
git log mainin both repos to verify accuracy before relying on this document in a new session.
Repository Structure
| Repo | Role | Tech |
|---|---|---|
| Application-Frontend | Everything visual in the platform — builder, reports, navigation, management views | Vue 2 (Options API), Vuex, SCSS |
| Creative-Engine | Vue library that renders interactive creatives in an isolated iframe environment | Vue 2, TypeScript, mixins |
| Application-Backend | Cloud API — authentication, campaign management, data, and integrations | AdonisJS, PostgreSQL |
| Creative-Composer | Build pipeline that compresses and optimizes creative data for deployment | Node.js |
All repos live under /Users/nicolaykjaernet/CavaiProduct/.
Recent Architectural Changes (as of v8.20.3 / CE 6.7.1)
Unified State System (PR #1779 / CE #693)
The state system was unified across buttons, form submit buttons, and form inputs. All use a shared StateConfig object:
typescript
type StateConfig = {
transition?: TransitionTiming // { duration, easing?, delay? }
hover?: StateStyle
focus?: StateStyle
active?: StateStyle
disabled?: StateStyle
loading?: StateStyle
success?: StateStyle
error?: StateStyle
}
type StateStyle = {
background?: boolean
backgroundSettings?: BackgroundSettings
border?: boolean
borderStyle?: BorderStyleSettings
boxShadow?: boolean
boxShadowSettings?: ShadowSettings
filter?: boolean
filterSettings?: FilterSettings
backdropFilter?: boolean
backdropFilterSettings?: FilterSettings
color?: string
transform?: TransformStyle // { scale?, rotate?, translateX?, translateY? }
opacity?: number
}State enums restrict which states are available per component:
ButtonState: default, hover, activeSubmitButtonState: default, hover, active, disabled, loading, success, errorInputFieldState: default, hover, focus
Key components: StateConfigurationCard.vue, StateSection.vue
Replaces the old separate hoverStyle/focusStyle/disabledStyle top-level properties.
Button Templates (PR #1779)
7 button templates added: Elevated, Minimal, Outline, Glass, Bold, Push, Cavai Classic. Each includes pre-designed state styles and transitions.
Template Modification Tracking (PR #1779)
All template-enabled blocks now track whether the user has modified a template. configurationLogic uses _templateReady flag to skip tracking during initial mount.
Bulk Export Delivery Settings (PR #1802)
Expanded bulk export modal with all delivery settings (VAST, VPAID, clickthrough, z-index, etc.) in collapsible sections. Includes fullscreen mode and conditional field display based on creative type.
New Reusable Section Components (PR #1779)
Added: DurationSection, DelaySection, TransitionEasingSection, ScaleSection, TranslateSection, FilterSection, IterationsSection
State Management: Vuex (NOT Pinia)
Note: Pinia is initialized in
main.tsfor future use, but no Pinia stores currently exist. All active state management uses Vuex. The planned migration (documented inpinia-store-architecture-migration.md) has not been executed.
Key Vuex Modules
| Module | Path | Purpose |
|---|---|---|
blocks | store/modules/blocks.ts | Block CRUD, ordering, reordering, creative block state |
builder | store/modules/builder.ts | Builder UI state (preview expanded, selected block, etc.) |
creatives | store/modules/creatives.ts | Creative listing and management |
creativeWizard | store/modules/creativeWizard.ts | Wizard flow state |
preview | store/modules/preview.ts | Preview iframe state |
auth | store/modules/auth.ts | Authentication |
How Config Components Talk to Store
User interaction
→ Section component emits event
→ Config component receives event
→ configurationLogic.updateValue(value, path)
→ Vuex mutation: updateBlockValue({ path, value })
→ state.creativeBlocks updatedTwo Core Mixins
1. configurationLogic (Config Components)
Used by 25 configuration components (*Configuration.vue).
Provides:
blockDatacomputed — fetches block from Vuex viagetBlockByNameupdateValue(value, path)— writes to Vuex storetitle,titleAddition— display name logicpathprop — dot-notation path to block (e.g.,formProperties.formInputProperties-3)
2. sectionLogic (Section Components)
Used by 41+ section components (reusable UI sections like BorderStyleSection, PaddingSection, etc.).
Provides:
inputLocked— master/child creative override logicoverrideProps/overrideHandlers— section-level override systemnarrow— responsive layout for expanded previewcssToNumber(),numberToPx()— utility methods
How They Compose
TextConfiguration.vue (uses configurationLogic)
└─ BorderStyleSection.vue (uses sectionLogic)
└─ BackgroundStyleSection.vue (uses sectionLogic)
└─ AlignmentSection.vue (uses sectionLogic)
└─ PaddingSection.vue (uses sectionLogic)
└─ ...Config component = orchestrator. Section components = reusable building blocks.
Block System
Block Types
Defined in src/constants/blocks.ts:
typescript
export const BLOCKS = {
BASE: 'baseProperties',
BUTTON: 'buttonProperties',
TEXT: 'textProperties',
GRAPHIC: 'graphicProperties',
HTML: 'htmlProperties',
SLIDER: 'sliderProperties',
SLIDER_V2: 'sliderV2Properties',
FORM: 'formProperties',
FORM_INPUT: 'formInputProperties',
FORM_SUBMIT_BUTTON: 'formSubmitButtonProperties',
VIDEO: 'videoProperties',
CONVERSATION: 'conversationProperties',
AR: 'arProperties',
// ... more
}
export const VISUAL_ELEMENTS = [BLOCKS.TEXT, BLOCKS.GRAPHIC, BLOCKS.HTML, BLOCKS.BUTTON]Two Categories
| Visual Elements | Specialized Blocks | |
|---|---|---|
| Examples | Text, Graphic, HTML, Button | Form, Slider, SliderV2, Video, Conversation |
In VISUAL_ELEMENTS? | Yes | No |
| Multiple instances? | Yes (textProperties-1, -2) | One per creative (static key) |
| Rendered via | CreativeVisualElements.vue | Dedicated components in CreativeBody.vue |
| Sub-blocks? | No | Yes (form inputs, slider slides) |
Key Data Files
| File | Purpose |
|---|---|
Blocks/data/types.ts | TypeScript interfaces for all block properties |
Blocks/data/defaults.ts | Default values, factory functions, blockDefaults map |
Blocks/utils.ts | getSubBlocks(), getBlockPath(), uniqueBlockId() |
Block Data Structure (in Vuex)
json
{
"creativeBlocks": {
"baseProperties": { "blockName": "baseProperties", "order": 0, ... },
"textProperties-1": { "blockName": "textProperties-1", "order": 1, ... },
"formProperties": {
"blockName": "formProperties", "order": 2,
"formInputProperties-1": { "blockName": "formInputProperties-1", "parent": "formProperties", "order": 0, ... },
"formSubmitButtonProperties": { ... }
}
}
}Sub-blocks are stored as object properties on the parent, NOT in arrays.
Style System ("Lego Blocks")
Each styling aspect is a modular config object combined through Creative-Engine mixins.
Style Config Objects (CE types)
BoxShadowConfig—boxShadow: boolean,boxShadowSettings: {...}BorderConfig—border: boolean,borderStyle: {...}BackgroundConfig—background: boolean,backgroundSettings: {...}SizeConfig—size: boolean,sizeStyle: {...}FlexAlignConfig— alignment propertiesCustomStylesConfig— custom CSS
CE Mixins (rendering)
StyleAndClassNameGenerationMixin— core style object + class generationBlockMixin— block-specific stylingVisualElementMixin— position, visibility for visual elements
Visual elements use all three mixins. Specialized blocks access DataStore directly and compute their own styles.
SCSS Variables (Frontend)
Location: src/styles/
| File | Contents |
|---|---|
_variables.scss | Sizes, border radius, transitions, font sizes, line heights |
_colors.scss | Color palette |
_newcolors.scss | Updated colors |
_shadows.scss | Box shadows |
_animations.scss | Animation utilities |
_fonts.scss | Font declarations |
Key transition variables:
scss
$transition-quick: 0.16s ease-in-out;
$transition-medium: 0.25s ease-in-out;
$transition-long: 0.5s ease-in-out;
$transition-pop-quick: 0.16s cubic-bezier(0.34, 1.56, 0.64, 1);Frontend ↔ Creative-Engine Integration
Architecture
Application-Frontend (main window, Vuex)
↕ postMessage / iframe src
Creative-Engine (iframe, isolated DataStore)They run in separate contexts. Changes don't reflect in real-time — preview requires iframe reload.
Data Flow
Frontend Config UI → Vuex Store → JSON Payload → Backend → Creative-Engine RenderingAdding/Updating Blocks (Checklist)
Application-Frontend:
constants/blocks.ts— register block typeBlocks/data/types.ts— TypeScript interfaceBlocks/data/defaults.ts— default values + register inblockDefaultsConfiguration/configs/<Block>Configuration.vue— config panelAddBlockTool.vue— add buttonassets/i18n/en.js— translations
Creative-Engine:
src/utils/constants.ts— register inBLOCKSsrc/interfaces/jsonTypes/payload-v2/index.ts— matching interface- Rendering component in
src/components/creative/ - For visual elements: register in
CreativeVisualElements.vue - For specialized: import in
CreativeBody.vue+showXxxBlock+blockOrders npm run build:libraryafter changes
CRITICAL: Types in both repos MUST match. No data transformation — Pinia/Vuex is just a state management layer.
Template System
Templates are type-safe, extensible presets for block configurations.
Pattern:
- Define enum in
types.ts(e.g.,FormTemplate) - Create generator functions returning JSON config
- Register in
templateRegistry(central map) - Use
TemplateButtoncomponent in config UI
Currently used for: Forms, HTML blocks, Code editor, SliderV2.
State Preview System
Allows previewing component states (hover, focus, disabled, loading) in builder without affecting runtime.
Key mechanism: isStatePreview boolean flag + previewState enum value on blocks. When isStatePreview is true, Creative-Engine shows preview styling from stateConfig[previewState] but doesn't apply actual disabled/hover behavior.
Unified state system (current):
- All stateful blocks use
stateConfig: StateConfigobject (see Recent Changes section) - State enums:
ButtonState,SubmitButtonState,InputFieldState - Helper functions in
stateHelpers.ts:getButtonStates(),getSubmitButtonStates(),getInputFieldStates() StateConfigurationCardcomponent renders state tabs with per-state styling sections- Reset on
beforeDestroyviaconfigurationLogic
Pattern for adding states to a new block:
- Define state enum in
types.ts - Add
stateConfig?: StateConfig+isStatePreview?: boolean+previewState?: YourEnumto block type - Create helpers in
stateHelpers.ts - Use
StateConfigurationCardin config component - Handle state rendering in Creative-Engine component
Reusable Section Components
All live in Configuration/components/. Each uses sectionLogic mixin.
Styling sections (reuse across blocks): BackgroundStyleSection, BorderStyleSection, BoxShadowSection, PaddingSection, MarginSection, AlignmentSection, FlexAlignmentSection, SizeSection, AdvancedSizeSection, MaxSizeSection, RotationSection, FilterSection
Typography (font settings): FontSizeSection, FontWeightSection, FontColorSection, TextAlignSection, TypefaceSection — all in FontSettings/ subfolder
Animation/transition sections: DurationSection, DelaySection, SpeedSection, TransitionEasingSection, TranslateSection, ScaleSection, IterationsSection, AnimationPresetSection, AnimationTriggerSection, AnimationDirectionSection
Animation system (animation?: AnimationConfig on BlockBase/BasicConfig): All block types can have a per-block animation config with 12 presets, 4 triggers (load/hover/click/scroll), looping, direction, easing. In CE, AnimationMixin provides reusable computeds and trigger handling. Keyframes generated via styles/animations/presets.ts and injected through StyleTreeParser. Designed for future Block Groups (#1805) — per-block config = "what", group childAnimation = "how" (stagger/cycle with reorderable sequence).
Block-specific: SliderArrowsSection, SliderFeedSection, SliderEdgeFadeSection, VideoSection, ConversationAnimationSection, ContentSection, HtmlSection
UI utilities: OptionList, TabNavigation, TemplateButton, StateSection, StateConfigurationCard, OverrideIndicator, DeleteButtonWithExpandingConfirmation, CustomCSSEditor, MultiPostfixInput
Component Composition Pattern (Options API)
The codebase primarily uses Vue 2 Options API. Only a few newer form sub-components use <script setup>.
Standard config component structure:
vue
<script>
import { configurationLogic } from '../mixins/configurationLogic'
import BorderStyleSection from '../components/BorderStyleSection.vue'
// ... more sections
export default {
name: 'TextConfiguration',
mixins: [configurationLogic],
components: { BorderStyleSection, ... },
// component-specific computed/methods
}
</script>Standard section component structure:
vue
<script>
import { sectionLogic } from '../mixins/sectionLogic'
export default {
name: 'BorderStyleSection',
mixins: [sectionLogic],
// section-specific logic, emits events to parent config
}
</script>Master/Child Creative Override System
Creatives can be "child" creatives that inherit from a "master". Sections can be individually overridden.
sectionLogicprovidesinputLocked,isOverridden,overridePropsgetSectionSettings()inutils.tsmaps section names to block paths- Overrides are tracked in Vuex and synced to backend
Backend Communication
- Routes:
Application-Backend/start/routes/ - Controllers:
Application-Backend/app/Controllers/Http/ - Resources: shape API responses, include
linksfor available actions - Frontend services:
src/services/call API endpoints - Build artifacts:
Application-Backend/tmp/creatives/assets/creatives/[id]/
Build Pipeline
AF config → JSON payload → Backend stores → Composer builds →
├── creative-engine.js (CE bundle)
├── stub.js (compiled creative)
├── creative.json (full data)
├── tag.html (embed tag)
└── log.txt (build logs)Common Gotchas
- No Pinia — migration docs are aspirational, not reality
- Sub-blocks are object properties, not arrays (legacy
inputsarray was removed) blockNamenever changes after creation — can diverge fromorderandtypeIndex- CE must be rebuilt after changes:
npm run build:library - No real-time preview updates — iframe isolation requires reload
- Scoped
<style>in CE components breaks preview — use mixin-injected CSS only - Types must match exactly between AF
types.tsand CEpayload-v2/index.ts - Don't add specialized blocks to
VISUAL_ELEMENTSarray - SafeFrame viewport units —
100vw/100vhinside a SafeFrame iframe resolve to the SF's internal dimensions, not the device screen. See SafeFrame Responsive Scaling research and CE #727 for test infrastructure. Not currently a live issue but may resurface.