<turbo-stream action="append" target="posts_list"><template>    <div class="postbit" id="386058" data-post-id="386058">
  <section>
    <div class="post-wrap">


					<div class="post-header">
		        <div class="user-avatar">
		          <img alt="bartblast" src="https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/bartblast/120/17647_2.png" width="120" height="120" />
		        </div>
					
						<div class="user-details">
		          <div class="user-name">
		            <h3>
                  bartblast
                      <span class="op-star" title="Thread Starter">
                        <img alt="OP" class="op-star-icon" src="/assets/thread-icons/thread-icon-thread-starter-df91e872.png" />
                      </span>
                  </h3>
		          </div>
						
			          <div class="user-title">
									<span>Creator of Hologram</span>
			          </div>
						</div>
					
					</div>

	        <div class="thread-main">
	            <div class="post-body" data-turbo="false">
								<aside class="quote no-group" data-username="esambo" data-post="6" data-topic="74862" data-full="true">
<div class="title">
<div class="quote-controls"></div>
<img alt="" width="24" height="24" src="https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/esambo/48/14415_2.png" class="avatar"> esambo:</div>
<blockquote>
<p>Local first would be awesome for LiveView Native, as native app users are used to instant responsiveness.</p>
</blockquote>
</aside>
<p>Worth noting that LiveView Native was discontinued - the core library is archived on GitHub. The project maintainer shared the reasons publicly.</p>
<p>Just to clarify - Hologram is a separate project to LiveView with a different architecture - it compiles Elixir to JavaScript and runs on the client, so it’s not related to LiveView Native. That said, mobile support is on the Hologram roadmap as well, and Local-First would definitely be important for that use case.</p> 
	            </div>

	            <div class="base-line">
	                <div class="thread-counters">
	                    <span class="thread-count count-likes js-likers-trigger" title="Likes" data-post-id="386058" data-batch-url="/posts/batch_likers">
                        1
                      </span>
                      <!-- <span class="thread-count js-solved-indicator" title="Marked as solution"></span> -->
	                </div>
	                <div class="go-to-post">
	                  <a title="Go to post" alt="Go to post" href="https://forum.elixirforum.com/t/designing-local-first-features-for-hologram-whats-your-dream-dx/74862/12">Post #11</a>
	                </div>
	            </div>
              <div id="likers-container-386058" 
                   class="likers-container"
                   data-first-post="false"
                   data-batch-url="/posts/batch_likers">
                   <div class="likers-placeholder" 
                     data-likers-post-id="386058"
                     data-batch-url="/posts/batch_likers">
                  <div class="post-likers"></div>
                </div>
              </div>
	        </div>
			

    </div>

    <div class="triangle-top-right type-standard-post cat-standard-post" title="Post #11"></div>
  </section>
</div>
    <div class="postbit" id="386066" data-post-id="386066">
  <section>
    <div class="post-wrap">


					<div class="post-header">
		        <div class="user-avatar">
		          <img alt="woylie" src="https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/woylie/120/38856_2.png" width="120" height="120" />
		        </div>
					
						<div class="user-details">
		          <div class="user-name">
		            <h3>
                  woylie
                  </h3>
		          </div>
						
						</div>
					
					</div>

	        <div class="thread-main">
	            <div class="post-body" data-turbo="false">
								<p>From a user and dogfooding perspective, the ideal app I’d want to build would indeed be local-first in the sense of online-optional. A PWA that runs offline by default without limitations, but can connect to an official sync server or a self-hosted one for backups and sharing data between devices or users. Even better if in addition to the browser version, I was able to install a desktop or mobile version that writes to an SQLite database on the local file system. So in the end, it would be a three-way sync between browser storage, online server, and local file system. If the data is trapped in IndexedDB, I don’t really own the data as a user.</p> 
	            </div>

	            <div class="base-line">
	                <div class="thread-counters">
	                    <span class="thread-count count-likes js-likers-trigger" title="Likes" data-post-id="386066" data-batch-url="/posts/batch_likers">
                        2
                      </span>
                      <!-- <span class="thread-count js-solved-indicator" title="Marked as solution"></span> -->
	                </div>
	                <div class="go-to-post">
	                  <a title="Go to post" alt="Go to post" href="https://forum.elixirforum.com/t/designing-local-first-features-for-hologram-whats-your-dream-dx/74862/13">Post #12</a>
	                </div>
	            </div>
              <div id="likers-container-386066" 
                   class="likers-container"
                   data-first-post="false"
                   data-batch-url="/posts/batch_likers">
                   <div class="likers-placeholder" 
                     data-likers-post-id="386066"
                     data-batch-url="/posts/batch_likers">
                  <div class="post-likers"></div>
                </div>
              </div>
	        </div>
			

    </div>

    <div class="triangle-top-right type-standard-post cat-standard-post" title="Post #12"></div>
  </section>
</div>
    <div class="postbit" id="386087" data-post-id="386087">
  <section>
    <div class="post-wrap">


					<div class="post-header">
		        <div class="user-avatar">
		          <img alt="kingdomcoder" src="https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/kingdomcoder/120/24667_2.png" width="120" height="120" />
		        </div>
					
						<div class="user-details">
		          <div class="user-name">
		            <h3>
                  kingdomcoder
                  </h3>
		          </div>
						
						</div>
					
					</div>

	        <div class="thread-main">
	            <div class="post-body" data-turbo="false">
								<p>I think that Hologram as it stands today already does a world-class job separating client-side and server-side concerns. Extending that principle into managing offline data is how I would approach this problem.</p>
