coolify/.ai/patterns/frontend-patterns.md
Andras Bacsai 76e93806bf docs: consolidate AI documentation into .ai/ directory
- Update CLAUDE.md to reference .ai/ directory as single source of truth
- Move documentation structure to organized .ai/ directory with core/, development/, patterns/, meta/ subdirectories
- Update .ai/README.md with correct path references
- Update .ai/meta/maintaining-docs.md to reflect new structure
- Consolidate sync-guide.md with detailed synchronization rules
- Fix cross-reference in frontend-patterns.md

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-18 17:28:56 +01:00

20 KiB

Coolify Frontend Architecture & Patterns

Frontend Philosophy

Coolify uses a server-side first approach with minimal JavaScript, leveraging Livewire for reactivity and Alpine.js for lightweight client-side interactions.

Core Frontend Stack

Livewire 3.5+ (Primary Framework)

  • Server-side rendering with reactive components
  • Real-time updates without page refreshes
  • State management handled on the server
  • WebSocket integration for live updates

Alpine.js (Client-Side Interactivity)

  • Lightweight JavaScript for DOM manipulation
  • Declarative directives in HTML
  • Component-like behavior without build steps
  • Perfect companion to Livewire

Tailwind CSS 4.1+ (Styling)

  • Utility-first CSS framework
  • Custom design system for deployment platform
  • Responsive design built-in
  • Dark mode support

Livewire Component Structure

Location: app/Livewire/

Core Application Components

Server Management

  • Server/ directory - Server configuration and monitoring
  • Real-time server status updates
  • SSH connection management
  • Resource monitoring

Project & Application Management

  • Project/ directory - Project organization
  • Application deployment interfaces
  • Environment variable management
  • Service configuration

Settings & Configuration

User & Team Management

  • Team/ directory - Team collaboration features
  • Profile/ directory - User profile management
  • Security/ directory - Security settings

Blade Template Organization

Location: resources/views/

Layout Structure

  • layouts/ - Base layout templates
  • components/ - Reusable UI components
  • livewire/ - Livewire component views

Feature-Specific Views

  • server/ - Server management interfaces
  • auth/ - Authentication pages
  • emails/ - Email templates
  • errors/ - Error pages

Interactive Components

Monaco Editor Integration

  • Code editing for configuration files
  • Syntax highlighting for multiple languages
  • Live validation and error detection
  • Integration with deployment process

Terminal Emulation (XTerm.js)

  • Real-time terminal access to servers
  • WebSocket-based communication
  • Multi-session support
  • Secure connection through SSH

Real-Time Updates

  • WebSocket connections via Laravel Echo
  • Live deployment logs streaming
  • Server monitoring with live metrics
  • Activity notifications in real-time

Alpine.js Patterns

Common Directives Used

<!-- State management -->
<div x-data="{ open: false }">

<!-- Event handling -->
<button x-on:click="open = !open">

<!-- Conditional rendering -->
<div x-show="open">

<!-- Data binding -->
<input x-model="searchTerm">

<!-- Component initialization -->
<div x-init="initializeComponent()">

Integration with Livewire

<!-- Livewire actions with Alpine state -->
<button 
    x-data="{ loading: false }"
    x-on:click="loading = true"
    wire:click="deploy"
    wire:loading.attr="disabled"
    wire:target="deploy"
>
    <span x-show="!loading">Deploy</span>
    <span x-show="loading">Deploying...</span>
</button>

Tailwind CSS Patterns

Design System

  • Consistent spacing using Tailwind scale
  • Color palette optimized for deployment platform
  • Typography hierarchy for technical content
  • Component classes for reusable elements

Responsive Design

<!-- Mobile-first responsive design -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
    <!-- Content adapts to screen size -->
</div>

Dark Mode Support

<!-- Dark mode variants -->
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100">
    <!-- Automatic dark mode switching -->
</div>

Build Process

Vite Configuration (vite.config.js)

  • Fast development with hot module replacement
  • Optimized production builds
  • Asset versioning for cache busting
  • CSS processing with PostCSS

Asset Compilation

# Development
npm run dev

# Production build
npm run build

State Management Patterns

