SkillAgentSearch skills...

Duo

A offline-first plug and play package to convert your Livewire components on runtime.

Install / Use

/learn @joshcirre/Duo
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Duo (VERY MUCH A WIP)

Local-first IndexedDB syncing for Laravel and Livewire applications.

Duo enables automatic client-side caching and synchronization of your Eloquent models using IndexedDB, providing a seamless offline-first experience for your Laravel/Livewire applications. Just add a trait to your Livewire component and Duo handles the rest—automatically transforming your server-side components to work with IndexedDB.

Features

  • 🚀 Zero Configuration: Add one trait and Duo automatically transforms your Livewire components to Alpine.js
  • 💾 Automatic IndexedDB Caching: Transparently cache Eloquent models in the browser
  • 🗄️ Schema Extraction: Automatically extracts database column types, nullability, and defaults for IndexedDB
  • Optimistic Updates: Instant UI updates with background server synchronization
  • 🔄 Offline Support: Automatic offline detection with sync queue that resumes when back online
  • 📊 Visual Sync Status: Built-in component showing online/offline/syncing states
  • 🎯 Livewire Integration: Seamless integration with Livewire 3+ and Volt components
  • 📦 Type-Safe: Full TypeScript support with auto-generated types from database schema
  • 🔌 Vite Plugin: Automatic manifest generation with file watching

Local Development Setup

Want to contribute or test Duo locally? Follow these steps to set up local development with symlinked packages.

1. Clone and Install Duo

# Clone the Duo package repository
git clone https://github.com/joshcirre/duo.git
cd duo

# Install PHP dependencies
composer install

# Install Node dependencies
npm install

# Build the package
npm run build

2. Symlink Composer Package

Link the Duo package to your local Laravel application:

# In your Laravel app directory (e.g., ~/Code/my-laravel-app)
cd ~/Code/my-laravel-app

# Add the local repository to composer.json
composer config repositories.duo path ../duo

# Require the package from the local path
composer require joshcirre/duo:@dev

This creates a symlink in vendor/joshcirre/duo pointing to your local Duo directory. Changes to the PHP code are immediately reflected.

3. Symlink NPM Package

Link the Vite plugin to your Laravel application:

# In the Duo package directory
cd ~/Code/duo
npm link

# In your Laravel app directory
cd ~/Code/my-laravel-app
npm link @joshcirre/vite-plugin-duo

Now your Laravel app uses the local version of the Vite plugin.

4. Watch for Changes

In the Duo package directory, run the build watcher:

cd ~/Code/duo
npm run dev

This watches for TypeScript changes and rebuilds automatically. Changes are immediately available in your linked Laravel app.

5. Test Your Changes

In your Laravel app:

# Run both Vite and Laravel (recommended)
composer run dev

This runs both npm run dev and php artisan serve concurrently. Any changes you make to Duo's PHP or TypeScript code will be reflected immediately!

Alternative (manual):

# Terminal 1: Vite
npm run dev

# Terminal 2: Laravel
php artisan serve

6. Unlinking (When Done)

To remove the symlinks:

# Unlink npm package (in your Laravel app)
cd ~/Code/my-laravel-app
npm unlink @joshcirre/vite-plugin-duo

# Unlink composer package
composer config repositories.duo --unset
composer require joshcirre/duo  # Reinstall from Packagist

# Unlink from Duo directory
cd ~/Code/duo
npm unlink

Development Tips

  • PHP Changes: Automatically picked up via symlink
  • TypeScript Changes: Require npm run build or npm run dev (watch mode)
  • View Changes: Blade components update automatically
  • Config Changes: May require php artisan optimize:clear
  • Manifest Changes: Run php artisan duo:generate manually if needed

Installation

Composer Package

composer require joshcirre/duo

NPM Package (Vite Plugin)

npm install -D @joshcirre/vite-plugin-duo

Note: Dexie is automatically installed as a dependency.

Publishing Assets (Optional)

Duo works out-of-the-box without publishing any files. However, you can publish various assets for customization:

# Publish configuration file
php artisan vendor:publish --tag=duo-config

# Publish Blade components (sync-status, debug panel)
php artisan vendor:publish --tag=duo-views

# Publish JavaScript assets (advanced users only)
php artisan vendor:publish --tag=duo-assets

# Publish everything
php artisan vendor:publish --provider="JoshCirre\Duo\DuoServiceProvider"

What gets published:

  • duo-configconfig/duo.php - Global configuration
  • duo-viewsresources/views/vendor/duo/components/ - Blade components for customization
  • duo-assetsresources/js/vendor/duo/ - JavaScript source files (rarely needed)

See Publishing Components in the Configuration section for customization examples.