<p>Let me explain the DX I wish for with some code examples. (Caveat: I will include Ash-like snippets in my examples, but that’s primarily because I work in Ash every day. I am much more comfortable expressing myself in it)</p>
<p>Imagine a simple Hologram page. Right now, opening and closing a ticket works this way:</p>
<pre data-code-wrap="elixir"><code class="lang-elixir">defmodule MyApp.TicketListPage do
  use Hologram.Page

  # ... route, layout, init, template omitted for brevity ...

  def action(:open_ticket, _params, component) do
    subject = component.state.new_subject
    component
    |&gt; put_state(:new_subject, "")
    |&gt; put_command(:create_ticket, %{subject: subject})
  end

  def action(:close_ticket, params, component) do
    component
    |&gt; put_command(:update_ticket, %{id: params.id, status: :closed})
  end

  def command(:create_ticket, params, server) do
    case do_open_ticket(params.subject) do
      {:ok, ticket} -&gt; put_action(server, :ticket_created, %{ticket: ticket})
      {:error, _} -&gt; put_action(server, :create_failed, %{})
    end
  end

  def command(:update_ticket, params, server) do
    case do_close_ticket(params.id) do
      {:ok, ticket} -&gt; put_action(server, :ticket_updated, %{ticket: ticket})
      {:error, _} -&gt; put_action(server, :update_failed, %{})
    end
  end

  defp do_open_ticket(subject) do
    # Does something... Starts an Oban job, calls an external API, sends an email, etc
    MyApp.Support.Ticket.open(subject)
    ...
  end

  defp do_close_ticket(id) do
    # Also does some very important server-side things
    MyApp.Support.Ticket.close(id)
    ...
  end
end
</code></pre>
<p>With a ticket resource that looks like this:</p>
<pre data-code-wrap="elixir"><code class="lang-elixir">defmodule MyApp.Support.Ticket do
  use Ash.Resource,
    domain: MyApp.Support,
    data_layer: AshPostgres.DataLayer,
    authorizers: [Ash.Policy.Authorizer]

  postgres do
    table "tickets"
    repo MyApp.Repo
  end

  attributes do
    uuid_primary_key :id

    attribute :subject, :string, allow_nil?: false, public?: true
    attribute :status, :atom do
      constraints one_of: [:open, :closed]
      default :open
      allow_nil? false
    end
  end

  relationships do
    belongs_to :representative, MyApp.Support.Representative
  end

  policies do
    policy action_type(:read) do
      authorize_if expr(representative_id == ^actor(:id))
    end

    policy action_type(:create) do
      authorize_if always()
    end

    policy action(:close) do
      authorize_if expr(representative_id == ^actor(:id))
    end
  end

  actions do
    defaults [:read]

    create :open do
      accept [:subject]
    end
    
    update :close do
      accept []
      argument :id, :uuid, allow_nil?: false
      validate attribute_does_not_equal(:status, :closed) do
        message "Ticket is already closed"
      end
      change set_attribute(:status, :closed)
    end
  end

  code_interface do
    define :open, args: [:subject]
    define :close, args: [:id]
  end
end
</code></pre>
<p>In my dream DX, upgrading what we have today to a local first app will only need these changes:</p>
<p>Configure an extension provided by Hologram:</p>
<pre data-code-wrap="elixir"><code class="lang-elixir">defmodule MyApp.Support.Ticket do
  use Ash.Resource,
    domain: MyApp.Support,
    data_layer: AshPostgres.DataLayer,
    authorizers: [Ash.Policy.Authorizer],
    extensions: [Hologram.Extension.IndexedDb] # add the local datastore you want to use

  # ... everything else stays the same ...

  indexed_db do
    store "tickets"
    scope :authorized  # sync everything the actor is authorized to read
    resolution_strategy :last_write_wins # can be the simple strategy for a start.  future versions can even allow anonymous functions for custom strategies
  end
end
</code></pre>
<p>On the page, actions now write to the local store first and then dispatch commands:</p>
<pre data-code-wrap="elixir"><code class="lang-elixir">defmodule MyApp.TicketListPage do
  use Hologram.Page

  # ... route, layout, template omitted for brevity ...

  def init(_params, component, _server) do
    # Reads come from the local store -- no server round-trip, no spinner.
    tickets = MyApp.Support.Ticket.read!()
    put_state(component, %{tickets: tickets, new_subject: ""})
  end

  def action(:open_ticket, _params, component) do
    subject = component.state.new_subject

    # 1. Write to local store immediately (optimistic update)
    {:ok, ticket} = MyApp.Support.Ticket.open(subject)
    tickets = MyApp.Support.Ticket.read!()

    component
    |&gt; put_state(%{tickets: tickets, new_subject: ""})
    |&gt; put_command(:create_ticket, %{subject: subject})
    # 2. Command syncs to server in the background
  end

  def action(:close_ticket, params, component) do
    # Write to local store immediately
    {:ok, _ticket} = MyApp.Support.Ticket.close(params.id)
    tickets = MyApp.Support.Ticket.read!()

    component
    |&gt; put_state(:tickets, tickets)
    |&gt; put_command(:update_ticket, %{id: params.id})
  end

  # Commands stay exactly the same -- they still do the important
  # server-side work (Oban jobs, external APIs, etc.)
  def command(:create_ticket, params, server) do
    case do_open_ticket(params.subject) do
      {:ok, ticket} -&gt; put_action(server, :ticket_created, %{ticket: ticket})
      {:error, _} -&gt; put_action(server, :create_failed, %{})
    end
  end

  def command(:update_ticket, params, server) do
    case do_close_ticket(params.id) do
      {:ok, ticket} -&gt; put_action(server, :ticket_updated, %{ticket: ticket})
      {:error, _} -&gt; put_action(server, :update_failed, %{})
    end
  end

  defp do_open_ticket(subject) do
    MyApp.Support.Ticket.open(subject)
    ...
  end

  defp do_close_ticket(id) do
    MyApp.Support.Ticket.close(id)
    ...
  end