Server-Side State (Livewire)

  • Component properties for persistent state
  • Session storage for user preferences
  • Database models for application state
  • Cache layer for performance

Client-Side State (Alpine.js)

  • Local component state for UI interactions
  • Form validation and user feedback
  • Modal and dropdown state management
  • Temporary UI states (loading, hover, etc.)

Real-Time Features

WebSocket Integration

// Livewire component with real-time updates
class ActivityMonitor extends Component
{
    public function getListeners()
    {
        return [
            'deployment.started' => 'refresh',
            'deployment.finished' => 'refresh',
            'server.status.changed' => 'updateServerStatus',
        ];
    }
}

Event Broadcasting

  • Laravel Echo for client-side WebSocket handling
  • Pusher protocol for real-time communication
  • Private channels for user-specific events
  • Presence channels for collaborative features

Performance Patterns

Lazy Loading

// Livewire lazy loading
class ServerList extends Component
{
    public function placeholder()
    {
        return view('components.loading-skeleton');
    }
}

Caching Strategies

  • Fragment caching for expensive operations
  • Image optimization with lazy loading
  • Asset bundling and compression
  • CDN integration for static assets

Enhanced Form Components

Built-in Authorization System

Coolify features enhanced form components with automatic authorization handling:

<!-- ✅ New Pattern: Single line with built-in authorization -->
<x-forms.input canGate="update" :canResource="$application" id="application.name" label="Name" />
<x-forms.checkbox instantSave canGate="update" :canResource="$application" id="application.settings.is_static" label="Static Site" />
<x-forms.button canGate="update" :canResource="$application" type="submit">Save</x-forms.button>

<!-- ❌ Old Pattern: Verbose @can/@else blocks (deprecated) -->
@can('update', $application)
    <x-forms.input id="application.name" label="Name" />
@else
    <x-forms.input disabled id="application.name" label="Name" />
@endcan

Authorization Parameters

// Available on all form components (Input, Select, Textarea, Checkbox, Button)
public ?string $canGate = null;        // Gate name: 'update', 'view', 'deploy', 'delete'
public mixed $canResource = null;      // Resource model instance to check against
public bool $autoDisable = true;       // Automatically disable if no permission (default: true)

Benefits

  • 90% code reduction for authorization-protected forms
  • Consistent security across all form components
  • Automatic disabling for unauthorized users
  • Smart behavior (disables instantSave on checkboxes for unauthorized users)

For complete documentation, see form-components.md

Form Handling Patterns

Livewire Component Data Synchronization Pattern

IMPORTANT: All Livewire components must use the manual syncData() pattern for synchronizing component properties with Eloquent models.

Property Naming Convention

  • Component properties: Use camelCase (e.g., $gitRepository, $isStatic)
  • Database columns: Use snake_case (e.g., git_repository, is_static)
  • View bindings: Use camelCase matching component properties (e.g., id="gitRepository")

The syncData() Method Pattern

use Livewire\Attributes\Validate;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;

class MyComponent extends Component
{
    use AuthorizesRequests;

    public Application $application;

    // Properties with validation attributes
    #[Validate(['required'])]
    public string $name;

    #[Validate(['string', 'nullable'])]
    public ?string $description = null;

    #[Validate(['boolean', 'required'])]
    public bool $isStatic = false;

    public function mount()
    {
        $this->authorize('view', $this->application);
        $this->syncData(); // Load from model
    }

    public function syncData(bool $toModel = false): void
    {
        if ($toModel) {
            $this->validate();

            // Sync TO model (camelCase → snake_case)
            $this->application->name = $this->name;
            $this->application->description = $this->description;
            $this->application->is_static = $this->isStatic;

            $this->application->save();
        } else {
            // Sync FROM model (snake_case → camelCase)
            $this->name = $this->application->name;
            $this->description = $this->application->description;
            $this->isStatic = $this->application->is_static;
        }
    }

    public function submit()
    {
        $this->authorize('update', $this->application);
        $this->syncData(toModel: true); // Save to model
        $this->dispatch('success', 'Saved successfully.');
    }
}

Validation with #[Validate] Attributes

All component properties should have #[Validate] attributes:

// Boolean properties
#[Validate(['boolean'])]
public bool $isEnabled = false;

// Required strings
#[Validate(['string', 'required'])]
public string $name;

// Nullable strings
#[Validate(['string', 'nullable'])]
public ?string $description = null;

// With constraints
#[Validate(['integer', 'min:1'])]
public int $timeout;

Benefits of syncData() Pattern

  • Explicit Control: Clear visibility of what's being synchronized
  • Type Safety: #[Validate] attributes provide compile-time validation info
  • Easy Debugging: Single method to check for data flow issues
  • Maintainability: All sync logic in one place
  • Flexibility: Can add custom logic (encoding, transformations, etc.)

Creating New Form Components with syncData()

Step-by-Step Component Creation Guide

Step 1: Define properties in camelCase with #[Validate] attributes

use Livewire\Attributes\Validate;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;

class MyFormComponent extends Component
{
    use AuthorizesRequests;

    // The model we're syncing with
    public Application $application;

    // Component properties in camelCase with validation
    #[Validate(['string', 'required'])]
    public string $name;

    #[Validate(['string', 'nullable'])]
    public ?string $gitRepository = null;

    #[Validate(['string', 'nullable'])]
    public ?string $installCommand = null;

    #[Validate(['boolean'])]
    public bool $isStatic = false;
}

Step 2: Implement syncData() method

public function syncData(bool $toModel = false): void
{
    if ($toModel) {
        $this->validate();

        // Sync TO model (component camelCase → database snake_case)
        $this->application->name = $this->name;
        $this->application->git_repository = $this->gitRepository;
        $this->application->install_command = $this->installCommand;
        $this->application->is_static = $this->isStatic;

        $this->application->save();
    } else {
        // Sync FROM model (database snake_case → component camelCase)
        $this->name = $this->application->name;
        $this->gitRepository = $this->application->git_repository;
        $this->installCommand = $this->application->install_command;
        $this->isStatic = $this->application->is_static;
    }
}

Step 3: Implement mount() to load initial data

public function mount()
{
    $this->authorize('view', $this->application);
    $this->syncData(); // Load data from model to component properties
}

Step 4: Implement action methods with authorization

public function instantSave()
{
    try {
        $this->authorize('update', $this->application);
        $this->syncData(toModel: true); // Save component properties to model
        $this->dispatch('success', 'Settings saved.');
    } catch (\Throwable $e) {
        return handleError($e, $this);
    }
}

public function submit()
{
    try {
        $this->authorize('update', $this->application);
        $this->syncData(toModel: true); // Save component properties to model
        $this->dispatch('success', 'Changes saved successfully.');
    } catch (\Throwable $e) {
        return handleError($e, $this);
    }
}

Step 5: Create Blade view with camelCase bindings

<div>
    <form wire:submit="submit">
        <x-forms.input
            canGate="update"
            :canResource="$application"
            id="name"
            label="Name"
            required />

        <x-forms.input
            canGate="update"
            :canResource="$application"
            id="gitRepository"
            label="Git Repository" />

        <x-forms.input
            canGate="update"
            :canResource="$application"
            id="installCommand"
            label="Install Command" />

        <x-forms.checkbox
            instantSave
            canGate="update"
            :canResource="$application"
            id="isStatic"
            label="Static Site" />

        <x-forms.button
            canGate="update"
            :canResource="$application"
            type="submit">
            Save Changes
        </x-forms.button>
    </form>
</div>

Key Points:

  • Use wire:model="camelCase" and id="camelCase" in Blade views
  • Component properties are camelCase, database columns are snake_case
  • Always include authorization checks (authorize(), canGate, canResource)
  • Use instantSave for checkboxes that save immediately without form submission

Special Patterns

Pattern 1: Related Models (e.g., Application → Settings)

public function syncData(bool $toModel = false): void
{
    if ($toModel) {
        $this->validate();

        // Sync main model
        $this->application->name = $this->name;
        $this->application->save();

        // Sync related model
        $this->application->settings->is_static = $this->isStatic;
        $this->application->settings->save();
    } else {
        // From main model
        $this->name = $this->application->name;

        // From related model
        $this->isStatic = $this->application->settings->is_static;
    }
}

