PHPackages                             joshcirre/duo - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. joshcirre/duo

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

joshcirre/duo
=============

Local-first IndexedDB syncing for Laravel and Livewire applications.

v0.1.7(6mo ago)3620[1 issues](https://github.com/joshcirre/duo/issues)MITTypeScriptPHP ^8.2

Since Oct 29Pushed 2mo agoCompare

[ Source](https://github.com/joshcirre/duo)[ Packagist](https://packagist.org/packages/joshcirre/duo)[ RSS](/packages/joshcirre-duo/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (7)Dependencies (16)Versions (9)Used By (0)

Duo (VERY MUCH A WIP)
=====================

[](#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
--------

[](#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
-----------------------

[](#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

[](#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

[](#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

[](#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

[](#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

[](#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)

[](#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

[](#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
------------

[](#installation)

### Composer Package

[](#composer-package)

```
composer require joshcirre/duo
```

### NPM Package (Vite Plugin)

[](#npm-package-vite-plugin)

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

> **Note:** Dexie is automatically installed as a dependency.

### Publishing Assets (Optional)

[](#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-config` → `config/duo.php` - Global configuration
- `duo-views` → `resources/views/vendor/duo/components/` - Blade components for customization
- `duo-assets` → `resources/js/vendor/duo/` - JavaScript source files (rarely needed)

See [Publishing Components](#publishing-components) in the Configuration section for customization examples.

Quick Start
-----------

[](#quick-start)

### 1. Add the Syncable Trait to Your Models

[](#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

[](#2-add-duometa-directive-to-your-layout)

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

```

    @duoMeta

    {{ $title ?? config('app.name') }}

```

The `@duoMeta` directive outputs:

- `` - Required for API sync requests
- `` - Tells the service worker to cache this page for offline access

### 3. Configure Vite

[](#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

[](#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:**

```