end
</code></pre>
<p>This approach is beautiful to me because it’s declarative and explicit, and it preserves the mental model that the developer already works with (client-side concerns happen in actions and server-side concerns in commands, no magic). Calling <code>MyApp.Support.Ticket.open</code> in both the action and the command might initially seem redundant. But to me, it is explicit. It’s not redundant.</p>
<p>In addition, this approach suddenly unlocks several new possibilities! Local-first, local-only, local-never (server-only), <a class="mention" href="/u/woylie" rel="nofollow">@woylie</a>‘s vision of 3-way-sync, or any other permutation of interest, all by configuring resource extensions and choosing where to call a write function:</p>
<ul>
<li><strong>Local-only:</strong> Action writes to local store. No command dispatched. Data never leaves the client. Use case: drafts, preferences, UI state.</li>
<li><strong>Local-first:</strong> Action writes to local store AND dispatches a command. UI updates instantly, server syncs in the background. Use case: most app data. This is what the example above does.</li>
<li><strong>Server-only:</strong> Action dispatches a command without writing locally. Waits for server response. Use case: payments, sensitive operations.</li>
<li><strong>Three-way sync:</strong> Same resource configured with multiple extensions (e.g., IndexedDB in the browser, SQLite on the filesystem, Postgres on the server). Each syncs via the same command queue. Use case: <a class="mention" href="/u/woylie" rel="nofollow">@woylie</a>’s vision of PWA + desktop + self-hosted server.</li>
</ul>
<p>The wiring to achieve this is non-trivial. Perhaps Hologram has a <strong>command queue</strong> already for handling commands when disconnected, but that’s a key piece. When a command dispatch fails, it queues locally and retries with backoff. On reconnection, queued commands replay through the same server-side business logic (<a class="mention" href="/u/jam" rel="nofollow">@jam</a>’s proposal). Also simplifies the retry logic.</p>
<hr>
<p>So, in summary, mapping back to <a class="mention" href="/u/bartblast" rel="nofollow">@bartblast</a>’s original questions:</p>
<h3><a name="p-386087-h-1-declarative-sync-1" class="anchor" href="#p-386087-h-1-declarative-sync-1" aria-label="Heading link" rel="nofollow"></a>1. Declarative sync</h3>
<p>Configure an extension on the resource. Authorization rules determine what syncs. Actions and commands control write locality. Define the model once, declare where it lives, write normal Elixir.</p>
<h3><a name="p-386087-h-2-conflict-resolution-2" class="anchor" href="#p-386087-h-2-conflict-resolution-2" aria-label="Heading link" rel="nofollow"></a>2. Conflict resolution</h3>
<p>Declared on the resource via the extension’s DSL (<code>resolution_strategy :last_write_wins</code>). Queued commands replay through business logic for natural conflict detection. Future versions can support per-field strategies and custom resolver functions.</p>
<h3><a name="p-386087-h-3-offline-experience-3" class="anchor" href="#p-386087-h-3-offline-experience-3" aria-label="Heading link" rel="nofollow"></a>3. Offline experience</h3>
<p>Transparent. Actions work against the local store regardless of connectivity. Commands queue when unreachable and replay on reconnection.</p>
<h3><a name="p-386087-h-4-inspiration-4" class="anchor" href="#p-386087-h-4-inspiration-4" aria-label="Heading link" rel="nofollow"></a>4. Inspiration</h3>
<p>The biggest inspiration is Ash’s <strong>data-layer agnosticism</strong> – same resource definition, multiple storage backends. <a class="mention" href="/u/jam" rel="nofollow">@jam</a> referenced Meteor’s “it just works” DX as the bar. I think the action/command model gets us there.</p>
<p>Really excited about this discussion!</p> 
	            </div>

	            <div class="base-line">
	                <div class="thread-counters">
	                    <span class="thread-count count-likes js-likers-trigger" title="Likes" data-post-id="386087" data-batch-url="/posts/batch_likers">
                        6
                      </span>
                      <!-- <span class="thread-count js-solved-indicator" title="Marked as solution"></span> -->
	                </div>
	                <div class="go-to-post">
	                  <a title="Go to post" alt="Go to post" href="https://forum.elixirforum.com/t/designing-local-first-features-for-hologram-whats-your-dream-dx/74862/14">Post #13</a>
	                </div>
	            </div>
              <div id="likers-container-386087" 
                   class="likers-container"
                   data-first-post="false"
                   data-batch-url="/posts/batch_likers">
                   <div class="likers-placeholder" 
                     data-likers-post-id="386087"
                     data-batch-url="/posts/batch_likers">
                  <div class="post-likers"></div>
                </div>
              </div>
	        </div>
			

    </div>

    <div class="triangle-top-right type-most-liked cat-most-liked" title="One of the top 3 liked posts in this thread!"></div>
  </section>