Pattern 2: Custom Encoding/Decoding

public function syncData(bool $toModel = false): void
{
    if ($toModel) {
        $this->validate();

        // Encode before saving
        $this->application->custom_labels = base64_encode($this->customLabels);
        $this->application->save();
    } else {
        // Decode when loading
        $this->customLabels = $this->application->parseContainerLabels();
    }
}

Pattern 3: Error Rollback

public function submit()
{
    $this->authorize('update', $this->resource);
    $original = $this->model->getOriginal();

    try {
        $this->syncData(toModel: true);
        $this->dispatch('success', 'Saved successfully.');
    } catch (\Throwable $e) {
        // Rollback on error
        $this->model->setRawAttributes($original);
        $this->model->save();
        $this->syncData(); // Reload from model
        return handleError($e, $this);
    }
}

Property Type Patterns

Required Strings

#[Validate(['string', 'required'])]
public string $name;  // No ?, no default, always has value

Nullable Strings

#[Validate(['string', 'nullable'])]
public ?string $description = null;  // ?, = null, can be empty

Booleans

#[Validate(['boolean'])]
public bool $isEnabled = false;  // Always has default value

Integers with Constraints

#[Validate(['integer', 'min:1'])]
public int $timeout;  // Required

#[Validate(['integer', 'min:1', 'nullable'])]
public ?int $port = null;  // Nullable

Testing Checklist

After creating a new component with syncData(), verify:

  • All checkboxes save correctly (especially instantSave ones)
  • All form inputs persist to database
  • Custom encoded fields (like labels) display correctly if applicable
  • Form validation works for all fields
  • No console errors in browser
  • Authorization checks work (@can directives and authorize() calls)
  • Error rollback works if exceptions occur
  • Related models save correctly if applicable (e.g., Application + ApplicationSetting)

Common Pitfalls to Avoid

  1. snake_case in component properties: Always use camelCase for component properties (e.g., $gitRepository not $git_repository)
  2. Missing #[Validate] attributes: Every property should have validation attributes for type safety
  3. Forgetting to call syncData(): Must call syncData() in mount() to load initial data
  4. Missing authorization: Always use authorize() in methods and canGate/canResource in views
  5. View binding mismatch: Use camelCase in Blade (e.g., id="gitRepository" not id="git_repository")
  6. wire:model vs wire:model.live: Use .live for instantSave checkboxes to avoid timing issues
  7. Validation sync: If using rules() method, keep it in sync with #[Validate] attributes
  8. Related models: Don't forget to save both main and related models in syncData() method

Livewire Forms

class ServerCreateForm extends Component
{
    public $name;
    public $ip;

    protected $rules = [
        'name' => 'required|min:3',
        'ip' => 'required|ip',
    ];

    public function save()
    {
        $this->validate();
        // Save logic
    }
}

Real-Time Validation

  • Live validation as user types
  • Server-side validation rules
  • Error message display
  • Success feedback patterns

Component Communication

Parent-Child Communication

// Parent component
$this->emit('serverCreated', $server->id);

// Child component
protected $listeners = ['serverCreated' => 'refresh'];

Cross-Component Events

  • Global events for application-wide updates
  • Scoped events for feature-specific communication
  • Browser events for JavaScript integration

Error Handling & UX

Loading States

  • Skeleton screens during data loading
  • Progress indicators for long operations
  • Optimistic updates with rollback capability

Error Display

  • Toast notifications for user feedback
  • Inline validation errors
  • Global error handling
  • Retry mechanisms for failed operations

Accessibility Patterns

ARIA Labels and Roles

<button
    aria-label="Deploy application"
    aria-describedby="deploy-help"
    wire:click="deploy"
>
    Deploy
</button>

Keyboard Navigation

  • Tab order management
  • Keyboard shortcuts for power users
  • Focus management in modals and forms
  • Screen reader compatibility

Mobile Optimization

Touch-Friendly Interface

  • Larger tap targets for mobile devices
  • Swipe gestures where appropriate
  • Mobile-optimized forms and navigation

Progressive Enhancement

  • Core functionality works without JavaScript
  • Enhanced experience with JavaScript enabled
  • Offline capabilities where possible