joshcirre/duo
Duo brings local-first IndexedDB syncing to Laravel and Livewire. Add a single trait and your Livewire components become Alpine-powered, caching Eloquent models in the browser with offline support, optimistic updates, and automatic schema extraction.
Architecture fit. Duo adds a client-side persistence tier (IndexedDB via Dexie) to a standard Laravel + Livewire 3/Volt stack, plus a service worker and a Vite plugin. That is a materially larger architectural commitment than the one-line composer require suggests: you are introducing a second database, a second schema lifecycle, a build-time code generator, and a browser-side sync client into an app whose server-side persistence is currently the single source of truth. It fits applications where the primary product requirement is offline-first field/workflow capture on repeat-visit surfaces (inspections, checklists, POS, note-taking). It does not fit apps where offline is a nice-to-have or where the Livewire components are highly stateful.
Integration feasibility. The integration surface is unusually wide for a package of this size:
Syncable trait on models (schema extraction from DB columns), WithDuo trait on Livewire components.@duoMeta directive in <head>, optional <x-duo::sync-status> and debug panel.@joshcirre/vite-plugin-duo in vite.config.js with file-watching over app/Models/** and a manifest emitted to resources/js/duo/manifest.json.public/duo-sw.js and registered client-side.user() relationship must exist on user-scoped models, and user_id must not be in $fillable or the package's security model degrades.That last point is the tell. Security behavior is inferred from relationship naming and $fillable contents rather than configured explicitly — powerful when conventions hold, brittle when they don't, and hard to audit.
Technical risk. High, concentrated in three places:
WithDuo trait rewrites wire:click to @click, @forelse to x-for, {{ $x }} to x-text, and injects x-cloak. This is a source-to-source UI rewrite performed by the package. Any Livewire feature outside the supported grammar — validation error bags, computed properties, wire:model modifiers, file uploads, polling, dispatch/listeners, nested components, third-party Livewire packages — is a candidate for silent breakage. This is the single largest unknown and must be scoped by feature-by-feature testing, not by reading the README.Key questions for the maintainer / spike.
Stack fit. Qualified yes for: Laravel 11/12, Livewire 3+, Volt, Vite, Postgres/MySQL, SPA-ish or PWA-oriented front ends, single-tenant or cleanly tenant-scoped Eloquent models. Explicit no for: Inertia/React/Vue front ends (the transform is Livewire-specific), API-first architectures serving mobile clients, apps that rely on Livewire file uploads or heavy client-side validation on the components in scope, and anything where the affected models are large or highly relational (no evidence relationships are synced — the README's examples are all flat records).
Migration path. Do not treat this as an incremental upgrade to existing components. The realistic path is a carve-out: pick one new or low-traffic surface (e.g., a "my tasks" list, an offline inspection form), model it with Syncable + WithDuo from day one, and leave all existing Livewire components untouched. The shipped debug panel's toggle between Duo mode and standard Livewire mode is the rollback lever — validate that it works, and treat it as the kill switch.
Compatibility work to budget for.
resources/js/duo/manifest.json exists and matches expected columns for each Syncable model. Model changes that silently don't regenerate are the most likely production surprise.Syncable model for user() relationship presence and user_id fillable status as a review checklist item, not as tribal knowledge.Sequencing.
Maintenance. This is the dominant cost. One maintainer, no dependents, no visible ecosystem, and a README that opens with "VERY MUCH A WIP." Treat the package as a codebase you are adopting, not a dependency you are consuming. Budget for a named internal owner, a fork in your VCS, and the ability to patch both the PHP and TypeScript sides. Every Laravel or Livewire minor upgrade becomes a compatibility question for a package with no stated version support matrix.
Support. GitHub issues only, no commercial backing, no SLA. Internal escalation path must exist before pilot. The debug panel is a developer aid, not an operational tool — it is gated to the local environment.
Scaling. Read path scales well: repeat renders hit IndexedDB instead of the server, which is the actual value proposition and will show up as a real reduction in Livewire round-trips. Write path scales worse than before in one specific dimension — reconnect bursts. After an outage or a deploy, every offline client flushes its queue at once, producing a synchronized write spike and a correlated read-your-writes problem. Plan for sync-endpoint rate limiting and for server-side headroom sized to the burst, not the average. Also model storage: IndexedDB quotas are per-origin and evictable, especially on iOS Safari, so "the data is on the device" is not a durability guarantee.
Failure modes to instrument.
Ramp-up. Steeper than the "one trait" pitch implies. Engineers need working fluency in Livewire 3 internals, Alpine, IndexedDB/Dexie, and service worker lifecycle — four layers, and the abstraction actively hides three of them. Debugging moves from "read the stack trace" to "inspect the generated Alpine markup and the IndexedDB contents," which most Laravel teams are not set up for. Expect a 2–4 week ramp per engineer and a hard dependency on whoever ran the spike. Front-load the runbook: how to inspect IndexedDB in production, how to force a schema regeneration, how to drain a stuck queue, how to disable Duo per-user.
How can I help you explore Laravel packages today?