</div>
    <div class="postbit" id="386104" data-post-id="386104">
  <section>
    <div class="post-wrap">


					<div class="post-header">
		        <div class="user-avatar">
		          <img alt="effinbanjos" src="https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/effinbanjos/120/16608_2.png" width="120" height="120" />
		        </div>
					
						<div class="user-details">
		          <div class="user-name">
		            <h3>
                  effinbanjos
                  </h3>
		          </div>
						
						</div>
					
					</div>

	        <div class="thread-main">
	            <div class="post-body" data-turbo="false">
								<aside class="quote no-group" data-username="bartblast" data-post="9" data-topic="74862">
<div class="title">
<div class="quote-controls"></div>
<img alt="" width="24" height="24" src="https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/bartblast/48/17647_2.png" class="avatar"> bartblast:</div>
<blockquote>
<p>By the way, Hologram doesn’t use WebSockets - it communicates via standard HTTP requests with a keep-alive mechanism. But the challenge applies regardless of transport.</p>
</blockquote>
</aside>
<p>I guess HTTP retries are more predictable durable - don’t get filtered by firewalls, the mobile networking issues <a class="mention" href="/u/derek-zhou" rel="nofollow">@derek-zhou</a> mentioned? Just curious about this design choice (I have a somewhat relevant project in this area coming up).</p>
<p>The big picture for me - what I would personally want/love out of hologram - is zero/low latency interactivity on the client for clicks/touches, graceful handling of <em>temporary</em> network disruptions (seconds/minutes not days), guaranteed order from a single node/client, near real-time updates on the client, and an option to choose a concurrent write resolution strategy for multiple nodes/clients.</p>
<p>Some strategies I might want:</p>
<ol>
<li>First One Wins (one gets an error)</li>
<li>Throw them both away (both get errors)</li>
<li>Configurable priority (that defaults to the two other strategies when messages are tied)
<ol>
<li>Not exactly sure how this would work on the client but a use-case would be to prefer a in-store terminal over a shopping cart on the website.</li>
</ol>
</li>
</ol>
<p>I’d say 1 would be my first pick if I had to choose.</p>
<p>Just an implementation detail but with the http retry I suppose we also have to account for single node message ordering? Maybe a genserver buffer + sort based on a client-provided lamport or HLC - then merge multiple nodes…somehow? Interesting problems!</p>
<p>Also, hologram looks awesome! <img src="https://forum.elixirforum.com/images/emoji/apple/purple_heart.png?v=15" title=":purple_heart:" class="emoji" alt=":purple_heart:" loading="lazy" width="20" height="20"></p> 
	            </div>

	            <div class="base-line">
	                <div class="thread-counters">
	                    <span class="thread-count count-likes js-likers-trigger" title="Likes" data-post-id="386104" data-batch-url="/posts/batch_likers">
                        1
                      </span>
                      <!-- <span class="thread-count js-solved-indicator" title="Marked as solution"></span> -->
	                </div>
	                <div class="go-to-post">
	                  <a title="Go to post" alt="Go to post" href="https://forum.elixirforum.com/t/designing-local-first-features-for-hologram-whats-your-dream-dx/74862/15">Post #14</a>
	                </div>
	            </div>
              <div id="likers-container-386104" 
                   class="likers-container"
                   data-first-post="false"
                   data-batch-url="/posts/batch_likers">
                   <div class="likers-placeholder" 
                     data-likers-post-id="386104"
                     data-batch-url="/posts/batch_likers">
                  <div class="post-likers"></div>
                </div>
              </div>
	        </div>
			

    </div>

    <div class="triangle-top-right type-standard-post cat-standard-post" title="Post #14"></div>
  </section>
</div>
    <div class="postbit" id="386112" data-post-id="386112">
  <section>
    <div class="post-wrap">


					<div class="post-header">
		        <div class="user-avatar">
		          <img alt="zachdaniel" src="https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/zachdaniel/120/31980_2.png" width="120" height="120" />
		        </div>
					
						<div class="user-details">
		          <div class="user-name">
		            <h3>
                  zachdaniel
                  </h3>
		          </div>
						
			          <div class="user-title">
									<span>Creator of Ash</span>
			          </div>
						</div>
					
					</div>

	        <div class="thread-main">
	            <div class="post-body" data-turbo="false">
								<p>FWIW I do personally believe that using a declarative data backend like Ash is how this should actually be accomplished. Being able to rely on a data-layer agnostic storage layer that is introspectable and extensible is how I’ve personally always been planning on doing this.</p>
<p>With Ash you can ask a resource “can your data layer do X type of thing”, meaning that hologram can react to differently-capable data layers to make strategic choices, i.e if the data layer can transact, then you can batch operations. Your extension could add additional attributes to the resource even. Sky is the limit.</p> 
	            </div>

	            <div class="base-line">
	                <div class="thread-counters">
	                    <span class="thread-count count-likes js-likers-trigger" title="Likes" data-post-id="386112" data-batch-url="/posts/batch_likers">
                        10
                      </span>
                      <!-- <span class="thread-count js-solved-indicator" title="Marked as solution"></span> -->
	                </div>
	                <div class="go-to-post">
	                  <a title="Go to post" alt="Go to post" href="https://forum.elixirforum.com/t/designing-local-first-features-for-hologram-whats-your-dream-dx/74862/16">Post #15</a>
	                </div>
	            </div>
              <div id="likers-container-386112" 
                   class="likers-container"
                   data-first-post="false"
                   data-batch-url="/posts/batch_likers">
                   <div class="likers-placeholder" 
                     data-likers-post-id="386112"
                     data-batch-url="/posts/batch_likers">
                  <div class="post-likers"></div>
                </div>
              </div>
	        </div>
			

    </div>

    <div class="triangle-top-right type-most-liked cat-most-liked" title="One of the top 3 liked posts in this thread!"></div>
  </section>