Quick Start

1. Add the Syncable Trait to Your Models

Add the Syncable trait to any Eloquent model you want to cache in IndexedDB:

use JoshCirre\Duo\Syncable;

class Todo extends Model
{
    use Syncable;

    protected $fillable = ['title', 'description', 'completed'];
}

Both $fillable and $guarded are supported:

// Option 1: Using $fillable (explicit allow list)
protected $fillable = ['title', 'description', 'completed'];

// Option 2: Using $guarded (explicit deny list)
protected $guarded = ['id']; // Everything except 'id' is fillable

Duo automatically extracts your model's fillable attributes and database schema (column types, nullable, defaults) to generate the IndexedDB manifest—no manual configuration needed!

User-Scoped Models:

For models that belong to users, add a user() relationship but do NOT add user_id to $fillable:

class Todo extends Model
{
    use Syncable;

    // ✅ CORRECT: user_id is NOT in $fillable (security)
    protected $fillable = ['title', 'description', 'completed'];

    // ✅ Add user relationship - Duo auto-assigns user_id during sync
    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

Why? Including user_id in $fillable is a security risk—users could assign items to other users. Duo automatically detects the user() relationship and assigns the authenticated user's ID securely during sync.

2. Add @duoMeta Directive to Your Layout

CRITICAL: Add the @duoMeta directive to the <head> section of your main layout. This provides the CSRF token and enables offline page caching:

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    @duoMeta

    <title>{{ $title ?? config('app.name') }}</title>
    <!-- rest of your head content -->
</head>

The @duoMeta directive outputs:

  • <meta name="csrf-token"> - Required for API sync requests
  • <meta name="duo-cache"> - Tells the service worker to cache this page for offline access

3. Configure Vite

Add the Duo plugin to your vite.config.js:

import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import { duo } from '@joshcirre/vite-plugin-duo';

export default defineConfig({
    plugins: [
        laravel({
            input: ['resources/css/app.css', 'resources/js/app.js'],
            refresh: true,
        }),
        duo(), // That's it! Uses sensible defaults
    ],
});

That's all you need! The plugin will automatically:

  • ✅ Generate the manifest at resources/js/duo/manifest.json
  • ✅ Watch app/Models/**/*.php for changes
  • ✅ Auto-regenerate manifest when models change
  • ✅ Inject Duo initialization code into resources/js/app.js
  • ✅ Copy the service worker to public/duo-sw.js on build

Want to customize? All options have sensible defaults and are optional:

duo({
    // Manifest path (default: 'resources/js/duo/manifest.json')
    manifestPath: 'resources/js/duo/manifest.json',

    // Watch for file changes (default: true)
    watch: true,

    // Auto-generate manifest (default: true)
    autoGenerate: true,

    // Files to watch for changes (default: ['app/Models/**/*.php'])
    patterns: [
        'app/Models/**/*.php',
        'resources/views/livewire/**/*.php',  // Include Volt components
        'app/Livewire/**/*.php',              // Include class-based components
    ],

    // Entry file for auto-injection (default: 'resources/js/app.js')
    entry: 'resources/js/app.js',

    // Auto-inject initialization code (default: true)
    autoInject: true,

    // Custom artisan command (default: 'php artisan duo:generate')
    command: 'php artisan duo:generate',
})

4. Add the WithDuo Trait to Your Livewire Components

This is where the magic happens! Add the WithDuo trait to any Livewire component and Duo will automatically transform it to use IndexedDB:

Volt Component Example:

<?php
use Livewire\Volt\Component;
use App\Models\Todo;
use JoshCirre\Duo\WithDuo;

new class extends Component {
    use WithDuo;  // ✨ This is all you need!

    public string $newTodoTitle = '';

    public function addTodo()
    {
        Todo::create(['title' => $this->newTodoTitle]);
        $this->reset('newTodoTitle');
    }

    public function toggleTodo($id)
    {
        $todo = Todo::findOrFail($id);
        $todo->update(['completed' => !$todo->completed]);
    }

    public function deleteTodo($id)
    {
        Todo::findOrFail($id)->delete();
    }

    public function with()
    {
        return ['todos' => Todo::latest()->get()];
    }
}; ?>

<div>
    <form wire:submit="addTodo">
        <input type="text" wire:model="newTodoTitle" placeholder="New todo...">
        <button type="submit">Add</button>
    </form>

    <div class="space-y-2">
        @forelse($todos as $todo)
            <div>
                <input
                    type="checkbox"
                  
View on GitHub
GitHub Stars36
CategoryDevelopment
Updated13d ago
Forks0

Languages

TypeScript

Security Score

90/100

Audited on Mar 19, 2026

No findings