</div>
    <div class="postbit" id="386162" data-post-id="386162">
  <section>
    <div class="post-wrap">


					<div class="post-header">
		        <div class="user-avatar">
		          <img alt="bartblast" src="https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/bartblast/120/17647_2.png" width="120" height="120" />
		        </div>
					
						<div class="user-details">
		          <div class="user-name">
		            <h3>
                  bartblast
                      <span class="op-star" title="Thread Starter">
                        <img alt="OP" class="op-star-icon" src="/assets/thread-icons/thread-icon-thread-starter-df91e872.png" />
                      </span>
                  </h3>
		          </div>
						
			          <div class="user-title">
									<span>Creator of Hologram</span>
			          </div>
						</div>
					
					</div>

	        <div class="thread-main">
	            <div class="post-body" data-turbo="false">
								<aside class="quote no-group" data-username="derek-zhou" data-post="10" data-topic="74862">
<div class="title">
<div class="quote-controls"></div>
<img alt="" width="24" height="24" src="https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/derek-zhou/48/19943_2.png" class="avatar"> derek-zhou:</div>
<blockquote>
<p>I was under the impression that Hologram use websocket, just not Phoenix Socket. Maybe I was hallucinating.</p>
</blockquote>
</aside>
<p>You didn’t hallucinate! Hologram did use WebSockets for commands and page fetching in earlier versions. I moved away from that in v0.5.0 - mainly because cookies and sessions can only be set via HTTP, and the workaround (a CRDT-based cookie store syncing across nodes) was way too complex. Plus other practical issues like corporate firewalls blocking WebSockets and so on.</p>
<p>The key realization was that Hologram doesn’t need WebSockets the way LiveView does. Since the code runs in the browser, the server only gets hit for page fetching and commands - not for every user interaction. HTTP persistent connections work great for that.</p> 
	            </div>

	            <div class="base-line">
	                <div class="thread-counters">
	                    <span class="thread-count count-likes js-likers-trigger" title="Likes" data-post-id="386162" data-batch-url="/posts/batch_likers">
                        4
                      </span>
                      <!-- <span class="thread-count js-solved-indicator" title="Marked as solution"></span> -->
	                </div>
	                <div class="go-to-post">
	                  <a title="Go to post" alt="Go to post" href="https://forum.elixirforum.com/t/designing-local-first-features-for-hologram-whats-your-dream-dx/74862/17">Post #16</a>
	                </div>
	            </div>
              <div id="likers-container-386162" 
                   class="likers-container"
                   data-first-post="false"
                   data-batch-url="/posts/batch_likers">
                   <div class="likers-placeholder" 
                     data-likers-post-id="386162"
                     data-batch-url="/posts/batch_likers">
                  <div class="post-likers"></div>
                </div>
              </div>
	        </div>
			

    </div>

    <div class="triangle-top-right type-most-liked cat-most-liked" title="One of the top 3 liked posts in this thread!"></div>
  </section>
</div>
    <div class="postbit" id="386163" data-post-id="386163">
  <section>
    <div class="post-wrap">


					<div class="post-header">
		        <div class="user-avatar">
		          <img alt="bartblast" src="https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/bartblast/120/17647_2.png" width="120" height="120" />
		        </div>
					
						<div class="user-details">
		          <div class="user-name">
		            <h3>
                  bartblast
                      <span class="op-star" title="Thread Starter">
                        <img alt="OP" class="op-star-icon" src="/assets/thread-icons/thread-icon-thread-starter-df91e872.png" />
                      </span>
                  </h3>
		          </div>
						
			          <div class="user-title">
									<span>Creator of Hologram</span>
			          </div>
						</div>
					
					</div>

	        <div class="thread-main">
	            <div class="post-body" data-turbo="false">
								<aside class="quote no-group" data-username="esambo" data-post="8" data-topic="74862" data-full="true">
<div class="title">
<div class="quote-controls"></div>
<img alt="" width="24" height="24" src="https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/esambo/48/14415_2.png" class="avatar"> esambo:</div>
<blockquote>
<p>Some iOS apps offer a sync feature that makes this <strong>local first</strong> concept more explicit, where the apps is first and foremost a local app, and sync is secondary. Some apps that come to mind are:</p>
<ul>
<li><a href="https://apps.apple.com/us/app/day-one-daily-journal-diary/id1044867788" rel="noopener nofollow ugc">Day One</a> (offers a “Reset Sync” in the app which indicates that the <em>source of truth</em> is the cloud version. It is also very explicit about the <a href="https://dayoneapp.com/guides/day-one-sync/sync-status/" rel="noopener nofollow ugc">Sync Status</a>. Each item also has an <a href="https://dayoneapp.com/guides/troubleshooting/entry-version-history/" rel="noopener nofollow ugc">Entry Version History</a>)</li>
<li><a href="https://apps.apple.com/us/app/strongbox-password-manager/id897283731" rel="noopener nofollow ugc">Strongbox</a> (allows user to explicitly define what the <a href="https://strongbox.reamaze.com/kb/sync/what-is-wi-fi-sync-and-how-do-i-use-it#:~:text=complexity%20as%20possible.-,Source%20Device,-To%20use%20Wi" rel="noopener nofollow ugc">Source Device</a> (and then resolve any merge conflicts accordingly, which isn’t too bad, as each entry has a <em>history</em> versions exposed in the app). It also offers a <a href="https://strongboxsafe.com/support/#reamaze#0#/kb/sync/what-is-lazy-sync" rel="noopener nofollow ugc">Lazy Sync</a>.)</li>
</ul>
<p>And then there are web apps that offer an <em>offline mode</em>:</p>
<ul>
<li>Google Docs, MS Office, Zoho, Proton, WPS</li>
</ul>
<p><strong>Git</strong> as probably also a good example of different merge conflict strategies, and 3-way-merge.</p>
</blockquote>
</aside>
<p>Interesting examples, thanks! Sync status and version history are definitely on my radar.</p> 
	            </div>

	            <div class="base-line">
	                <div class="thread-counters">
	                    <span class="thread-count count-likes js-likers-trigger" title="Likes" data-post-id="386163" data-batch-url="/posts/batch_likers">
                        0
                      </span>
                      <!-- <span class="thread-count js-solved-indicator" title="Marked as solution"></span> -->
	                </div>
	                <div class="go-to-post">
	                  <a title="Go to post" alt="Go to post" href="https://forum.elixirforum.com/t/designing-local-first-features-for-hologram-whats-your-dream-dx/74862/18">Post #17</a>
	                </div>
	            </div>
              <div id="likers-container-386163" 
                   class="likers-container"
                   data-first-post="false"
                   data-batch-url="/posts/batch_likers">
                   <div class="likers-placeholder" 
                     data-likers-post-id="386163"
                     data-batch-url="/posts/batch_likers">
                  <div class="post-likers"></div>
                </div>
              </div>
	        </div>
			

    </div>

    <div class="triangle-top-right type-standard-post cat-standard-post" title="Post #17"></div>
  </section>
</div>
    <div class="postbit" id="386167" data-post-id="386167">
  <section>
    <div class="post-wrap">


					<div class="post-header">
		        <div class="user-avatar">
		          <img alt="bartblast" src="https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/bartblast/120/17647_2.png" width="120" height="120" />
		        </div>
					
						<div class="user-details">
		          <div class="user-name">
		            <h3>
                  bartblast
                      <span class="op-star" title="Thread Starter">
                        <img alt="OP" class="op-star-icon" src="/assets/thread-icons/thread-icon-thread-starter-df91e872.png" />
                      </span>
                  </h3>
		          </div>
						
			          <div class="user-title">
									<span>Creator of Hologram</span>
			          </div>
						</div>
					
					</div>

	        <div class="thread-main">
	            <div class="post-body" data-turbo="false">
								<aside class="quote no-group" data-username="woylie" data-post="13" data-topic="74862" data-full="true">
<div class="title">
<div class="quote-controls"></div>
<img alt="" width="24" height="24" src="https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/woylie/48/38856_2.png" class="avatar"> woylie:</div>
<blockquote>
<p>From a user and dogfooding perspective, the ideal app I’d want to build would indeed be local-first in the sense of online-optional. A PWA that runs offline by default without limitations, but can connect to an official sync server or a self-hosted one for backups and sharing data between devices or users. Even better if in addition to the browser version, I was able to install a desktop or mobile version that writes to an SQLite database on the local file system. So in the end, it would be a three-way sync between browser storage, online server, and local file system. If the data is trapped in IndexedDB, I don’t really own the data as a user.</p>
</blockquote>
</aside>
<p>That aligns well with where Hologram is heading - cross-platform from a single codebase (browser, desktop, mobile), with the local-first layer working the same across all of them. Multi-device sync should work out of the box. Good point about data ownership and not being trapped in IndexedDB - I’m currently leaning toward OPFS rather than IndexedDB for the storage layer, though I still need to do proper benchmarking before committing to that. I’ll take the data ownership aspect into account too.</p> 
	            </div>

	            <div class="base-line">
	                <div class="thread-counters">
	                    <span class="thread-count count-likes js-likers-trigger" title="Likes" data-post-id="386167" data-batch-url="/posts/batch_likers">
                        3
                      </span>
                      <!-- <span class="thread-count js-solved-indicator" title="Marked as solution"></span> -->
	                </div>
	                <div class="go-to-post">
	                  <a title="Go to post" alt="Go to post" href="https://forum.elixirforum.com/t/designing-local-first-features-for-hologram-whats-your-dream-dx/74862/19">Post #18</a>
	                </div>
	            </div>
              <div id="likers-container-386167" 
                   class="likers-container"
                   data-first-post="false"
                   data-batch-url="/posts/batch_likers">
                   <div class="likers-placeholder" 
                     data-likers-post-id="386167"
                     data-batch-url="/posts/batch_likers">
                  <div class="post-likers"></div>
                </div>
              </div>
	        </div>
			

    </div>

    <div class="triangle-top-right type-standard-post cat-standard-post" title="Post #18"></div>
  </section>
</div>
    <div class="postbit" id="386211" data-post-id="386211">
  <section>
    <div class="post-wrap">


					<div class="post-header">
		        <div class="user-avatar">
		          <img alt="bartblast" src="https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/bartblast/120/17647_2.png" width="120" height="120" />
		        </div>
					
						<div class="user-details">
		          <div class="user-name">
		            <h3>
                  bartblast
                      <span class="op-star" title="Thread Starter">
                        <img alt="OP" class="op-star-icon" src="/assets/thread-icons/thread-icon-thread-starter-df91e872.png" />
                      </span>
                  </h3>
		          </div>
						
			          <div class="user-title">
									<span>Creator of Hologram</span>
			          </div>
						</div>
					
					</div>

	        <div class="thread-main">
	            <div class="post-body" data-turbo="false">
								<aside class="quote no-group quote-modified" data-username="kingdomcoder" data-post="14" data-topic="74862" data-full="true">
<div class="title">
<div class="quote-controls"></div>
<img alt="" width="24" height="24" src="https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/kingdomcoder/48/24667_2.png" class="avatar"> kingdomcoder:</div>
<blockquote>
<p>I think that Hologram as it stands today already does a world-class job separating client-side and server-side concerns. Extending that principle into managing offline data is how I would approach this problem.</p>
<p>Let me explain the DX I wish for with some code examples. (Caveat: I will include Ash-like snippets in my examples, but that’s primarily because I work in Ash every day. I am much more comfortable expressing myself in it)</p>
<p>Imagine a simple Hologram page. Right now, opening and closing a ticket works this way:</p>
<p>(…)</p>
</blockquote>
</aside>
<p>Thanks for that incredibly thorough reply, the code examples really help ground the discussion!</p>
<p>I liked the local-only, local-first, server-only, three-way sync breakdown. I’m thinking along similar lines - and ideally this could be granular enough to configure per-model or even per-field if needed.</p>
<p>I’d push back a little on calling <code>Ticket.open</code> in both the action and the command. The whole goal of a local-first approach is to remove that kind of plumbing - the developer writes to the local store once, and the framework handles syncing to the server declaratively in the background. So the ideal DX would be closer to:</p>
<pre data-code-wrap="elixir"><code class="lang-elixir">def action(:open_ticket, _params, component) do
  subject = component.state.new_subject
  {:ok, _ticket} = Ticket.open(subject)
  # Done. Framework syncs to server, handles conflicts, retries.

  put_state(component, :new_subject, nil)
end
</code></pre>
<p>No <code>put_command</code>, no server callback for the write itself. The framework knows <code>Ticket</code> is a synced model, so it writes locally and queues the sync automatically. The command queue you mentioned (retries, backoff, maybe replay on reconnection?, etc.) is a key piece - but it should be entirely opaque to the developer. They just write, and the framework takes care of the rest.</p>
<p>That still leaves inherently server-side effects - sending emails, calling external APIs, starting Oban jobs. Today those live in commands, but we could potentially push this further with some kind of declarative workflow mechanism tied to the model itself - server-side effects that trigger automatically when a synced write lands on the server. That way the page code stays focused purely on intent and state, no manual command wiring at all. But that’s a separate discussion.</p>
<p>Regarding the integration angle - I think the right approach is for Hologram to provide its own sync protocol and conflict resolution hooks as native primitives (think “Hologram sync protocol”, “Hologram conflict resolution hooks”). This would make Hologram’s local-first layer extendable, so any library or custom solution can plug into it.</p>
<p>Really appreciate the enthusiasm and depth here <img src="https://forum.elixirforum.com/images/emoji/apple/slight_smile.png?v=15" title=":slight_smile:" class="emoji" alt=":slight_smile:" loading="lazy" width="20" height="20"></p> 
	            </div>

	            <div class="base-line">
	                <div class="thread-counters">
	                    <span class="thread-count count-likes js-likers-trigger" title="Likes" data-post-id="386211" data-batch-url="/posts/batch_likers">
                        2
                      </span>
                      <!-- <span class="thread-count js-solved-indicator" title="Marked as solution"></span> -->
	                </div>
	                <div class="go-to-post">
	                  <a title="Go to post" alt="Go to post" href="https://forum.elixirforum.com/t/designing-local-first-features-for-hologram-whats-your-dream-dx/74862/20">Post #19</a>
	                </div>
	            </div>
              <div id="likers-container-386211" 
                   class="likers-container"
                   data-first-post="false"
                   data-batch-url="/posts/batch_likers">
                   <div class="likers-placeholder" 
                     data-likers-post-id="386211"
                     data-batch-url="/posts/batch_likers">
                  <div class="post-likers"></div>
                </div>
              </div>
	        </div>
			

    </div>

    <div class="triangle-top-right type-standard-post cat-standard-post" title="Post #19"></div>
  </section>
</div>
    <div class="postbit" id="386230" data-post-id="386230">
  <section>
    <div class="post-wrap">


					<div class="post-header">
		        <div class="user-avatar">
		          <img alt="bartblast" src="https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/bartblast/120/17647_2.png" width="120" height="120" />
		        </div>
					
						<div class="user-details">
		          <div class="user-name">
		            <h3>
                  bartblast
                      <span class="op-star" title="Thread Starter">
                        <img alt="OP" class="op-star-icon" src="/assets/thread-icons/thread-icon-thread-starter-df91e872.png" />
                      </span>
                  </h3>
		          </div>
						
			          <div class="user-title">
									<span>Creator of Hologram</span>
			          </div>
						</div>
					
					</div>

	        <div class="thread-main">
	            <div class="post-body" data-turbo="false">
								<aside class="quote no-group quote-modified" data-username="effinbanjos" data-post="15" data-topic="74862" data-full="true">
<div class="title">
<div class="quote-controls"></div>
<img alt="" width="24" height="24" src="https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/effinbanjos/48/16608_2.png" class="avatar"> effinbanjos:</div>
<blockquote>
<aside class="quote no-group" data-username="bartblast" data-post="9" data-topic="74862">
<div class="title">
<div class="quote-controls"></div>
<img alt="" width="24" height="24" src="https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/bartblast/48/17647_2.png" class="avatar"> bartblast:</div>
<blockquote>
<p>By the way, Hologram doesn’t use WebSockets - it communicates via standard HTTP requests with a keep-alive mechanism. But the challenge applies regardless of transport.</p>
</blockquote>
</aside>
<p>I guess HTTP retries are more predictable durable - don’t get filtered by firewalls, the mobile networking issues <a class="mention" href="/u/derek-zhou" rel="nofollow">@derek-zhou</a> mentioned? Just curious about this design choice (I have a somewhat relevant project in this area coming up).</p>
<p>The big picture for me - what I would personally want/love out of hologram - is zero/low latency interactivity on the client for clicks/touches, graceful handling of <em>temporary</em> network disruptions (seconds/minutes not days), guaranteed order from a single node/client, near real-time updates on the client, and an option to choose a concurrent write resolution strategy for multiple nodes/clients.</p>
<p>Some strategies I might want:</p>
<ol>
<li>First One Wins (one gets an error)</li>
<li>Throw them both away (both get errors)</li>
<li>Configurable priority (that defaults to the two other strategies when messages are tied)</li>
<li>Not exactly sure how this would work on the client but a use-case would be to prefer a in-store terminal over a shopping cart on the website.</li>
</ol>
<p>I’d say 1 would be my first pick if I had to choose.</p>
<p>Just an implementation detail but with the http retry I suppose we also have to account for single node message ordering? Maybe a genserver buffer + sort based on a client-provided lamport or HLC - then merge multiple nodes…somehow? Interesting problems!</p>
<p>Also, hologram looks awesome! <img src="https://forum.elixirforum.com/images/emoji/apple/purple_heart.png?v=15" title=":purple_heart:" class="emoji" alt=":purple_heart:" loading="lazy" width="20" height="20"></p>
</blockquote>
</aside>
<p>Thanks for the kind words and the great breakdown!</p>
<p><strong>On the HTTP choice</strong> - I touched on this a bit <a href="https://forum.elixirforum.com/t/designing-local-first-features-for-hologram-whats-your-dream-dx/74862/17" rel="nofollow">in my reply above</a>, but to expand on your specific question: you’re right that HTTP is more predictable and durable as a transport. That was definitely part of the reasoning. The bigger driver though was that WebSockets forced me into distributed state for cookies and sessions - I had a CRDT-based cookie store syncing across nodes, and while it worked, it was a debugging nightmare. Cookies and sessions just work naturally with request-response. And then the practical stuff adds up - firewalls, no HTTP caching, mobile reconnection being flakier with persistent sockets, and each WebSocket holding a persistent process and connection state per client which eats more memory. The key insight was that Hologram doesn’t need WebSockets the way LiveView does - since the code runs in the browser, the server only gets hit for page fetching and commands, not every interaction. HTTP persistent connections are plenty for that, and it maps nicely onto offline queuing for local-first too.</p>
<p><strong>On your wishlist</strong> - good news on the zero-latency interactivity front - Hologram already gives you that! Since the code runs directly in the browser, clicks and touches are handled on the client in step with the browser’s event loop - no server round-trip needed.</p>
<p>Near real-time updates on the client is definitely on the list too - that’s where the sync layer comes in.</p>
<p>As for offline resilience - the seconds/minutes case is definitely the most common scenario. Ideally I’d love for it to work offline indefinitely on the existing schema - that way we don’t have to make compromises. Not sure yet if that’s fully achievable, but it’s worth aiming high and seeing where the constraints actually bite.</p>
<p><strong>On conflict strategies</strong> - First One Wins is clean and covers most cases, I agree. Throw Both Away feels more niche - curious if you have a specific use case in mind for that one? But the priority-based example is what really caught my eye - in-store terminal beating a shopping cart is a great real-world scenario. That’s essentially a “trust hierarchy” where some clients are more authoritative than others. I could see this being a declarative per-resource policy rather than a global setting.</p>
<p>Actually, quick follow-up on that: when the in-store terminal wins over the shopping cart, what should happen on the cart client? Silent override, a notification, something else? Curious what you’d want as a developer there.</p>
<p><strong>On ordering</strong> - good call on this. Single-client ordering is something you get for free with WebSockets, but with HTTP separate requests can arrive out of order, so it needs to be handled explicitly. Cross-client ordering adds another layer on top of that. Both are on my radar - haven’t settled on the exact approach yet, there are a few directions I’m exploring.</p>
<p>Great timing with your upcoming project btw - would love to hear more about it as the design evolves!</p> 
	            </div>

	            <div class="base-line">
	                <div class="thread-counters">
	                    <span class="thread-count count-likes js-likers-trigger" title="Likes" data-post-id="386230" data-batch-url="/posts/batch_likers">
                        0
                      </span>
                      <!-- <span class="thread-count js-solved-indicator" title="Marked as solution"></span> -->
	                </div>
	                <div class="go-to-post">
	                  <a title="Go to post" alt="Go to post" href="https://forum.elixirforum.com/t/designing-local-first-features-for-hologram-whats-your-dream-dx/74862/21">Post #20</a>
	                </div>
	            </div>
              <div id="likers-container-386230" 
                   class="likers-container"
                   data-first-post="false"
                   data-batch-url="/posts/batch_likers">
                   <div class="likers-placeholder" 
                     data-likers-post-id="386230"
                     data-batch-url="/posts/batch_likers">
                  <div class="post-likers"></div>
                </div>
              </div>
	        </div>
			

    </div>

    <div class="triangle-top-right type-standard-post cat-standard-post" title="Post #20"></div>
  </section>
</div>
</template></turbo-stream><turbo-stream action="replace" target="load-more-container"><template><div id="load-more-container" class="load-more-container">
    <a class="load-more-button" data-turbo-stream="true" href="/topics/74862/load_more?page=3">Load more posts</a>
</div></template></turbo-stream>