<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Martin Palopoli</title>
    <description>The latest articles on DEV Community by Martin Palopoli (@martin_palopoli).</description>
    <link>https://dev.to/martin_palopoli</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1593262%2F1f1a00c6-c7c8-4524-9741-0cc1ae77f741.JPG</url>
      <title>DEV Community: Martin Palopoli</title>
      <link>https://dev.to/martin_palopoli</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/martin_palopoli"/>
    <language>en</language>
    <item>
      <title>Isomorphic hydration in Fitz: first paint on the server, then WASM adopts the DOM</title>
      <dc:creator>Martin Palopoli</dc:creator>
      <pubDate>Thu, 17 Sep 2026 09:20:53 +0000</pubDate>
      <link>https://dev.to/martin_palopoli/isomorphic-hydration-in-fitz-first-paint-on-the-server-then-wasm-adopts-the-dom-35he</link>
      <guid>https://dev.to/martin_palopoli/isomorphic-hydration-in-fitz-first-paint-on-the-server-then-wasm-adopts-the-dom-35he</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;TL;DR — Mark a component &lt;code&gt;hydrate&lt;/code&gt; and the &lt;strong&gt;same &lt;code&gt;.fitzv&lt;/code&gt;&lt;/strong&gt; does two&lt;br&gt;
jobs: the server renders it to HTML for a fast first paint (SEO,&lt;br&gt;
works-without-JS), and on boot the client-WASM runtime &lt;strong&gt;adopts&lt;/strong&gt; that&lt;br&gt;
exact DOM — restoring the serialized state from an embedded&lt;br&gt;
&lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt;, walking the existing nodes, and wiring the event&lt;br&gt;
listeners — instead of wiping and rebuilding. No re-render flash, no&lt;br&gt;
lost DOM identity, no "hydration mismatch" class of bug. &lt;em&gt;(Part 8 of&lt;br&gt;
the FitzLiveViews series — the payoff Part 7 promised.)&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Part 7 closed the fullstack loop: a &lt;code&gt;.fitzv&lt;/code&gt; component calling an &lt;code&gt;@rpc&lt;/code&gt;&lt;br&gt;
server function, typed end to end. It ended on a promise — &lt;em&gt;SSR&lt;br&gt;
hydration: the same component rendered on the server for first paint,&lt;br&gt;
then the WASM runtime taking over the existing DOM&lt;/em&gt;. This is that part.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why hydration, and why it's usually painful
&lt;/h2&gt;

&lt;p&gt;Server-side rendering gives you the good first paint: the browser shows&lt;br&gt;
real HTML immediately, search engines see content, the page works with&lt;br&gt;
JavaScript disabled. But then the interactive runtime has to &lt;em&gt;take over&lt;/em&gt;&lt;br&gt;
that page. The naive way — mount the app fresh — throws the&lt;br&gt;
server-painted DOM away and rebuilds it, producing a flash and doing the&lt;br&gt;
work twice. The framework way — reconcile a virtual DOM against the&lt;br&gt;
server HTML — is where "hydration mismatch" warnings come from, and it&lt;br&gt;
ships the whole framework runtime to do it.&lt;/p&gt;

&lt;p&gt;Fitz does neither. The WASM runtime &lt;strong&gt;adopts&lt;/strong&gt; the exact nodes the&lt;br&gt;
server painted.&lt;/p&gt;
&lt;h2&gt;
  
  
  One source, both ends
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;.fitzv&lt;/code&gt; component already compiles two ways: to server HTML (the&lt;br&gt;
LiveViews path) and to a standalone WASM app. Hydration is opt-in with&lt;br&gt;
one marker on the root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;component&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt; &lt;span class="nx"&gt;hydrate&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;world&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="nf"&gt;on_name&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;value&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;greeting&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Hello&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;span&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;nm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/span&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;on_name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;{name}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/template&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The marker is opt-in so components rendered for the LiveViews&lt;br&gt;
WS-takeover (whose DOM diff forbids a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; in the root) stay&lt;br&gt;
byte-identical. When it's present, both halves cooperate.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the server emits
&lt;/h2&gt;

&lt;p&gt;The SSR emitter paints the DOM &lt;strong&gt;and&lt;/strong&gt; leaves the client everything it&lt;br&gt;
needs to adopt it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The rendered HTML, exactly as the component's template produces it.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;state payload&lt;/strong&gt; — &lt;code&gt;&amp;lt;script type="application/json"
id="__flv_state_App"&amp;gt;{"name":"Ada"}&amp;lt;/script&amp;gt;&lt;/code&gt; — so the client boots
with the server's state, not the template defaults.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adoption markers&lt;/strong&gt; the client walks by: &lt;code&gt;&amp;lt;!--fi--&amp;gt;…&amp;lt;!--/fi--&amp;gt;&lt;/code&gt;
around interpolations in mixed text (&lt;code&gt;Hello, {name}!&lt;/code&gt;),
&lt;code&gt;&amp;lt;!--fr--&amp;gt;…&amp;lt;!--/fr--&amp;gt;&lt;/code&gt; around &lt;code&gt;{#if}&lt;/code&gt;/&lt;code&gt;{#for}&lt;/code&gt; regions, a
&lt;code&gt;&amp;lt;div class="__fitz-child-Card"&amp;gt;&lt;/code&gt; wrapper around a composed child, and
slot content inlined in the parent's scope.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a real render-a-string on the server — not a hand-authored&lt;br&gt;
&lt;code&gt;index.html&lt;/code&gt;. The server computes &lt;code&gt;App_render(App { name: "Ada" })&lt;/code&gt; and&lt;br&gt;
that string is what the browser paints and the WASM adopts.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the client does on boot
&lt;/h2&gt;

&lt;p&gt;Instead of &lt;code&gt;mount()&lt;/code&gt; (which wipes the root and builds), a hydratable&lt;br&gt;
component runs &lt;code&gt;hydrate()&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It sees the mount root already has content.&lt;/li&gt;
&lt;li&gt;It restores the serialized state from the &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; payload —
primitives, and composite state too (&lt;code&gt;List&amp;lt;T&amp;gt;&lt;/code&gt;, &lt;code&gt;Map&amp;lt;Str,V&amp;gt;&lt;/code&gt;,
nullables, imported nominal types round-trip through JSON).&lt;/li&gt;
&lt;li&gt;It walks the existing DOM with a cursor (depth-first), mapping each
element/text/comment node onto the same keep-node handles the build
walk would have created — &lt;strong&gt;no &lt;code&gt;create_element&lt;/code&gt;, no wipe&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;It wires the &lt;code&gt;@input&lt;/code&gt;/&lt;code&gt;@click&lt;/code&gt; listeners onto the adopted nodes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;From there a state change patches in place (keep-node reconciliation),&lt;br&gt;
so the live &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; keeps its caret. The page was never rebuilt — it&lt;br&gt;
was taken over.&lt;/p&gt;

&lt;h2&gt;
  
  
  It really runs
&lt;/h2&gt;

&lt;p&gt;Verified end-to-end in real Chrome (via Puppeteer), and the test is the&lt;br&gt;
convincing part: a JS property is set on a server-painted node &lt;strong&gt;before&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;init()&lt;/code&gt; runs. After hydration, that property is &lt;strong&gt;still there&lt;/strong&gt; — proof&lt;br&gt;
the node was &lt;em&gt;adopted&lt;/em&gt;, not recreated. The greeting shows &lt;code&gt;"Ada"&lt;/code&gt; (the&lt;br&gt;
server's state, not the template default &lt;code&gt;"world"&lt;/code&gt;), typing patches the&lt;br&gt;
text in place, and there are zero page errors. The runnable examples&lt;br&gt;
cover the shapes:&lt;br&gt;
&lt;a href="https://github.com/Thegreekman76/fitz/tree/main/examples/view/hydrate" rel="noopener noreferrer"&gt;&lt;code&gt;hydrate&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
(base),&lt;br&gt;
&lt;a href="https://github.com/Thegreekman76/fitz/tree/main/examples/view/hydrate-mixed" rel="noopener noreferrer"&gt;&lt;code&gt;hydrate-mixed&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
(mixed static+interpolated text),&lt;br&gt;
&lt;a href="https://github.com/Thegreekman76/fitz/tree/main/examples/view/hydrate-regions" rel="noopener noreferrer"&gt;&lt;code&gt;hydrate-regions&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
(&lt;code&gt;{#if}&lt;/code&gt;/&lt;code&gt;{#for}&lt;/code&gt;), and&lt;br&gt;
&lt;a href="https://github.com/Thegreekman76/fitz/tree/main/examples/view/hydrate-composition" rel="noopener noreferrer"&gt;&lt;code&gt;hydrate-composition&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
(&lt;code&gt;&amp;lt;Child /&amp;gt;&lt;/code&gt; + slots, adopted across the component boundary).&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is different
&lt;/h2&gt;

&lt;p&gt;Hydration is a solved problem in the JS world — the point is &lt;em&gt;what it&lt;br&gt;
costs&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;React / Next.js&lt;/strong&gt; ship the framework runtime to the client and run a
reconciliation pass to attach to the server HTML; a mismatch between
the two is a whole category of warnings. Fitz adopts the exact nodes
by walking them — there's nothing to reconcile and no framework
runtime to ship.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Astro islands / partial hydration&lt;/strong&gt; are great, but each island is
still a JS-framework runtime. Fitz's client is a single compiled WASM
app that took over the whole server-painted tree.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phoenix LiveView / Hotwire&lt;/strong&gt; are server-driven: the "client" is a
thin JS layer patching the DOM. Fitz's client is a real compiled app
with its own state — it just &lt;em&gt;starts&lt;/em&gt; from the server's DOM instead of
a blank mount.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same source both ends, node-for-node adoption, opt-in per component,&lt;br&gt;
keep-node patching afterward, zero framework runtime. That combination&lt;br&gt;
is the differentiator.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest edges (MVP)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Hydration is &lt;strong&gt;opt-in&lt;/strong&gt; with the &lt;code&gt;hydrate&lt;/code&gt; marker (universal
auto-hydration is a future improvement — opt-in keeps the LiveViews
path byte-identical).&lt;/li&gt;
&lt;li&gt;Composite state restore is covered; types that don't round-trip
through JSON (a &lt;code&gt;Map&lt;/code&gt; with a non-&lt;code&gt;Str&lt;/code&gt; key, tuples, functions) reset to
their default on restore, symmetric with the dump.&lt;/li&gt;
&lt;li&gt;Named slots in the SSR emitter and dynamic composition inside a
&lt;code&gt;{#if}&lt;/code&gt;/&lt;code&gt;{#for}&lt;/code&gt; are later slices; the default slot + static
composition adopt today.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the loop the series has been building toward: the server paints&lt;br&gt;
first (fast, indexable, &lt;code&gt;@rpc&lt;/code&gt;-fed), and the same &lt;code&gt;.fitzv&lt;/code&gt; — as a WASM&lt;br&gt;
app — takes over that exact DOM and keeps it alive. Next up: the&lt;br&gt;
companion UI library the flagship admin panel is built from — ~40&lt;br&gt;
importable components, extracted from a real app.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>rust</category>
      <category>opensource</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Hidratación isomórfica en Fitz: first paint en el server, y después WASM adopta el DOM</title>
      <dc:creator>Martin Palopoli</dc:creator>
      <pubDate>Thu, 17 Sep 2026 09:19:49 +0000</pubDate>
      <link>https://dev.to/martin_palopoli/hidratacion-isomorfica-en-fitz-first-paint-en-el-server-y-despues-wasm-adopta-el-dom-4b7m</link>
      <guid>https://dev.to/martin_palopoli/hidratacion-isomorfica-en-fitz-first-paint-en-el-server-y-despues-wasm-adopta-el-dom-4b7m</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;TL;DR — Marcás un componente con &lt;code&gt;hydrate&lt;/code&gt; y el &lt;strong&gt;mismo &lt;code&gt;.fitzv&lt;/code&gt;&lt;/strong&gt; hace&lt;br&gt;
dos trabajos: el server lo renderiza a HTML para un first paint rápido&lt;br&gt;
(SEO, funciona-sin-JS), y al boot el runtime client-WASM &lt;strong&gt;adopta&lt;/strong&gt; ese&lt;br&gt;
DOM exacto — restaurando el estado serializado desde un &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt;&lt;br&gt;
embebido, recorriendo los nodos existentes y cableando los event&lt;br&gt;
listeners — en vez de tirar todo y reconstruir. Sin flash de&lt;br&gt;
re-render, sin perder la identidad del DOM, sin la categoría de bug&lt;br&gt;
"hydration mismatch". &lt;em&gt;(Parte 8 de la serie FitzLiveViews — el pago que&lt;br&gt;
prometió la Parte 7.)&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;La Parte 7 cerró el loop fullstack: un componente &lt;code&gt;.fitzv&lt;/code&gt; llamando a una&lt;br&gt;
función de servidor &lt;code&gt;@rpc&lt;/code&gt;, tipada de punta a punta. Terminó con una&lt;br&gt;
promesa — &lt;em&gt;hidratación SSR: el mismo componente renderizado en el server&lt;br&gt;
para el first paint, y después el runtime WASM tomando control del DOM&lt;br&gt;
existente&lt;/em&gt;. Esta es esa parte.&lt;/p&gt;
&lt;h2&gt;
  
  
  Por qué hidratación, y por qué suele doler
&lt;/h2&gt;

&lt;p&gt;El server-side rendering te da el buen first paint: el browser muestra&lt;br&gt;
HTML real de una, los buscadores ven contenido, la página funciona con&lt;br&gt;
JavaScript deshabilitado. Pero después el runtime interactivo tiene que&lt;br&gt;
&lt;em&gt;tomar control&lt;/em&gt; de esa página. La forma naive —montar la app de cero—&lt;br&gt;
tira el DOM pintado por el server y lo reconstruye, produciendo un flash&lt;br&gt;
y haciendo el trabajo dos veces. La forma framework —reconciliar un&lt;br&gt;
virtual DOM contra el HTML del server— es de donde salen los warnings de&lt;br&gt;
"hydration mismatch", y encima manda el runtime entero del framework para&lt;br&gt;
hacerlo.&lt;/p&gt;

&lt;p&gt;Fitz no hace ninguna de las dos. El runtime WASM &lt;strong&gt;adopta&lt;/strong&gt; los nodos&lt;br&gt;
exactos que pintó el server.&lt;/p&gt;
&lt;h2&gt;
  
  
  Un solo source, los dos extremos
&lt;/h2&gt;

&lt;p&gt;Un componente &lt;code&gt;.fitzv&lt;/code&gt; ya compila de dos formas: a HTML del server (el&lt;br&gt;
camino LiveViews) y a una app WASM standalone. La hidratación es opt-in&lt;br&gt;
con un marcador en el root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;component App hydrate {
  state {
    name: Str = "world"
  }

  event on_name() { name = payload["value"] }

  &amp;lt;template&amp;gt;
    &amp;lt;p class="greeting"&amp;gt;Hello, &amp;lt;span class="nm"&amp;gt;{name}&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
    &amp;lt;input @input="on_name" value="{name}" /&amp;gt;
  &amp;lt;/template&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El marcador es opt-in para que los componentes renderizados para el&lt;br&gt;
WS-takeover de LiveViews (cuyo diff de DOM prohíbe un &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; en el&lt;br&gt;
root) queden byte-idénticos. Cuando está, las dos mitades cooperan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué emite el server
&lt;/h2&gt;

&lt;p&gt;El emisor SSR pinta el DOM &lt;strong&gt;y&lt;/strong&gt; le deja al cliente todo lo que necesita&lt;br&gt;
para adoptarlo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;El HTML renderizado, exactamente como lo produce el template del
componente.&lt;/li&gt;
&lt;li&gt;Un &lt;strong&gt;payload de estado&lt;/strong&gt; — &lt;code&gt;&amp;lt;script type="application/json"
id="__flv_state_App"&amp;gt;{"name":"Ada"}&amp;lt;/script&amp;gt;&lt;/code&gt; — así el cliente bootea
con el estado del server, no con los defaults del template.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Marcadores de adopción&lt;/strong&gt; por los que el cliente camina:
&lt;code&gt;&amp;lt;!--fi--&amp;gt;…&amp;lt;!--/fi--&amp;gt;&lt;/code&gt; alrededor de las interpolaciones en texto mixto
(&lt;code&gt;Hello, {name}!&lt;/code&gt;), &lt;code&gt;&amp;lt;!--fr--&amp;gt;…&amp;lt;!--/fr--&amp;gt;&lt;/code&gt; alrededor de las regiones
&lt;code&gt;{#if}&lt;/code&gt;/&lt;code&gt;{#for}&lt;/code&gt;, un &lt;code&gt;&amp;lt;div class="__fitz-child-Card"&amp;gt;&lt;/code&gt; envolviendo a un
hijo compuesto, y el contenido de slot inlineado en el scope del padre.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Esto es un render-a-string de verdad en el server — no un &lt;code&gt;index.html&lt;/code&gt;&lt;br&gt;
hecho a mano. El server computa &lt;code&gt;App_render(App { name: "Ada" })&lt;/code&gt; y ese&lt;br&gt;
string es lo que el browser pinta y el WASM adopta.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué hace el cliente al boot
&lt;/h2&gt;

&lt;p&gt;En vez de &lt;code&gt;mount()&lt;/code&gt; (que limpia el root y construye), un componente&lt;br&gt;
hidratable corre &lt;code&gt;hydrate()&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Ve que el root del mount ya tiene contenido.&lt;/li&gt;
&lt;li&gt;Restaura el estado serializado desde el payload del &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; —
primitivos, y estado compuesto también (&lt;code&gt;List&amp;lt;T&amp;gt;&lt;/code&gt;, &lt;code&gt;Map&amp;lt;Str,V&amp;gt;&lt;/code&gt;,
nullables, tipos nominales importados round-trippean por JSON).&lt;/li&gt;
&lt;li&gt;Recorre el DOM existente con un cursor (depth-first), mapeando cada
nodo elemento/texto/comentario sobre los mismos handles keep-node que
el walk de build habría creado — &lt;strong&gt;sin &lt;code&gt;create_element&lt;/code&gt;, sin wipe&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Cablea los listeners &lt;code&gt;@input&lt;/code&gt;/&lt;code&gt;@click&lt;/code&gt; sobre los nodos adoptados.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;De ahí en más un cambio de estado parchea in-place (reconciliación&lt;br&gt;
keep-node), así que el &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; vivo conserva su caret. La página nunca&lt;br&gt;
se reconstruyó — se tomó control de ella.&lt;/p&gt;

&lt;h2&gt;
  
  
  Corre de verdad
&lt;/h2&gt;

&lt;p&gt;Verificado end-to-end en Chrome real (vía Puppeteer), y el test es la&lt;br&gt;
parte convincente: se le pone una propiedad JS a un nodo pintado por el&lt;br&gt;
server &lt;strong&gt;antes&lt;/strong&gt; de que corra &lt;code&gt;init()&lt;/code&gt;. Después de la hidratación, esa&lt;br&gt;
propiedad &lt;strong&gt;sigue ahí&lt;/strong&gt; — prueba de que el nodo fue &lt;em&gt;adoptado&lt;/em&gt;, no&lt;br&gt;
recreado. El saludo muestra &lt;code&gt;"Ada"&lt;/code&gt; (el estado del server, no el default&lt;br&gt;
del template &lt;code&gt;"world"&lt;/code&gt;), tipear parchea el texto in-place, y hay cero&lt;br&gt;
errores de página. Los ejemplos runnable cubren las formas:&lt;br&gt;
&lt;a href="https://github.com/Thegreekman76/fitz/tree/main/examples/view/hydrate" rel="noopener noreferrer"&gt;&lt;code&gt;hydrate&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
(base),&lt;br&gt;
&lt;a href="https://github.com/Thegreekman76/fitz/tree/main/examples/view/hydrate-mixed" rel="noopener noreferrer"&gt;&lt;code&gt;hydrate-mixed&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
(texto mixto estático+interpolado),&lt;br&gt;
&lt;a href="https://github.com/Thegreekman76/fitz/tree/main/examples/view/hydrate-regions" rel="noopener noreferrer"&gt;&lt;code&gt;hydrate-regions&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
(&lt;code&gt;{#if}&lt;/code&gt;/&lt;code&gt;{#for}&lt;/code&gt;), y&lt;br&gt;
&lt;a href="https://github.com/Thegreekman76/fitz/tree/main/examples/view/hydrate-composition" rel="noopener noreferrer"&gt;&lt;code&gt;hydrate-composition&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
(&lt;code&gt;&amp;lt;Child /&amp;gt;&lt;/code&gt; + slots, adoptados cruzando el borde del componente).&lt;/p&gt;

&lt;h2&gt;
  
  
  Por qué es distinto
&lt;/h2&gt;

&lt;p&gt;La hidratación es un problema resuelto en el mundo JS — la cuestión es&lt;br&gt;
&lt;em&gt;qué cuesta&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;React / Next.js&lt;/strong&gt; mandan el runtime del framework al cliente y
corren una pasada de reconciliación para engancharse al HTML del
server; un mismatch entre los dos es una categoría entera de warnings.
Fitz adopta los nodos exactos recorriéndolos — no hay nada que
reconciliar ni runtime de framework que mandar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Astro islands / hidratación parcial&lt;/strong&gt; están buenísimos, pero cada
isla sigue siendo un runtime de framework JS. El cliente de Fitz es una
sola app WASM compilada que tomó control del árbol entero pintado por
el server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phoenix LiveView / Hotwire&lt;/strong&gt; son server-driven: el "cliente" es una
capa fina de JS parcheando el DOM. El cliente de Fitz es una app
compilada de verdad con su propio estado — solo que &lt;em&gt;arranca&lt;/em&gt; desde el
DOM del server en vez de un mount en blanco.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Mismo source los dos extremos, adopción nodo-por-nodo, opt-in por&lt;br&gt;
componente, parcheo keep-node después, cero runtime de framework. Esa&lt;br&gt;
combinación es el diferencial.&lt;/p&gt;

&lt;h2&gt;
  
  
  Los bordes honestos (MVP)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;La hidratación es &lt;strong&gt;opt-in&lt;/strong&gt; con el marcador &lt;code&gt;hydrate&lt;/code&gt; (la
auto-hidratación universal es una mejora futura — el opt-in mantiene el
camino LiveViews byte-idéntico).&lt;/li&gt;
&lt;li&gt;El restore de estado compuesto está cubierto; los tipos que no
round-trippean por JSON (un &lt;code&gt;Map&lt;/code&gt; con key no-&lt;code&gt;Str&lt;/code&gt;, tuplas, funciones)
resetean a su default al restaurar, simétrico con el dump.&lt;/li&gt;
&lt;li&gt;Los named slots en el emisor SSR y la composición dinámica adentro de
un &lt;code&gt;{#if}&lt;/code&gt;/&lt;code&gt;{#for}&lt;/code&gt; son slices posteriores; el slot default + la
composición estática adoptan hoy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ese es el loop hacia el que venía la serie: el server pinta primero&lt;br&gt;
(rápido, indexable, alimentado por &lt;code&gt;@rpc&lt;/code&gt;), y el mismo &lt;code&gt;.fitzv&lt;/code&gt; — como&lt;br&gt;
app WASM — toma control de ese DOM exacto y lo mantiene vivo. Lo que&lt;br&gt;
sigue: la librería de UI companion de la que está hecho el panel admin&lt;br&gt;
flagship — ~40 componentes importables, extraídos de una app real.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>rust</category>
      <category>opensource</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Server functions in Fitz: call the server from a WASM component, no plumbing</title>
      <dc:creator>Martin Palopoli</dc:creator>
      <pubDate>Fri, 11 Sep 2026 19:37:13 +0000</pubDate>
      <link>https://dev.to/martin_palopoli/server-functions-in-fitz-call-the-server-from-a-wasm-component-no-plumbing-55ao</link>
      <guid>https://dev.to/martin_palopoli/server-functions-in-fitz-call-the-server-from-a-wasm-component-no-plumbing-55ao</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;TL;DR — Mark an &lt;code&gt;async fn&lt;/code&gt; with &lt;code&gt;@rpc&lt;/code&gt; and call it from a &lt;code&gt;.fitzv&lt;/code&gt;&lt;br&gt;
component &lt;strong&gt;as if it were local&lt;/strong&gt;: &lt;code&gt;let u = get_user(42).await?&lt;/code&gt;. The&lt;br&gt;
compiler generates &lt;strong&gt;both halves&lt;/strong&gt; from that one declaration — a&lt;br&gt;
&lt;code&gt;POST /__rpc/get_user&lt;/code&gt; endpoint on the server (which can touch the DB,&lt;br&gt;
auth, secrets) and a &lt;code&gt;fetch&lt;/code&gt;-based stub on the client — with the same&lt;br&gt;
&lt;code&gt;type&lt;/code&gt; shared back and front. No hand-written HTTP handler, no&lt;br&gt;
&lt;code&gt;fetch&lt;/code&gt;, no JSON glue, no route strings, no external deps. &lt;em&gt;(Part 7 of&lt;br&gt;
the FitzLiveViews series — the "whole stack, no plumbing" moment.)&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Earlier parts showed a &lt;code&gt;.fitzv&lt;/code&gt; component compiling &lt;strong&gt;two ways&lt;/strong&gt;: to&lt;br&gt;
server-rendered HTML (LiveViews over a WebSocket) and to a standalone&lt;br&gt;
WebAssembly SPA. Both are great — but a component in the browser is an&lt;br&gt;
island. Anything &lt;em&gt;real&lt;/em&gt; — reading the database, checking a token — has&lt;br&gt;
to happen on the server, and the classic bridge is plumbing: write an&lt;br&gt;
endpoint, write a &lt;code&gt;fetch&lt;/code&gt;, marshal the JSON both ways, and keep the&lt;br&gt;
types in sync by hand.&lt;/p&gt;

&lt;p&gt;This part erases all of that with one decorator.&lt;/p&gt;
&lt;h2&gt;
  
  
  One declaration, both halves
&lt;/h2&gt;

&lt;p&gt;Here's a server function. It's a normal &lt;code&gt;async fn&lt;/code&gt; — it could open a&lt;br&gt;
Postgres connection, run an ORM query, verify a JWT. The only special&lt;br&gt;
thing is &lt;code&gt;@rpc&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// api.fitz (classic — has db, auth, secrets)&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Int&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Str&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;rpc&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;get_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="nf"&gt;.connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;db_url&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="nf"&gt;.where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="py"&gt;.id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.first&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here's the client calling it, inside a &lt;code&gt;.fitzv&lt;/code&gt; component's event&lt;br&gt;
handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// App.fitzv (client-WASM)&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;get_user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt;

&lt;span class="nx"&gt;component&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;   &lt;span class="c1"&gt;// ← a round-trip, typed, like a local call&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;click&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;load&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Load&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/template&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole app. No &lt;code&gt;fetch&lt;/code&gt;. No &lt;code&gt;/api/users/:id&lt;/code&gt;. No&lt;br&gt;
&lt;code&gt;JSON.parse&lt;/code&gt; + a hand-written &lt;code&gt;User&lt;/code&gt; interface that drifts from the&lt;br&gt;
server. The &lt;code&gt;User&lt;/code&gt; type lives &lt;strong&gt;once&lt;/strong&gt; in &lt;code&gt;api.fitz&lt;/code&gt;; the server&lt;br&gt;
compiles it into a native binary, the client compiles it into WASM, and&lt;br&gt;
they share the definition.&lt;/p&gt;
&lt;h2&gt;
  
  
  What the compiler actually emits
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;@rpc&lt;/code&gt; is invisible-but-typed because the compiler writes both sides for&lt;br&gt;
you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Server side&lt;/strong&gt; (&lt;code&gt;fitz build --bin server&lt;/code&gt;): the &lt;code&gt;@rpc&lt;/code&gt; fn is mounted as&lt;br&gt;
&lt;code&gt;POST /__rpc/get_user&lt;/code&gt;. The request body is a JSON object with one field&lt;br&gt;
per parameter (&lt;code&gt;{"id": 42}&lt;/code&gt;); each param is deserialized from its field,&lt;br&gt;
the function runs, and its &lt;code&gt;Result&amp;lt;T&amp;gt;&lt;/code&gt; becomes &lt;code&gt;200&lt;/code&gt; + the JSON of &lt;code&gt;T&lt;/code&gt;&lt;br&gt;
(Ok) or &lt;code&gt;500&lt;/code&gt; + &lt;code&gt;{"error": ...}&lt;/code&gt; (Err). It reuses the entire &lt;code&gt;@post&lt;/code&gt;&lt;br&gt;
pipeline — observability, panic-catch, everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Client side&lt;/strong&gt; (&lt;code&gt;fitz build --bin web --target wasm-client&lt;/code&gt;): the&lt;br&gt;
imported &lt;code&gt;@rpc&lt;/code&gt; fn is emitted as an async &lt;code&gt;fetch&lt;/code&gt; stub — its&lt;br&gt;
(server-side) body is &lt;strong&gt;not&lt;/strong&gt; transpiled into the WASM. The stub&lt;br&gt;
serializes the args, POSTs same-origin (so the session cookie rides&lt;br&gt;
along), and maps the reply back to &lt;code&gt;Result&amp;lt;T&amp;gt;&lt;/code&gt;. And because the event&lt;br&gt;
handler now &lt;code&gt;.await&lt;/code&gt;s, the compiler splits it into a sync wrapper + an&lt;br&gt;
async worker (&lt;code&gt;spawn_local&lt;/code&gt;), so state updates and a re-render fire when&lt;br&gt;
the reply lands.&lt;/p&gt;

&lt;p&gt;You write &lt;code&gt;get_user(42).await?&lt;/code&gt;. You get a network round-trip with a&lt;br&gt;
typed result.&lt;/p&gt;
&lt;h2&gt;
  
  
  It really runs
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/Thegreekman76/fitz/tree/main/examples/view/rpc" rel="noopener noreferrer"&gt;runnable example&lt;/a&gt;&lt;br&gt;
is a two-button SPA plus a server binary. Verified end-to-end in real&lt;br&gt;
Chrome: click "Greet" and the greeting comes back from the server&lt;br&gt;
(&lt;code&gt;"Hello, world!"&lt;/code&gt;); click "Load user" and the &lt;code&gt;User&lt;/code&gt; is fetched,&lt;br&gt;
deserialized, and rendered (&lt;code&gt;"Ada"&lt;/code&gt;) — zero page errors. The client&lt;br&gt;
crate compiles to a real &lt;code&gt;.wasm&lt;/code&gt; via &lt;code&gt;wasm-pack&lt;/code&gt;; the server answers the&lt;br&gt;
exact &lt;code&gt;/__rpc/*&lt;/code&gt; routes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;fitz build &lt;span class="nt"&gt;--bin&lt;/span&gt; server     &lt;span class="c"&gt;# mounts POST /__rpc/* on :3838&lt;/span&gt;
fitz build &lt;span class="nt"&gt;--bin&lt;/span&gt; web        &lt;span class="c"&gt;# → target/wasm/web/{web.js, web_bg.wasm}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Serve the SPA from the &lt;strong&gt;same origin&lt;/strong&gt; as the server (in production,&lt;br&gt;
have the server serve the bundle, or put both behind one reverse proxy)&lt;br&gt;
so the relative &lt;code&gt;fetch("/__rpc/...")&lt;/code&gt; reaches it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is different
&lt;/h2&gt;

&lt;p&gt;Server functions aren't new — Next.js Server Actions, Remix&lt;br&gt;
loaders/actions, SvelteKit &lt;code&gt;+page.server&lt;/code&gt;, tRPC, Phoenix all have the&lt;br&gt;
pattern. What's different in Fitz:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No infrastructure step.&lt;/strong&gt; No &lt;code&gt;"use server"&lt;/code&gt; directive, no tRPC
router, no code generator. One decorator; the compiler does the rest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The same &lt;code&gt;type&lt;/code&gt;, literally.&lt;/strong&gt; Not "inferred types" across a
boundary — the exact same &lt;code&gt;User&lt;/code&gt; definition compiled to both a native
struct and a WASM struct. Zero drift, by construction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero external deps.&lt;/strong&gt; The server half reuses the built-in HTTP
stack; the client half pulls in &lt;code&gt;fetch&lt;/code&gt; + &lt;code&gt;serde&lt;/code&gt; only when a crate
actually uses &lt;code&gt;@rpc&lt;/code&gt;. No &lt;code&gt;@trpc/*&lt;/code&gt;, no bundler magic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One language, both ends.&lt;/strong&gt; The server fn and the client component
are the same Fitz, checked by the same checker.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The honest edges (MVP)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A nominal type that crosses the wire is imported into the &lt;code&gt;.fitzv&lt;/code&gt;
too (&lt;code&gt;from api import ..., User&lt;/code&gt;) so the client gets its struct.&lt;/li&gt;
&lt;li&gt;Stacked auth on the generated endpoint (&lt;code&gt;@authenticated&lt;/code&gt;/&lt;code&gt;@admin&lt;/code&gt;) is
a post-MVP refinement; for now the same-origin session cookie travels
with the request and you check the token inside the &lt;code&gt;@rpc&lt;/code&gt; body.&lt;/li&gt;
&lt;li&gt;The component re-renders once, when the reply arrives — a mid-request
"loading…" flash is a later fine-grained-reactivity slice.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the fullstack loop closed: a component in the browser calling a&lt;br&gt;
function on the server, typed end-to-end, with nothing in between that&lt;br&gt;
you had to write. Next up: SSR hydration — the same &lt;code&gt;.fitzv&lt;/code&gt; rendered on&lt;br&gt;
the server for first paint, then the WASM runtime taking over the&lt;br&gt;
existing DOM (including the state your &lt;code&gt;@rpc&lt;/code&gt; calls produced).&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>rust</category>
      <category>opensource</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Funciones de servidor en Fitz: llamá al server desde un componente WASM, sin plomería</title>
      <dc:creator>Martin Palopoli</dc:creator>
      <pubDate>Fri, 11 Sep 2026 19:35:48 +0000</pubDate>
      <link>https://dev.to/martin_palopoli/funciones-de-servidor-en-fitz-llama-al-server-desde-un-componente-wasm-sin-plomeria-1286</link>
      <guid>https://dev.to/martin_palopoli/funciones-de-servidor-en-fitz-llama-al-server-desde-un-componente-wasm-sin-plomeria-1286</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;TL;DR — Marcás una &lt;code&gt;async fn&lt;/code&gt; con &lt;code&gt;@rpc&lt;/code&gt; y la llamás desde un&lt;br&gt;
componente &lt;code&gt;.fitzv&lt;/code&gt; &lt;strong&gt;como si fuera local&lt;/strong&gt;: &lt;code&gt;let u =&lt;br&gt;
get_user(42).await?&lt;/code&gt;. El compilador genera las &lt;strong&gt;dos mitades&lt;/strong&gt; desde&lt;br&gt;
esa única declaración — un endpoint &lt;code&gt;POST /__rpc/get_user&lt;/code&gt; del lado&lt;br&gt;
server (que puede tocar la DB, auth, secrets) y un stub &lt;code&gt;fetch&lt;/code&gt; del&lt;br&gt;
lado client — con el mismo &lt;code&gt;type&lt;/code&gt; compartido back y front. Sin handler&lt;br&gt;
HTTP a mano, sin &lt;code&gt;fetch&lt;/code&gt;, sin JSON glue, sin rutas escritas a mano,&lt;br&gt;
sin deps externas. &lt;em&gt;(Parte 7 de la serie FitzLiveViews — el momento&lt;br&gt;
"todo el stack junto, sin plomería".)&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Las partes anteriores mostraron un componente &lt;code&gt;.fitzv&lt;/code&gt; compilando de&lt;br&gt;
&lt;strong&gt;dos formas&lt;/strong&gt;: a HTML server-rendered (LiveViews sobre un WebSocket) y&lt;br&gt;
a una SPA WebAssembly standalone. Las dos están buenas — pero un&lt;br&gt;
componente en el browser es una isla. Cualquier cosa &lt;em&gt;real&lt;/em&gt; —leer la&lt;br&gt;
base, verificar un token— tiene que pasar en el server, y el puente&lt;br&gt;
clásico es plomería: escribís un endpoint, escribís un &lt;code&gt;fetch&lt;/code&gt;, parseás&lt;br&gt;
el JSON de ida y de vuelta, y mantenés los tipos sincronizados a mano.&lt;/p&gt;

&lt;p&gt;Esta parte borra todo eso con un decorator.&lt;/p&gt;
&lt;h2&gt;
  
  
  Una declaración, las dos mitades
&lt;/h2&gt;

&lt;p&gt;Acá hay una función de servidor. Es una &lt;code&gt;async fn&lt;/code&gt; normal — podría abrir&lt;br&gt;
una conexión a Postgres, correr una query del ORM, verificar un JWT. Lo&lt;br&gt;
único especial es &lt;code&gt;@rpc&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// api.fitz (classic — tiene db, auth, secrets)&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Int&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Str&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;rpc&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;get_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="nf"&gt;.connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;db_url&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="nf"&gt;.where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="py"&gt;.id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.first&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Y acá el cliente llamándola, dentro del event handler de un componente&lt;br&gt;
&lt;code&gt;.fitzv&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// App.fitzv (client-WASM)&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;get_user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt;

&lt;span class="nx"&gt;component&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;   &lt;span class="c1"&gt;// ← un round-trip, tipado, como una llamada local&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;click&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;load&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Cargar&lt;/span&gt; &lt;span class="nx"&gt;usuario&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/template&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Esa es toda la app. Sin &lt;code&gt;fetch&lt;/code&gt;. Sin &lt;code&gt;/api/users/:id&lt;/code&gt;. Sin&lt;br&gt;
&lt;code&gt;JSON.parse&lt;/code&gt; + una interface &lt;code&gt;User&lt;/code&gt; hecha a mano que se desincroniza del&lt;br&gt;
server. El tipo &lt;code&gt;User&lt;/code&gt; vive &lt;strong&gt;una vez&lt;/strong&gt; en &lt;code&gt;api.fitz&lt;/code&gt;; el server lo&lt;br&gt;
compila a un binario nativo, el client lo compila a WASM, y comparten la&lt;br&gt;
definición.&lt;/p&gt;
&lt;h2&gt;
  
  
  Qué emite el compilador en realidad
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;@rpc&lt;/code&gt; es invisible-pero-tipado porque el compilador escribe los dos&lt;br&gt;
lados por vos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Del lado server&lt;/strong&gt; (&lt;code&gt;fitz build --bin server&lt;/code&gt;): la &lt;code&gt;@rpc&lt;/code&gt; fn se monta&lt;br&gt;
como &lt;code&gt;POST /__rpc/get_user&lt;/code&gt;. El body es un objeto JSON con un campo por&lt;br&gt;
parámetro (&lt;code&gt;{"id": 42}&lt;/code&gt;); cada param se deserializa de su campo, la&lt;br&gt;
función corre, y su &lt;code&gt;Result&amp;lt;T&amp;gt;&lt;/code&gt; se vuelve &lt;code&gt;200&lt;/code&gt; + el JSON de &lt;code&gt;T&lt;/code&gt; (Ok) o&lt;br&gt;
&lt;code&gt;500&lt;/code&gt; + &lt;code&gt;{"error": ...}&lt;/code&gt; (Err). Reusa toda la cadena de &lt;code&gt;@post&lt;/code&gt; —&lt;br&gt;
observability, panic-catch, todo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Del lado client&lt;/strong&gt; (&lt;code&gt;fitz build --bin web --target wasm-client&lt;/code&gt;): la&lt;br&gt;
&lt;code&gt;@rpc&lt;/code&gt; fn importada se emite como un stub &lt;code&gt;fetch&lt;/code&gt; async — su cuerpo&lt;br&gt;
(server-side) &lt;strong&gt;no&lt;/strong&gt; se transpila al WASM. El stub serializa los args,&lt;br&gt;
POSTea al mismo origen (así la cookie de sesión viaja sola), y mapea la&lt;br&gt;
respuesta a &lt;code&gt;Result&amp;lt;T&amp;gt;&lt;/code&gt;. Y como el event handler ahora hace &lt;code&gt;.await&lt;/code&gt;, el&lt;br&gt;
compilador lo parte en un wrapper sync + un worker async&lt;br&gt;
(&lt;code&gt;spawn_local&lt;/code&gt;), así que el estado se actualiza y el componente&lt;br&gt;
re-renderiza cuando llega la respuesta.&lt;/p&gt;

&lt;p&gt;Vos escribís &lt;code&gt;get_user(42).await?&lt;/code&gt;. Obtenés un round-trip de red con un&lt;br&gt;
resultado tipado.&lt;/p&gt;
&lt;h2&gt;
  
  
  Corre de verdad
&lt;/h2&gt;

&lt;p&gt;El &lt;a href="https://github.com/Thegreekman76/fitz/tree/main/examples/view/rpc" rel="noopener noreferrer"&gt;ejemplo runnable&lt;/a&gt;&lt;br&gt;
es una SPA de dos botones más un binario de server. Verificado&lt;br&gt;
end-to-end en Chrome real: clickeás "Greet" y el saludo vuelve del&lt;br&gt;
server (&lt;code&gt;"Hello, world!"&lt;/code&gt;); clickeás "Cargar usuario" y el &lt;code&gt;User&lt;/code&gt; se&lt;br&gt;
pide, se deserializa y se renderiza (&lt;code&gt;"Ada"&lt;/code&gt;) — cero errores de página.&lt;br&gt;
El crate del cliente compila a un &lt;code&gt;.wasm&lt;/code&gt; real vía &lt;code&gt;wasm-pack&lt;/code&gt;; el&lt;br&gt;
server responde las rutas &lt;code&gt;/__rpc/*&lt;/code&gt; exactas.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;fitz build &lt;span class="nt"&gt;--bin&lt;/span&gt; server     &lt;span class="c"&gt;# monta POST /__rpc/* en :3838&lt;/span&gt;
fitz build &lt;span class="nt"&gt;--bin&lt;/span&gt; web        &lt;span class="c"&gt;# → target/wasm/web/{web.js, web_bg.wasm}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Serví la SPA desde el &lt;strong&gt;mismo origen&lt;/strong&gt; que el server (en producción, que&lt;br&gt;
el server sirva el bundle, o poné a los dos detrás de un reverse proxy)&lt;br&gt;
para que el &lt;code&gt;fetch("/__rpc/...")&lt;/code&gt; relativo lo alcance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Por qué es distinto
&lt;/h2&gt;

&lt;p&gt;Las funciones de servidor no son nuevas — Next.js Server Actions, Remix&lt;br&gt;
loaders/actions, SvelteKit &lt;code&gt;+page.server&lt;/code&gt;, tRPC, Phoenix tienen el&lt;br&gt;
patrón. Lo distinto en Fitz:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sin paso de infraestructura.&lt;/strong&gt; Sin directiva &lt;code&gt;"use server"&lt;/code&gt;, sin
router de tRPC, sin generador de código. Un decorator; el compilador
hace el resto.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;El mismo &lt;code&gt;type&lt;/code&gt;, literal.&lt;/strong&gt; No "tipos inferidos" cruzando un borde
— la misma definición exacta de &lt;code&gt;User&lt;/code&gt; compilada a un struct nativo y
a un struct WASM. Cero drift, por construcción.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cero deps externas.&lt;/strong&gt; La mitad server reusa el stack HTTP nativo;
la mitad client trae &lt;code&gt;fetch&lt;/code&gt; + &lt;code&gt;serde&lt;/code&gt; solo cuando un crate realmente
usa &lt;code&gt;@rpc&lt;/code&gt;. Sin &lt;code&gt;@trpc/*&lt;/code&gt;, sin magia de bundler.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Un solo lenguaje, los dos extremos.&lt;/strong&gt; La fn del server y el
componente del client son el mismo Fitz, chequeados por el mismo
checker.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Los bordes honestos (MVP)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Un tipo nominal que cruza el cable se importa también en el &lt;code&gt;.fitzv&lt;/code&gt;
(&lt;code&gt;from api import ..., User&lt;/code&gt;) para que el cliente tenga su struct.&lt;/li&gt;
&lt;li&gt;Auth apilable sobre el endpoint generado (&lt;code&gt;@authenticated&lt;/code&gt;/&lt;code&gt;@admin&lt;/code&gt;)
es refinamiento post-MVP; por ahora la cookie de sesión same-origin
viaja con el request y verificás el token dentro del cuerpo de la
&lt;code&gt;@rpc&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;El componente re-renderiza una vez, cuando llega la respuesta — un
flash de "cargando…" a mitad de camino es un slice posterior de
reactividad fine-grained.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ese es el loop fullstack cerrado: un componente en el browser llamando a&lt;br&gt;
una función en el server, tipado de punta a punta, sin nada en el medio&lt;br&gt;
que hayas tenido que escribir. Lo que sigue: hidratación SSR — el mismo&lt;br&gt;
&lt;code&gt;.fitzv&lt;/code&gt; renderizado en el server para el first paint, y después el&lt;br&gt;
runtime WASM tomando control del DOM existente (incluyendo el estado que&lt;br&gt;
produjeron tus llamadas &lt;code&gt;@rpc&lt;/code&gt;).&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>rust</category>
      <category>opensource</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Building the flagship (3): a packaged UI library, i18n, and one-command Docker</title>
      <dc:creator>Martin Palopoli</dc:creator>
      <pubDate>Fri, 28 Aug 2026 15:15:52 +0000</pubDate>
      <link>https://dev.to/martin_palopoli/building-the-flagship-3-a-packaged-ui-library-i18n-and-one-command-docker-5di1</link>
      <guid>https://dev.to/martin_palopoli/building-the-flagship-3-a-packaged-ui-library-i18n-and-one-command-docker-5di1</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;TL;DR — The &lt;a href="https://github.com/Thegreekman76/fitz-liveviews/tree/main/examples/admin" rel="noopener noreferrer"&gt;Admin flagship&lt;/a&gt; isn't just an app — it's the app the &lt;strong&gt;companion UI library&lt;/strong&gt; (&lt;code&gt;fitz_liveviews.ui.*&lt;/code&gt;) was &lt;em&gt;extracted&lt;/em&gt; from. The DataGrid, the sortable headers, the toolbar, the pager, the confirm dialog, the toasts, the tree view, every form input — all drop-in components you &lt;code&gt;import&lt;/code&gt; and render. They're themed through &lt;code&gt;--flv-*&lt;/code&gt; design tokens (so a light/dark/auto switch is free), translated ES/EN through a tiny server-side dictionary, and the whole thing runs with one &lt;code&gt;docker compose up&lt;/code&gt;. This closes the flagship trilogy. &lt;em&gt;(Part 6 of the FitzLiveViews series.)&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Parts &lt;a href="https://dev.to/"&gt;4&lt;/a&gt; and &lt;a href="https://dev.to/"&gt;5&lt;/a&gt; showed the flagship's auth and its live grid. This one is about &lt;em&gt;reuse&lt;/em&gt;: the app is built almost entirely from packaged components, and this is how you'd build yours.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Excerpts from the real app&lt;/strong&gt; (&lt;a href="https://github.com/Thegreekman76/fitz-liveviews/tree/main/examples/admin" rel="noopener noreferrer"&gt;&lt;code&gt;examples/admin/&lt;/code&gt;&lt;/a&gt;); the runnable path is &lt;code&gt;git clone&lt;/code&gt; + &lt;code&gt;docker compose up&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The library was mined from this app
&lt;/h2&gt;

&lt;p&gt;Open the top of the employees screen and you see the library, imported piece by piece — this is verbatim from the app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fitz_liveviews.ui.DataGrid&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;data_grid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data_grid_render&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fitz_liveviews.ui.SortableHeader&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sortable_header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sortable_header_render&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fitz_liveviews.ui.GridToolbar&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;grid_toolbar&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;grid_toolbar_render&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fitz_liveviews.ui.GridFilters&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;grid_filters&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;grid_filters_render&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fitz_liveviews.ui.Pager&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pager&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pager_render&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fitz_liveviews.ui.ConfirmDialog&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;confirm_dialog&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;confirm_dialog_render&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fitz_liveviews.ui.Toast&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;toast&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;toast_render&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;toast_show&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;toast_dismiss&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fitz_liveviews.ui.TreeView&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;tree_view&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tree_view_render&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fitz_liveviews.ui.Chip&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;chip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chip_render&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fitz_liveviews.ui.CountBadge&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;count_badge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count_badge_render&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each is a &lt;code&gt;fitz.toml&lt;/code&gt; dependency sub-path (&lt;code&gt;fitz_liveviews.ui.X&lt;/code&gt;) — the cross-directory / dependency import that lets you pull a component out of the library instead of copying it. You render one by building its props and taking &lt;code&gt;.raw&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;filters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;grid_filters_render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grid_filters&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;pills&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;estado_pills&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;filter_label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"grid.col.estado"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="py"&gt;.raw&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The employee row (&lt;code&gt;EmpleadoRow&lt;/code&gt;) stays app-specific on purpose — it's the domain row, re-rendered N times per live frame. Everything &lt;em&gt;around&lt;/em&gt; it is the library. The catalog is in &lt;a href="https://thegreekman76.github.io/fitz-liveviews/ui-components/" rel="noopener noreferrer"&gt;&lt;code&gt;docs/ui-components.md&lt;/code&gt;&lt;/a&gt;, and you can play with the components in isolation in the &lt;a href="https://thegreekman76.github.io/fitz-liveviews/live/" rel="noopener noreferrer"&gt;gallery&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  One theme switch, everywhere
&lt;/h2&gt;

&lt;p&gt;Every packaged component reads its colors from &lt;code&gt;--flv-*&lt;/code&gt; design tokens (&lt;code&gt;--flv-color-primary&lt;/code&gt;, &lt;code&gt;--flv-surface&lt;/code&gt;, &lt;code&gt;--flv-text&lt;/code&gt;, …). The app defines those tokens once, aliased to its palette, with light and dark values behind a &lt;code&gt;data-theme&lt;/code&gt; attribute. A tiny inline script sets that attribute &lt;strong&gt;before first paint&lt;/strong&gt; from the saved preference — so there's no flash — and the theme switch just flips it. Because &lt;em&gt;every&lt;/em&gt; component reads the same tokens, the whole panel — grid, forms, dialogs, chips — switches light/dark/auto in one move. No per-component theme wiring.&lt;/p&gt;

&lt;h2&gt;
  
  
  i18n without a library
&lt;/h2&gt;

&lt;p&gt;Translation is a server-side dictionary — no i18n framework, no build step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// i18n.fitz&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Str&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;// "es"/"en" + key → string&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every user-facing string goes through &lt;code&gt;t(locale, "grid.search")&lt;/code&gt;. The active locale comes from a cookie; a &lt;code&gt;GET /lang/{code}&lt;/code&gt; handler sets it and redirects back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/lang/{code}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;set_lang&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;referer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Str&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Response&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;cookie&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;lang_cookie_name&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"="&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;loc&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"; Path=/; SameSite=Lax; Max-Age=31536000"&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Response&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;303&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"Set-Cookie"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cookie&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;back&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;   &lt;span class="c1"&gt;// back to where you were&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;🌐 ES / EN&lt;/code&gt; switch in the topbar hits that route. Because rendering is server-side, switching language re-renders every string on the next frame — including inside the live grid.&lt;/p&gt;

&lt;h2&gt;
  
  
  One command to run it
&lt;/h2&gt;

&lt;p&gt;The whole stack — Postgres with schema + seed, and the app — is one &lt;code&gt;docker compose up&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16-alpine&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;fitz&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;fitz&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;fitz_admin&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro&lt;/span&gt;   &lt;span class="c1"&gt;# schema + seed&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;fitz&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-d&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;fitz_admin"&lt;/span&gt;&lt;span class="pi"&gt;],&lt;/span&gt; &lt;span class="nv"&gt;...&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;../..&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;examples/admin/Dockerfile&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;service_healthy&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgres://fitz:fitz@db:5432/fitz_admin?sslmode=disable"&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3000:3000"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Dockerfile&lt;/code&gt; is two stages: a &lt;code&gt;fitz build&lt;/code&gt; in the official Fitz image, then the resulting &lt;strong&gt;native binary&lt;/strong&gt; copied into a &lt;code&gt;distroless&lt;/code&gt; runtime — no interpreter, no Python, no node. Postgres runs &lt;code&gt;db/init.sql&lt;/code&gt; (schema + seed) on first boot; the app waits for the healthcheck and serves. &lt;code&gt;fitz run&lt;/code&gt; and the built binary render bit-for-bit identical, so you develop on the interpreter and ship the binary.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Thegreekman76/fitz-liveviews
&lt;span class="nb"&gt;cd &lt;/span&gt;fitz-liveviews/examples/admin
docker compose up &lt;span class="nt"&gt;--build&lt;/span&gt;
&lt;span class="c"&gt;# → http://localhost:3000/   login: admin@fitz.dev / admin1234&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The whole stack, one language
&lt;/h2&gt;

&lt;p&gt;Step back and look at what that one command brings up: cookie auth (Argon2id + JWT), a responsive themed shell, live DataGrids querying Postgres over WebSockets, rich forms, i18n, CSV export, all built from a packaged UI library — and it's &lt;strong&gt;one language, one binary, no JavaScript build, no &lt;code&gt;node_modules&lt;/code&gt;&lt;/strong&gt;. That's the pitch from part 1, proven on something real.&lt;/p&gt;

&lt;h2&gt;
  
  
  That's the series (for now)
&lt;/h2&gt;

&lt;p&gt;Six posts: the pitch, the counter twice, forms and payloads, and the flagship in three parts. If any of it made you curious, the best next step is to run the flagship — &lt;code&gt;docker compose up&lt;/code&gt; — and then read the same components in the &lt;a href="https://thegreekman76.github.io/fitz-liveviews/ui-components/" rel="noopener noreferrer"&gt;catalog&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Star the &lt;a href="https://github.com/Thegreekman76/fitz-liveviews" rel="noopener noreferrer"&gt;repo&lt;/a&gt;, try it, and tell me what you'd build with it.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>rust</category>
      <category>opensource</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Construyendo el flagship (3): una librería de UI empaquetada, i18n, y Docker de un comando</title>
      <dc:creator>Martin Palopoli</dc:creator>
      <pubDate>Fri, 28 Aug 2026 15:15:08 +0000</pubDate>
      <link>https://dev.to/martin_palopoli/construyendo-el-flagship-3-una-libreria-de-ui-empaquetada-i18n-y-docker-de-un-comando-1319</link>
      <guid>https://dev.to/martin_palopoli/construyendo-el-flagship-3-una-libreria-de-ui-empaquetada-i18n-y-docker-de-un-comando-1319</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;TL;DR — El &lt;a href="https://github.com/Thegreekman76/fitz-liveviews/tree/main/examples/admin" rel="noopener noreferrer"&gt;Admin flagship&lt;/a&gt; no es solo una app — es la app de la que se &lt;em&gt;extrajo&lt;/em&gt; la &lt;strong&gt;librería de UI empaquetada&lt;/strong&gt; (&lt;code&gt;fitz_liveviews.ui.*&lt;/code&gt;). El DataGrid, los headers ordenables, la toolbar, el pager, el diálogo de confirmación, los toasts, el tree view, cada input de form — todos componentes drop-in que &lt;code&gt;import&lt;/code&gt;ás y renderizás. Están tematizados con design tokens &lt;code&gt;--flv-*&lt;/code&gt; (así un switch light/dark/auto es gratis), traducidos ES/EN con un diccionario chiquito del lado del servidor, y todo corre con un &lt;code&gt;docker compose up&lt;/code&gt;. Esto cierra la trilogía del flagship. &lt;em&gt;(Parte 6 de la serie FitzLiveViews.)&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Las partes &lt;a href="https://dev.to/"&gt;4&lt;/a&gt; y &lt;a href="https://dev.to/"&gt;5&lt;/a&gt; mostraron la auth del flagship y su grid en vivo. Esta es sobre &lt;em&gt;reuso&lt;/em&gt;: la app está construida casi enteramente con componentes empaquetados, y así construirías la tuya.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Extractos de la app real&lt;/strong&gt; (&lt;a href="https://github.com/Thegreekman76/fitz-liveviews/tree/main/examples/admin" rel="noopener noreferrer"&gt;&lt;code&gt;examples/admin/&lt;/code&gt;&lt;/a&gt;); el camino runnable es &lt;code&gt;git clone&lt;/code&gt; + &lt;code&gt;docker compose up&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  La librería se extrajo de esta app
&lt;/h2&gt;

&lt;p&gt;Abrí el tope de la pantalla de empleados y ves la librería, importada pieza por pieza — esto es textual de la app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fitz_liveviews.ui.DataGrid&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;data_grid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data_grid_render&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fitz_liveviews.ui.SortableHeader&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sortable_header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sortable_header_render&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fitz_liveviews.ui.GridToolbar&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;grid_toolbar&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;grid_toolbar_render&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fitz_liveviews.ui.GridFilters&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;grid_filters&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;grid_filters_render&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fitz_liveviews.ui.Pager&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pager&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pager_render&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fitz_liveviews.ui.ConfirmDialog&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;confirm_dialog&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;confirm_dialog_render&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fitz_liveviews.ui.Toast&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;toast&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;toast_render&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;toast_show&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;toast_dismiss&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fitz_liveviews.ui.TreeView&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;tree_view&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tree_view_render&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fitz_liveviews.ui.Chip&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;chip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chip_render&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fitz_liveviews.ui.CountBadge&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;count_badge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count_badge_render&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cada uno es un sub-path de dependencia del &lt;code&gt;fitz.toml&lt;/code&gt; (&lt;code&gt;fitz_liveviews.ui.X&lt;/code&gt;) — el import cross-directory / de dependencia que te deja sacar un componente de la librería en vez de copiarlo. Renderizás uno construyendo sus props y tomando &lt;code&gt;.raw&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;filters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;grid_filters_render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grid_filters&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;pills&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;estado_pills&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;filter_label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"grid.col.estado"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="py"&gt;.raw&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La fila de empleado (&lt;code&gt;EmpleadoRow&lt;/code&gt;) queda app-específica a propósito — es la fila del dominio, re-renderizada N veces por frame en vivo. Todo lo que la &lt;em&gt;rodea&lt;/em&gt; es la librería. El catálogo está en &lt;a href="https://thegreekman76.github.io/fitz-liveviews/ui-components/" rel="noopener noreferrer"&gt;&lt;code&gt;docs/ui-components.md&lt;/code&gt;&lt;/a&gt;, y podés jugar con los componentes aislados en la &lt;a href="https://thegreekman76.github.io/fitz-liveviews/live/" rel="noopener noreferrer"&gt;galería&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un switch de tema, en todos lados
&lt;/h2&gt;

&lt;p&gt;Cada componente empaquetado lee sus colores de design tokens &lt;code&gt;--flv-*&lt;/code&gt; (&lt;code&gt;--flv-color-primary&lt;/code&gt;, &lt;code&gt;--flv-surface&lt;/code&gt;, &lt;code&gt;--flv-text&lt;/code&gt;, …). La app define esos tokens una vez, aliaseados a su paleta, con valores light y dark detrás de un atributo &lt;code&gt;data-theme&lt;/code&gt;. Un script inline chiquito setea ese atributo &lt;strong&gt;antes del primer paint&lt;/strong&gt; desde la preferencia guardada — así no hay flash — y el switch de tema solo lo cambia. Como &lt;em&gt;todos&lt;/em&gt; los componentes leen los mismos tokens, todo el panel — grid, forms, diálogos, chips — cambia light/dark/auto de una. Sin cablear el tema por componente.&lt;/p&gt;

&lt;h2&gt;
  
  
  i18n sin librería
&lt;/h2&gt;

&lt;p&gt;La traducción es un diccionario del lado del servidor — sin framework de i18n, sin paso de build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// i18n.fitz&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Str&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;// "es"/"en" + key → string&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cada string user-facing pasa por &lt;code&gt;t(locale, "grid.search")&lt;/code&gt;. El locale activo viene de una cookie; un handler &lt;code&gt;GET /lang/{code}&lt;/code&gt; la setea y redirige de vuelta:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/lang/{code}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;set_lang&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;referer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Str&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Response&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;cookie&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;lang_cookie_name&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"="&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;loc&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"; Path=/; SameSite=Lax; Max-Age=31536000"&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Response&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;303&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"Set-Cookie"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cookie&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;back&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;   &lt;span class="c1"&gt;// de vuelta a donde estabas&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El switch &lt;code&gt;🌐 ES / EN&lt;/code&gt; en la topbar pega a esa ruta. Como el render es del lado del servidor, cambiar idioma re-renderiza cada string en el próximo frame — incluso adentro del grid en vivo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un comando para correrlo
&lt;/h2&gt;

&lt;p&gt;Todo el stack — Postgres con schema + seed, y la app — es un &lt;code&gt;docker compose up&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16-alpine&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;fitz&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;fitz&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;fitz_admin&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro&lt;/span&gt;   &lt;span class="c1"&gt;# schema + seed&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;fitz&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-d&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;fitz_admin"&lt;/span&gt;&lt;span class="pi"&gt;],&lt;/span&gt; &lt;span class="nv"&gt;...&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;../..&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;examples/admin/Dockerfile&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;service_healthy&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgres://fitz:fitz@db:5432/fitz_admin?sslmode=disable"&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3000:3000"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El &lt;code&gt;Dockerfile&lt;/code&gt; son dos stages: un &lt;code&gt;fitz build&lt;/code&gt; en la imagen oficial de Fitz, después el &lt;strong&gt;binario nativo&lt;/strong&gt; resultante copiado a un runtime &lt;code&gt;distroless&lt;/code&gt; — sin intérprete, sin Python, sin node. Postgres corre &lt;code&gt;db/init.sql&lt;/code&gt; (schema + seed) al primer boot; la app espera el healthcheck y sirve. &lt;code&gt;fitz run&lt;/code&gt; y el binario buildeado renderizan bit-a-bit idéntico, así que desarrollás sobre el intérprete y shipeás el binario.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Thegreekman76/fitz-liveviews
&lt;span class="nb"&gt;cd &lt;/span&gt;fitz-liveviews/examples/admin
docker compose up &lt;span class="nt"&gt;--build&lt;/span&gt;
&lt;span class="c"&gt;# → http://localhost:3000/   login: admin@fitz.dev / admin1234&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Todo el stack, un lenguaje
&lt;/h2&gt;

&lt;p&gt;Alejate y mirá lo que ese comando levanta: auth por cookie (Argon2id + JWT), un shell responsive tematizado, DataGrids en vivo consultando Postgres por WebSockets, forms ricos, i18n, export CSV, todo construido con una librería de UI empaquetada — y es &lt;strong&gt;un lenguaje, un binario, sin build de JavaScript, sin &lt;code&gt;node_modules&lt;/code&gt;&lt;/strong&gt;. Ese es el pitch de la parte 1, probado en algo real.&lt;/p&gt;

&lt;h2&gt;
  
  
  Esa es la serie (por ahora)
&lt;/h2&gt;

&lt;p&gt;Seis posts: el pitch, el counter dos veces, forms y payloads, y el flagship en tres partes. Si algo te dio curiosidad, el mejor próximo paso es correr el flagship — &lt;code&gt;docker compose up&lt;/code&gt; — y después leer los mismos componentes en el &lt;a href="https://thegreekman76.github.io/fitz-liveviews/ui-components/" rel="noopener noreferrer"&gt;catálogo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Dale una estrella al &lt;a href="https://github.com/Thegreekman76/fitz-liveviews" rel="noopener noreferrer"&gt;repo&lt;/a&gt;, probalo, y contame qué construirías con esto.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>rust</category>
      <category>opensource</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Building the flagship (2): a live DataGrid that queries Postgres on every keystroke</title>
      <dc:creator>Martin Palopoli</dc:creator>
      <pubDate>Mon, 24 Aug 2026 09:53:15 +0000</pubDate>
      <link>https://dev.to/martin_palopoli/building-the-flagship-2-a-live-datagrid-that-queries-postgres-on-every-keystroke-2i73</link>
      <guid>https://dev.to/martin_palopoli/building-the-flagship-2-a-live-datagrid-that-queries-postgres-on-every-keystroke-2i73</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;TL;DR — The &lt;a href="https://github.com/Thegreekman76/fitz-liveviews/tree/main/examples/admin" rel="noopener noreferrer"&gt;Admin flagship&lt;/a&gt;'s employees screen is a &lt;strong&gt;live DataGrid&lt;/strong&gt;: type in the search box, click a filter pill, sort a column, page through — and each one re-runs a &lt;strong&gt;real SQL query&lt;/strong&gt; against Postgres through the Fitz ORM, then diff-patches the table over a WebSocket. Nothing is filtered in memory; the &lt;code&gt;count&lt;/code&gt; reflects the active filters; the sort is a dynamic &lt;code&gt;ORDER BY&lt;/code&gt;. All grid state (search term, filters, sort, page) is &lt;strong&gt;per-connection&lt;/strong&gt;, so two browsers filter independently. No React, no API endpoints, no client state management. &lt;em&gt;(Part 5 of the FitzLiveViews series — the flagship's centerpiece.)&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://dev.to/"&gt;Part 4&lt;/a&gt; gave the flagship auth and a shell. This is the screen that makes it worth building: an employees grid that behaves like a rich SPA data table, but is &lt;em&gt;entirely&lt;/em&gt; server-driven.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The code below is excerpted from the real app&lt;/strong&gt; (&lt;a href="https://github.com/Thegreekman76/fitz-liveviews/tree/main/examples/admin" rel="noopener noreferrer"&gt;&lt;code&gt;examples/admin/&lt;/code&gt;&lt;/a&gt;) — the full grid is a few hundred lines. The snippets show the shape faithfully; the guaranteed-to-work path is &lt;code&gt;git clone&lt;/code&gt; + &lt;code&gt;docker compose up&lt;/code&gt; (see "Try it").&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The shape
&lt;/h2&gt;

&lt;p&gt;SSR paints the grid once (so the first load is instant and crawlable), then a &lt;code&gt;@ws&lt;/code&gt; socket takes over the live layer. Each connection keeps its own grid state — the current search, filters, sort, and page — and on every event it rebuilds the query, renders, diffs, and patches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"cookie"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="nf"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/live/empleados"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;live&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;WsConn&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LiveFrame&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cookie&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Str&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="nf"&gt;user_from_cookie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cookie&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;// gate the socket&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="nf"&gt;.connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;db_url&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;

  &lt;span class="c1"&gt;// Per-connection grid state — two browsers filter independently.&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;            &lt;span class="c1"&gt;// search term&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;estado&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"all"&lt;/span&gt;    &lt;span class="c1"&gt;// Todos / Activos / Inactivos&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;depto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;         &lt;span class="c1"&gt;// department filter (0 = all)&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;sort_col&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"nombre"&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;asc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;render_grid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;estado&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;depto&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sort_col&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;asc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;

  &lt;span class="k"&gt;loop&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="nf"&gt;.recv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
    &lt;span class="c1"&gt;// update state from the event's payload&lt;/span&gt;
    &lt;span class="n"&gt;q&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="py"&gt;.payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"q"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;estado&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="py"&gt;.payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"estado"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;estado&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;depto&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pget_int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="py"&gt;.payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"depto"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;depto&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;// ... sort + page ...&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;render_grid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;estado&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;depto&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sort_col&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;asc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;patches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;diff_html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="nf"&gt;.send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LiveFrame&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;patches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;patches&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
    &lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pget&lt;/code&gt; / &lt;code&gt;pget_int&lt;/code&gt; just read the event payload (a &lt;code&gt;Map&amp;lt;Str, Str&amp;gt;&lt;/code&gt;) with defaults — &lt;code&gt;str.to_int()&lt;/code&gt; parses the page number and department id. Notice there's no per-event branching: the loop reads the payload into state and re-renders. Add a filter dimension by adding a state variable and a &lt;code&gt;.where&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every dimension is real SQL
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;render_grid&lt;/code&gt; builds one query through the Fitz ORM. Each active dimension is a chained &lt;code&gt;.where&lt;/code&gt; (they AND together), the sort is a dynamic &lt;code&gt;ORDER BY&lt;/code&gt;, and pagination is &lt;code&gt;LIMIT&lt;/code&gt;/&lt;code&gt;OFFSET&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;rows_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;estado&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;depto&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sort_col&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;asc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Empleado&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Empleado&lt;/span&gt;&lt;span class="nf"&gt;.where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="py"&gt;.nombre&lt;/span&gt;&lt;span class="nf"&gt;.ilike&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"%"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"%"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

  &lt;span class="c1"&gt;// Estado + department filters are more ANDed WHEREs, applied&lt;/span&gt;
  &lt;span class="c1"&gt;// conditionally — the whole thing is one SQL statement.&lt;/span&gt;
  &lt;span class="c1"&gt;// ... .where(fn(e) =&amp;gt; e.activo == true) ...&lt;/span&gt;
  &lt;span class="c1"&gt;// ... .where(fn(e) =&amp;gt; e.departamento_id == depto) ...&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;
    &lt;span class="nf"&gt;.order_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sort_col&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;asc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;// dynamic ORDER BY &amp;lt;col&amp;gt; ASC|DESC&lt;/span&gt;
    &lt;span class="nf"&gt;.limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PAGE_SIZE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.offset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;PAGE_SIZE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;strong&gt;count reflects the filters&lt;/strong&gt; — the same &lt;code&gt;.where&lt;/code&gt; chain, terminated with &lt;code&gt;.count(conn)&lt;/code&gt; instead of &lt;code&gt;.all&lt;/code&gt; — so "showing 8 of 23" is honest, not a client-side guess. Search is &lt;code&gt;ilike("%q%")&lt;/code&gt; (case-insensitive &lt;code&gt;LIKE&lt;/code&gt;); the estado pills map to &lt;code&gt;.where(e.activo == …)&lt;/code&gt;; the department pills to &lt;code&gt;.where(e.departamento_id == …)&lt;/code&gt;. It's all SQL, all typed, all in the same &lt;code&gt;.fitz&lt;/code&gt; file as the socket.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the browser does
&lt;/h2&gt;

&lt;p&gt;Nothing you write. The grid's controls are the same &lt;code&gt;data-flv-*&lt;/code&gt; conventions from &lt;a href="https://dev.to/"&gt;part 3&lt;/a&gt;: the search box is a &lt;code&gt;data-flv-change&lt;/code&gt; input that sends &lt;code&gt;{"q": "&amp;lt;text&amp;gt;"}&lt;/code&gt;, a filter pill is a &lt;code&gt;data-flv-click&lt;/code&gt; with a &lt;code&gt;data-flv-value-estado&lt;/code&gt;, a sortable header sends &lt;code&gt;{"sort": "cargo"}&lt;/code&gt;. The framework serializes those into the socket message; your loop reads them from &lt;code&gt;frame.payload&lt;/code&gt;. The diff engine sends only the &lt;code&gt;&amp;lt;tr&amp;gt;&lt;/code&gt;s that changed. Keyed diffing (&lt;code&gt;{#for … key=e.id}&lt;/code&gt;) means a mid-list insert (a new row from a save) patches cleanly instead of cascading.&lt;/p&gt;

&lt;p&gt;The result feels like a client-side data table — instant search, snappy sort — but the source of truth is Postgres, the query is real, and there's zero client state to keep in sync with the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Per-connection, for free
&lt;/h2&gt;

&lt;p&gt;Because the grid state lives in the &lt;code&gt;@ws&lt;/code&gt; loop's local variables, &lt;strong&gt;each connection has its own&lt;/strong&gt;. Two admins on the same page filter independently; there's no shared client store, no query-param juggling. Open the app in two tabs and filter each differently — they don't interfere. That isolation is just... how server-side local variables work.&lt;/p&gt;

&lt;p&gt;The same screen also does row selection + multi-delete, group-by, per-row expand, and a CSV export (a &lt;code&gt;Response { content_type: "text/csv", ... }&lt;/code&gt; that bakes the active search into its &lt;code&gt;href&lt;/code&gt;). All of it is more &lt;code&gt;.where&lt;/code&gt; chains and more diffs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;fitz-liveviews/examples/admin &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; docker compose up &lt;span class="nt"&gt;--build&lt;/span&gt;
&lt;span class="c"&gt;# → http://localhost:3000/empleados   (login: admin@fitz.dev / admin1234)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7dv2lowvyba7cadwsc29.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7dv2lowvyba7cadwsc29.png" alt=" " width="800" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Type in the search, click the estado/department pills, sort a column, page through — every one is a round-trip to Postgres, diffed back to your table.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next in this series
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;#6 — The packaged UI library + i18n + Docker.&lt;/strong&gt; Every reusable piece of this app — &lt;code&gt;DataGrid&lt;/code&gt;, &lt;code&gt;GridToolbar&lt;/code&gt;, &lt;code&gt;Pager&lt;/code&gt;, &lt;code&gt;ConfirmDialog&lt;/code&gt;, &lt;code&gt;Toast&lt;/code&gt;, &lt;code&gt;TreeView&lt;/code&gt;, the form inputs — is a drop-in component from &lt;code&gt;fitz_liveviews.ui.*&lt;/code&gt;. The library was &lt;em&gt;extracted&lt;/em&gt; from this app. Plus ES/EN i18n and the one-command Docker setup.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Star the &lt;a href="https://github.com/Thegreekman76/fitz-liveviews" rel="noopener noreferrer"&gt;repo&lt;/a&gt; if a Postgres-backed live grid without a client framework is the kind of thing you'd use. Next: the library it's all built from.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>rust</category>
      <category>opensource</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Construyendo el flagship (2): un DataGrid en vivo que consulta Postgres en cada tecla</title>
      <dc:creator>Martin Palopoli</dc:creator>
      <pubDate>Mon, 24 Aug 2026 09:52:14 +0000</pubDate>
      <link>https://dev.to/martin_palopoli/construyendo-el-flagship-2-un-datagrid-en-vivo-que-consulta-postgres-en-cada-tecla-4bc8</link>
      <guid>https://dev.to/martin_palopoli/construyendo-el-flagship-2-un-datagrid-en-vivo-que-consulta-postgres-en-cada-tecla-4bc8</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;TL;DR — La pantalla de empleados del &lt;a href="https://github.com/Thegreekman76/fitz-liveviews/tree/main/examples/admin" rel="noopener noreferrer"&gt;Admin flagship&lt;/a&gt; es un &lt;strong&gt;DataGrid en vivo&lt;/strong&gt;: tipeás en el buscador, clickeás un pill de filtro, ordenás una columna, paginás — y cada uno re-ejecuta una &lt;strong&gt;query SQL real&lt;/strong&gt; contra Postgres por el ORM de Fitz, después diff-parchea la tabla por WebSocket. Nada se filtra en memoria; el &lt;code&gt;count&lt;/code&gt; refleja los filtros activos; el sort es un &lt;code&gt;ORDER BY&lt;/code&gt; dinámico. Todo el estado del grid (término de búsqueda, filtros, sort, página) es &lt;strong&gt;per-connection&lt;/strong&gt;, así que dos browsers filtran independientemente. Sin React, sin endpoints de API, sin manejo de estado en el cliente. &lt;em&gt;(Parte 5 de la serie FitzLiveViews — la pieza central del flagship.)&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;La &lt;a href="https://dev.to/"&gt;parte 4&lt;/a&gt; le dio al flagship auth y un shell. Esta es la pantalla que hace que valga la pena construirlo: una grilla de empleados que se comporta como una data table de SPA rica, pero es &lt;em&gt;enteramente&lt;/em&gt; server-driven.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;El código de abajo son extractos de la app real&lt;/strong&gt; (&lt;a href="https://github.com/Thegreekman76/fitz-liveviews/tree/main/examples/admin" rel="noopener noreferrer"&gt;&lt;code&gt;examples/admin/&lt;/code&gt;&lt;/a&gt;) — el grid completo son unos cientos de líneas. Los snippets muestran la forma fielmente; el camino garantizado-que-funciona es &lt;code&gt;git clone&lt;/code&gt; + &lt;code&gt;docker compose up&lt;/code&gt; (ver "Probalo").&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  La forma
&lt;/h2&gt;

&lt;p&gt;SSR pinta el grid una vez (así la primera carga es instantánea y crawleable), después un socket &lt;code&gt;@ws&lt;/code&gt; toma la capa en vivo. Cada conexión guarda su propio estado del grid — el search, filtros, sort y página actuales — y en cada evento reconstruye la query, renderiza, diffea y parchea:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"cookie"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="nf"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/live/empleados"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;live&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;WsConn&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LiveFrame&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cookie&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Str&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="nf"&gt;user_from_cookie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cookie&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;// gatea el socket&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="nf"&gt;.connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;db_url&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;

  &lt;span class="c1"&gt;// Estado del grid per-connection — dos browsers filtran independientemente.&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;            &lt;span class="c1"&gt;// término de búsqueda&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;estado&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"all"&lt;/span&gt;    &lt;span class="c1"&gt;// Todos / Activos / Inactivos&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;depto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;         &lt;span class="c1"&gt;// filtro de departamento (0 = todos)&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;sort_col&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"nombre"&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;asc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;render_grid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;estado&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;depto&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sort_col&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;asc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;

  &lt;span class="k"&gt;loop&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="nf"&gt;.recv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
    &lt;span class="c1"&gt;// actualiza el estado desde el payload del evento&lt;/span&gt;
    &lt;span class="n"&gt;q&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="py"&gt;.payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"q"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;estado&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="py"&gt;.payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"estado"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;estado&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;depto&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pget_int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="py"&gt;.payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"depto"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;depto&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;// ... sort + page ...&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;render_grid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;estado&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;depto&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sort_col&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;asc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;patches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;diff_html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="nf"&gt;.send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LiveFrame&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;patches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;patches&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
    &lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pget&lt;/code&gt; / &lt;code&gt;pget_int&lt;/code&gt; solo leen el payload del evento (un &lt;code&gt;Map&amp;lt;Str, Str&amp;gt;&lt;/code&gt;) con defaults — &lt;code&gt;str.to_int()&lt;/code&gt; parsea el número de página y el id de departamento. Fijate que no hay branching por evento: el loop lee el payload al estado y re-renderiza. Agregás una dimensión de filtro agregando una variable de estado y un &lt;code&gt;.where&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cada dimensión es SQL real
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;render_grid&lt;/code&gt; construye una query por el ORM de Fitz. Cada dimensión activa es un &lt;code&gt;.where&lt;/code&gt; encadenado (que ANDean juntos), el sort es un &lt;code&gt;ORDER BY&lt;/code&gt; dinámico, y la paginación es &lt;code&gt;LIMIT&lt;/code&gt;/&lt;code&gt;OFFSET&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;rows_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;estado&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;depto&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sort_col&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;asc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Empleado&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Empleado&lt;/span&gt;&lt;span class="nf"&gt;.where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="py"&gt;.nombre&lt;/span&gt;&lt;span class="nf"&gt;.ilike&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"%"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"%"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

  &lt;span class="c1"&gt;// Los filtros de estado + departamento son más WHEREs ANDeados,&lt;/span&gt;
  &lt;span class="c1"&gt;// aplicados condicionalmente — todo es un solo statement SQL.&lt;/span&gt;
  &lt;span class="c1"&gt;// ... .where(fn(e) =&amp;gt; e.activo == true) ...&lt;/span&gt;
  &lt;span class="c1"&gt;// ... .where(fn(e) =&amp;gt; e.departamento_id == depto) ...&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;
    &lt;span class="nf"&gt;.order_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sort_col&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;asc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;// ORDER BY &amp;lt;col&amp;gt; ASC|DESC dinámico&lt;/span&gt;
    &lt;span class="nf"&gt;.limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PAGE_SIZE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.offset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;PAGE_SIZE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El &lt;strong&gt;count refleja los filtros&lt;/strong&gt; — la misma cadena de &lt;code&gt;.where&lt;/code&gt;, terminada con &lt;code&gt;.count(conn)&lt;/code&gt; en vez de &lt;code&gt;.all&lt;/code&gt; — así que "mostrando 8 de 23" es honesto, no una adivinanza del cliente. El search es &lt;code&gt;ilike("%q%")&lt;/code&gt; (&lt;code&gt;LIKE&lt;/code&gt; case-insensitive); los pills de estado mapean a &lt;code&gt;.where(e.activo == …)&lt;/code&gt;; los de departamento a &lt;code&gt;.where(e.departamento_id == …)&lt;/code&gt;. Todo es SQL, todo tipado, todo en el mismo archivo &lt;code&gt;.fitz&lt;/code&gt; que el socket.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué hace el browser
&lt;/h2&gt;

&lt;p&gt;Nada que escribas vos. Los controles del grid son las mismas convenciones &lt;code&gt;data-flv-*&lt;/code&gt; de la &lt;a href="https://dev.to/"&gt;parte 3&lt;/a&gt;: el buscador es un input &lt;code&gt;data-flv-change&lt;/code&gt; que manda &lt;code&gt;{"q": "&amp;lt;texto&amp;gt;"}&lt;/code&gt;, un pill de filtro es un &lt;code&gt;data-flv-click&lt;/code&gt; con un &lt;code&gt;data-flv-value-estado&lt;/code&gt;, un header ordenable manda &lt;code&gt;{"sort": "cargo"}&lt;/code&gt;. El framework las serializa al mensaje del socket; tu loop las lee de &lt;code&gt;frame.payload&lt;/code&gt;. El motor de diff manda solo los &lt;code&gt;&amp;lt;tr&amp;gt;&lt;/code&gt; que cambiaron. El diffing por key (&lt;code&gt;{#for … key=e.id}&lt;/code&gt;) hace que un insert en el medio de la lista (una fila nueva de un save) parchee limpio en vez de cascadear.&lt;/p&gt;

&lt;p&gt;El resultado se siente como una data table de cliente — search instantáneo, sort ágil — pero la fuente de verdad es Postgres, la query es real, y hay cero estado de cliente que mantener en sync con el servidor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Per-connection, gratis
&lt;/h2&gt;

&lt;p&gt;Como el estado del grid vive en las variables locales del loop &lt;code&gt;@ws&lt;/code&gt;, &lt;strong&gt;cada conexión tiene el suyo&lt;/strong&gt;. Dos admins en la misma página filtran independientemente; no hay store de cliente compartido, ni malabares de query-params. Abrí la app en dos pestañas y filtrá cada una distinto — no se interfieren. Ese aislamiento es simplemente... cómo funcionan las variables locales del lado del servidor.&lt;/p&gt;

&lt;p&gt;La misma pantalla también hace selección de filas + multi-delete, group-by, expand por fila, y un export CSV (un &lt;code&gt;Response { content_type: "text/csv", ... }&lt;/code&gt; que bakea el search activo en su &lt;code&gt;href&lt;/code&gt;). Todo eso son más cadenas de &lt;code&gt;.where&lt;/code&gt; y más diffs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Probalo
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;fitz-liveviews/examples/admin &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; docker compose up &lt;span class="nt"&gt;--build&lt;/span&gt;
&lt;span class="c"&gt;# → http://localhost:3000/empleados   (login: admin@fitz.dev / admin1234)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7dv2lowvyba7cadwsc29.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7dv2lowvyba7cadwsc29.png" alt=" " width="800" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Tipeá en el buscador, clickeá los pills de estado/departamento, ordená una columna, paginá — cada uno es un round-trip a Postgres, diffeado de vuelta a tu tabla.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué viene en la serie
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;#6 — La librería de UI empaquetada + i18n + Docker.&lt;/strong&gt; Cada pieza reusable de esta app — &lt;code&gt;DataGrid&lt;/code&gt;, &lt;code&gt;GridToolbar&lt;/code&gt;, &lt;code&gt;Pager&lt;/code&gt;, &lt;code&gt;ConfirmDialog&lt;/code&gt;, &lt;code&gt;Toast&lt;/code&gt;, &lt;code&gt;TreeView&lt;/code&gt;, los inputs del form — es un componente drop-in de &lt;code&gt;fitz_liveviews.ui.*&lt;/code&gt;. La librería se &lt;em&gt;extrajo&lt;/em&gt; de esta app. Más i18n ES/EN y el setup Docker de un comando.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Dale una estrella al &lt;a href="https://github.com/Thegreekman76/fitz-liveviews" rel="noopener noreferrer"&gt;repo&lt;/a&gt; si un grid en vivo sobre Postgres sin framework de cliente es el tipo de cosa que usarías. Lo próximo: la librería de la que está todo construido.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>rust</category>
      <category>opensource</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Building the flagship (1): browser-correct cookie auth in Fitz</title>
      <dc:creator>Martin Palopoli</dc:creator>
      <pubDate>Sat, 15 Aug 2026 11:15:17 +0000</pubDate>
      <link>https://dev.to/martin_palopoli/building-the-flagship-1-browser-correct-cookie-auth-in-fitz-mi2</link>
      <guid>https://dev.to/martin_palopoli/building-the-flagship-1-browser-correct-cookie-auth-in-fitz-mi2</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;TL;DR — Fitz has &lt;code&gt;@auth_provider&lt;/code&gt; / &lt;code&gt;@authenticated&lt;/code&gt; for Bearer-token APIs, but a &lt;strong&gt;browser&lt;/strong&gt; admin panel needs something different: the browser can't send an &lt;code&gt;Authorization&lt;/code&gt; header on a page navigation or a WebSocket handshake, and an unauthenticated request should &lt;strong&gt;redirect to &lt;code&gt;/login&lt;/code&gt;&lt;/strong&gt;, not return a JSON 401. So the &lt;a href="https://github.com/Thegreekman76/fitz-liveviews/tree/main/examples/admin" rel="noopener noreferrer"&gt;flagship Admin app&lt;/a&gt; uses a session &lt;strong&gt;cookie&lt;/strong&gt;: login verifies the password with &lt;strong&gt;Argon2id&lt;/strong&gt;, signs a &lt;strong&gt;JWT&lt;/strong&gt;, and puts it in an &lt;code&gt;HttpOnly&lt;/code&gt; cookie; every protected page reads the cookie, resolves the user through the &lt;strong&gt;ORM&lt;/strong&gt;, and redirects on failure — the way Django or Rails gate a browser session. All in Fitz, no external auth library. &lt;em&gt;(Part 4 of the FitzLiveViews series — the first of a few on the flagship.)&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Parts 1–3 built components in isolation. Now the real thing: a complete back-office admin panel — auth, a responsive shell, live DataGrids over Postgres, i18n, Docker. This post is auth and the shell; the next ones are the grids.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The code below is excerpted from the real app&lt;/strong&gt; (&lt;a href="https://github.com/Thegreekman76/fitz-liveviews/tree/main/examples/admin" rel="noopener noreferrer"&gt;&lt;code&gt;examples/admin/&lt;/code&gt;&lt;/a&gt;) to show the shape — it's a 14-file, Postgres-backed, dockerized project, so the guaranteed-to-work path is to clone it and run &lt;code&gt;docker compose up&lt;/code&gt; (see "Try it" at the end). The snippets are faithful to the source; the repo is the runnable artifact.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why cookies, not &lt;code&gt;@auth_provider&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Fitz core ships &lt;code&gt;@auth_provider&lt;/code&gt; + &lt;code&gt;@authenticated&lt;/code&gt; — and they're great for a JSON API where the client sends &lt;code&gt;Authorization: Bearer &amp;lt;token&amp;gt;&lt;/code&gt; and gets a &lt;code&gt;401&lt;/code&gt; on failure. A browser admin panel wants two different things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The browser can't send an &lt;code&gt;Authorization&lt;/code&gt; header&lt;/strong&gt; on plain page navigation, or on a WebSocket handshake. It &lt;em&gt;does&lt;/em&gt; send cookies, automatically. So the session token rides in an &lt;code&gt;HttpOnly&lt;/code&gt; cookie.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An unauthenticated page should redirect to &lt;code&gt;/login&lt;/code&gt;&lt;/strong&gt;, not dump a JSON error at the user.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So the app hand-rolls a cookie session — which in Fitz is about 40 lines, because JWT and password hashing are built into the language.&lt;/p&gt;

&lt;h2&gt;
  
  
  Login — Argon2id + JWT → Set-Cookie
&lt;/h2&gt;

&lt;p&gt;Login is a &lt;code&gt;@post&lt;/code&gt; that takes JSON credentials, verifies the password against the stored Argon2id hash, signs a JWT, and returns it as a &lt;code&gt;Set-Cookie&lt;/code&gt; header using the &lt;code&gt;Response { ... }&lt;/code&gt; built-in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/login"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;login_submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;creds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Credentials&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cookie&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Str&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Response&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="nf"&gt;find_by_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;creds&lt;/span&gt;&lt;span class="py"&gt;.email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// hash.verify is Argon2id — built into Fitz, no passlib/bcrypt dep.&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;not&lt;/span&gt; &lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="nf"&gt;.verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;creds&lt;/span&gt;&lt;span class="py"&gt;.password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="py"&gt;.password_hash&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;login_failed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;claims&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="py"&gt;.email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="py"&gt;.role&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;&lt;span class="nf"&gt;.encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;jwt_secret&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;     &lt;span class="c1"&gt;// HS256, built-in&lt;/span&gt;

  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;set_cookie&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;session_cookie_name&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"="&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;
    &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"; HttpOnly; Path=/; SameSite=Lax; Max-Age=86400"&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Response&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"Set-Cookie"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;set_cookie&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;hash.verify&lt;/code&gt; (Argon2id) and &lt;code&gt;jwt.encode&lt;/code&gt; (HS256) are Fitz built-ins — no &lt;code&gt;pip install passlib python-jose&lt;/code&gt;, no &lt;code&gt;npm install bcrypt jsonwebtoken&lt;/code&gt;. The &lt;code&gt;Response { ... }&lt;/code&gt; built-in is what lets a handler set a custom header instead of returning plain JSON.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(One wrinkle: login posts JSON via a small &lt;code&gt;fetch&lt;/code&gt;, not a native &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt;, because Fitz &lt;code&gt;@post&lt;/code&gt; handlers take a JSON body, not form-urlencoded. A future core enhancement can accept form posts.)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The session check — cookie → JWT → user (via the ORM)
&lt;/h2&gt;

&lt;p&gt;Resolving the logged-in user is: pull the token out of the raw &lt;code&gt;Cookie&lt;/code&gt; header, decode the JWT, read the &lt;code&gt;email&lt;/code&gt; claim, and look the user up in Postgres — through the Fitz ORM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;user_from_cookie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cookie&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Str&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;token_from_cookie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cookie&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;          &lt;span class="c1"&gt;// parse the Cookie header&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;claims&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;&lt;span class="nf"&gt;.decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;jwt_secret&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;    &lt;span class="c1"&gt;// verify signature + expiry&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;claims&lt;/span&gt;&lt;span class="nf"&gt;.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="nf"&gt;.connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;db_url&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="nf"&gt;.where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="py"&gt;.email&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.first&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any failure — missing cookie, bad signature, expired token, unknown user — surfaces as &lt;code&gt;Err&lt;/code&gt;, which a protected page turns into a redirect. Notice &lt;code&gt;User.where(fn(u) =&amp;gt; u.email == email).first(conn)&lt;/code&gt;: that's the native Fitz ORM, a typed query against Postgres, in the same file as the auth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Protecting a page
&lt;/h2&gt;

&lt;p&gt;A protected handler reads the cookie with &lt;code&gt;@header(name="cookie")&lt;/code&gt; and redirects to &lt;code&gt;/login&lt;/code&gt; on any auth failure — a real &lt;code&gt;303&lt;/code&gt;, browser-correct:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"cookie"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;dashboard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cookie&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Str&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Response&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="nf"&gt;user_from_cookie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cookie&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;redirect_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/login"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;   &lt;span class="c1"&gt;// 303 → /login&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// ... render the dashboard for `user` ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole gate. An unauthenticated request to &lt;code&gt;/&lt;/code&gt; gets &lt;code&gt;303 → /login&lt;/code&gt; — exactly what a browser expects, and what you can't express with a JSON-401 auth provider. &lt;code&gt;/logout&lt;/code&gt; clears the cookie (&lt;code&gt;Max-Age=0&lt;/code&gt;) and redirects. I verified the full flow end-to-end: &lt;code&gt;GET /&lt;/code&gt; with no cookie → &lt;code&gt;303 /login&lt;/code&gt;; &lt;code&gt;POST /login&lt;/code&gt; → &lt;code&gt;Set-Cookie&lt;/code&gt; with the &lt;code&gt;HttpOnly&lt;/code&gt; JWT; &lt;code&gt;GET /&lt;/code&gt; with the cookie → the dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  The responsive shell
&lt;/h2&gt;

&lt;p&gt;Around every protected page is a shell built entirely from the companion UI library (&lt;code&gt;AppShell&lt;/code&gt; / &lt;code&gt;Sidebar&lt;/code&gt; / &lt;code&gt;Topbar&lt;/code&gt; / &lt;code&gt;Breadcrumbs&lt;/code&gt; / &lt;code&gt;ThemeToggle&lt;/code&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Collapsible sidebar&lt;/strong&gt; on desktop, &lt;strong&gt;off-canvas drawer&lt;/strong&gt; on mobile — works down to 320px.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;light / dark / auto&lt;/strong&gt; theme switch, persisted per-browser and &lt;strong&gt;applied before first paint&lt;/strong&gt; (a tiny inline script reads the preference and sets the theme attribute before the body renders — no flash of the wrong theme).&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;🌐 ES / EN&lt;/strong&gt; language switch (&lt;code&gt;GET /lang/{code}&lt;/code&gt; sets a language cookie and redirects back).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything is themed through &lt;code&gt;--flv-*&lt;/code&gt; design tokens aliased to the admin's palette, so the whole panel — every packaged component — inherits the theme switch for free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Thegreekman76/fitz-liveviews
&lt;span class="nb"&gt;cd &lt;/span&gt;fitz-liveviews/examples/admin
docker compose up &lt;span class="nt"&gt;--build&lt;/span&gt;          &lt;span class="c"&gt;# Postgres + the app, one command&lt;/span&gt;
&lt;span class="c"&gt;# → http://localhost:3000/   login: admin@fitz.dev / admin1234&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What's next in this series
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;#5 — The live DataGrid.&lt;/strong&gt; The flagship's centerpiece: an employees grid with search, filters, sorting, and pagination, re-querying Postgres and diff-patching over a WebSocket on every keystroke.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Star the &lt;a href="https://github.com/Thegreekman76/fitz-liveviews" rel="noopener noreferrer"&gt;repo&lt;/a&gt; if a real app in one language is your thing. Next: the live grid.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>rust</category>
      <category>opensource</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Construyendo el flagship (1): auth por cookie browser-correcta en Fitz</title>
      <dc:creator>Martin Palopoli</dc:creator>
      <pubDate>Sat, 15 Aug 2026 11:14:36 +0000</pubDate>
      <link>https://dev.to/martin_palopoli/construyendo-el-flagship-1-auth-por-cookie-browser-correcta-en-fitz-4fmm</link>
      <guid>https://dev.to/martin_palopoli/construyendo-el-flagship-1-auth-por-cookie-browser-correcta-en-fitz-4fmm</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;TL;DR — Fitz tiene &lt;code&gt;@auth_provider&lt;/code&gt; / &lt;code&gt;@authenticated&lt;/code&gt; para APIs con Bearer token, pero un panel de administración de &lt;strong&gt;browser&lt;/strong&gt; necesita otra cosa: el browser no puede mandar un header &lt;code&gt;Authorization&lt;/code&gt; en una navegación de página ni en un handshake de WebSocket, y una request no autenticada debería &lt;strong&gt;redirigir a &lt;code&gt;/login&lt;/code&gt;&lt;/strong&gt;, no devolver un JSON 401. Así que el &lt;a href="https://github.com/Thegreekman76/fitz-liveviews/tree/main/examples/admin" rel="noopener noreferrer"&gt;Admin flagship&lt;/a&gt; usa una &lt;strong&gt;cookie&lt;/strong&gt; de sesión: el login verifica la contraseña con &lt;strong&gt;Argon2id&lt;/strong&gt;, firma un &lt;strong&gt;JWT&lt;/strong&gt;, y lo pone en una cookie &lt;code&gt;HttpOnly&lt;/code&gt;; cada página protegida lee la cookie, resuelve el usuario por el &lt;strong&gt;ORM&lt;/strong&gt;, y redirige si falla — como Django o Rails gatean una sesión de browser. Todo en Fitz, sin librería de auth externa. &lt;em&gt;(Parte 4 de la serie FitzLiveViews — la primera de varias sobre el flagship.)&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Las partes 1–3 construyeron componentes aislados. Ahora lo real: un panel de administración de back-office completo — auth, un shell responsive, DataGrids en vivo sobre Postgres, i18n, Docker. Este post es la auth y el shell; los próximos son las grillas.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;El código de abajo son extractos de la app real&lt;/strong&gt; (&lt;a href="https://github.com/Thegreekman76/fitz-liveviews/tree/main/examples/admin" rel="noopener noreferrer"&gt;&lt;code&gt;examples/admin/&lt;/code&gt;&lt;/a&gt;) para mostrar la forma — es un proyecto de 14 archivos, con Postgres, dockerizado, así que el camino garantizado-que-funciona es clonarlo y correr &lt;code&gt;docker compose up&lt;/code&gt; (ver "Probalo" al final). Los snippets son fieles a la fuente; el repo es el artefacto runnable.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Por qué cookies, no &lt;code&gt;@auth_provider&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Fitz core trae &lt;code&gt;@auth_provider&lt;/code&gt; + &lt;code&gt;@authenticated&lt;/code&gt; — y son geniales para una API JSON donde el cliente manda &lt;code&gt;Authorization: Bearer &amp;lt;token&amp;gt;&lt;/code&gt; y recibe un &lt;code&gt;401&lt;/code&gt; si falla. Un panel de administración de browser quiere dos cosas distintas:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;El browser no puede mandar un header &lt;code&gt;Authorization&lt;/code&gt;&lt;/strong&gt; en una navegación de página, ni en un handshake de WebSocket. Sí manda cookies, automático. Así que el token de sesión viaja en una cookie &lt;code&gt;HttpOnly&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Una página no autenticada debería redirigir a &lt;code&gt;/login&lt;/code&gt;&lt;/strong&gt;, no tirarle un error JSON al usuario.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Así que la app se arma una sesión por cookie a mano — que en Fitz son unas 40 líneas, porque JWT y el hashing de contraseñas están integrados en el lenguaje.&lt;/p&gt;

&lt;h2&gt;
  
  
  Login — Argon2id + JWT → Set-Cookie
&lt;/h2&gt;

&lt;p&gt;El login es un &lt;code&gt;@post&lt;/code&gt; que toma credenciales JSON, verifica la contraseña contra el hash Argon2id guardado, firma un JWT, y lo devuelve como header &lt;code&gt;Set-Cookie&lt;/code&gt; usando el built-in &lt;code&gt;Response { ... }&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/login"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;login_submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;creds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Credentials&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cookie&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Str&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Response&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="nf"&gt;find_by_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;creds&lt;/span&gt;&lt;span class="py"&gt;.email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// hash.verify es Argon2id — built-in de Fitz, sin dep passlib/bcrypt.&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;not&lt;/span&gt; &lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="nf"&gt;.verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;creds&lt;/span&gt;&lt;span class="py"&gt;.password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="py"&gt;.password_hash&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;login_failed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;claims&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="py"&gt;.email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="py"&gt;.role&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;&lt;span class="nf"&gt;.encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;jwt_secret&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;     &lt;span class="c1"&gt;// HS256, built-in&lt;/span&gt;

  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;set_cookie&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;session_cookie_name&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"="&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;
    &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"; HttpOnly; Path=/; SameSite=Lax; Max-Age=86400"&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Response&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"Set-Cookie"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;set_cookie&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;hash.verify&lt;/code&gt; (Argon2id) y &lt;code&gt;jwt.encode&lt;/code&gt; (HS256) son built-ins de Fitz — sin &lt;code&gt;pip install passlib python-jose&lt;/code&gt;, sin &lt;code&gt;npm install bcrypt jsonwebtoken&lt;/code&gt;. El built-in &lt;code&gt;Response { ... }&lt;/code&gt; es lo que deja a un handler setear un header custom en vez de devolver JSON plano.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Un detalle: el login postea JSON con un &lt;code&gt;fetch&lt;/code&gt; chiquito, no un &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; nativo, porque los handlers &lt;code&gt;@post&lt;/code&gt; de Fitz toman un body JSON, no form-urlencoded. Una mejora futura del core puede aceptar form posts.)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  El chequeo de sesión — cookie → JWT → usuario (vía el ORM)
&lt;/h2&gt;

&lt;p&gt;Resolver el usuario logueado es: sacar el token del header &lt;code&gt;Cookie&lt;/code&gt; crudo, decodificar el JWT, leer el claim &lt;code&gt;email&lt;/code&gt;, y buscar el usuario en Postgres — por el ORM de Fitz:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;user_from_cookie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cookie&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Str&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;token_from_cookie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cookie&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;          &lt;span class="c1"&gt;// parsea el header Cookie&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;claims&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;&lt;span class="nf"&gt;.decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;jwt_secret&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;    &lt;span class="c1"&gt;// verifica firma + expiración&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;claims&lt;/span&gt;&lt;span class="nf"&gt;.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="nf"&gt;.connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;db_url&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="nf"&gt;.where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="py"&gt;.email&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.first&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cualquier falla — cookie ausente, firma mala, token expirado, usuario desconocido — sale como &lt;code&gt;Err&lt;/code&gt;, que una página protegida convierte en redirect. Fijate en &lt;code&gt;User.where(fn(u) =&amp;gt; u.email == email).first(conn)&lt;/code&gt;: ese es el ORM nativo de Fitz, una query tipada contra Postgres, en el mismo archivo que la auth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proteger una página
&lt;/h2&gt;

&lt;p&gt;Un handler protegido lee la cookie con &lt;code&gt;@header(name="cookie")&lt;/code&gt; y redirige a &lt;code&gt;/login&lt;/code&gt; ante cualquier falla de auth — un &lt;code&gt;303&lt;/code&gt; real, browser-correcto:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"cookie"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;dashboard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cookie&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Str&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Response&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="nf"&gt;user_from_cookie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cookie&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;redirect_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/login"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;   &lt;span class="c1"&gt;// 303 → /login&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// ... renderiza el dashboard para `user` ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ese es todo el gate. Una request no autenticada a &lt;code&gt;/&lt;/code&gt; recibe &lt;code&gt;303 → /login&lt;/code&gt; — exactamente lo que un browser espera, y lo que no podés expresar con un auth provider de JSON-401. &lt;code&gt;/logout&lt;/code&gt; limpia la cookie (&lt;code&gt;Max-Age=0&lt;/code&gt;) y redirige. Verifiqué el flujo entero end-to-end: &lt;code&gt;GET /&lt;/code&gt; sin cookie → &lt;code&gt;303 /login&lt;/code&gt;; &lt;code&gt;POST /login&lt;/code&gt; → &lt;code&gt;Set-Cookie&lt;/code&gt; con el JWT &lt;code&gt;HttpOnly&lt;/code&gt;; &lt;code&gt;GET /&lt;/code&gt; con la cookie → el dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  El shell responsive
&lt;/h2&gt;

&lt;p&gt;Alrededor de cada página protegida hay un shell armado enteramente con la librería de UI empaquetada (&lt;code&gt;AppShell&lt;/code&gt; / &lt;code&gt;Sidebar&lt;/code&gt; / &lt;code&gt;Topbar&lt;/code&gt; / &lt;code&gt;Breadcrumbs&lt;/code&gt; / &lt;code&gt;ThemeToggle&lt;/code&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sidebar colapsable&lt;/strong&gt; en desktop, &lt;strong&gt;drawer off-canvas&lt;/strong&gt; en mobile — funciona hasta 320px.&lt;/li&gt;
&lt;li&gt;Un switch de tema &lt;strong&gt;light / dark / auto&lt;/strong&gt;, persistido por browser y &lt;strong&gt;aplicado antes del primer paint&lt;/strong&gt; (un script inline chiquito lee la preferencia y setea el atributo de tema antes de que renderice el body — sin flash del tema equivocado).&lt;/li&gt;
&lt;li&gt;Un switch de idioma &lt;strong&gt;🌐 ES / EN&lt;/strong&gt; (&lt;code&gt;GET /lang/{code}&lt;/code&gt; setea una cookie de idioma y redirige de vuelta).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Todo está tematizado con design tokens &lt;code&gt;--flv-*&lt;/code&gt; aliaseados a la paleta del admin, así que todo el panel — cada componente empaquetado — hereda el switch de tema gratis.&lt;/p&gt;

&lt;h2&gt;
  
  
  Probalo
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Thegreekman76/fitz-liveviews
&lt;span class="nb"&gt;cd &lt;/span&gt;fitz-liveviews/examples/admin
docker compose up &lt;span class="nt"&gt;--build&lt;/span&gt;          &lt;span class="c"&gt;# Postgres + la app, un comando&lt;/span&gt;
&lt;span class="c"&gt;# → http://localhost:3000/   login: admin@fitz.dev / admin1234&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Qué viene en la serie
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;#5 — El DataGrid en vivo.&lt;/strong&gt; La pieza central del flagship: una grilla de empleados con búsqueda, filtros, ordenamiento y paginación, re-consultando Postgres y diff-parcheando por WebSocket en cada tecla.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Dale una estrella al &lt;a href="https://github.com/Thegreekman76/fitz-liveviews" rel="noopener noreferrer"&gt;repo&lt;/a&gt; si una app real en un solo lenguaje es lo tuyo. Lo próximo: la grilla en vivo.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>rust</category>
      <category>opensource</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Benchmarkée el ORM Postgres nativo de mi lenguaje contra SQLAlchemy: ~8 más rápido en reads, 5.7 menos memoria — y dónde empata</title>
      <dc:creator>Martin Palopoli</dc:creator>
      <pubDate>Wed, 12 Aug 2026 09:26:12 +0000</pubDate>
      <link>https://dev.to/martin_palopoli/benchmarkee-el-orm-postgres-nativo-de-mi-lenguaje-contra-sqlalchemy-8x-mas-rapido-en-reads-57x-39bd</link>
      <guid>https://dev.to/martin_palopoli/benchmarkee-el-orm-postgres-nativo-de-mi-lenguaje-contra-sqlalchemy-8x-mas-rapido-en-reads-57x-39bd</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Parte 16 de la &lt;strong&gt;serie Fitz&lt;/strong&gt;. Fitz es un lenguaje compilado donde HTTP, Postgres, JWT y WebSockets son parte de la sintaxis. Encadena con la Parte 9 (ORM tipado + migraciones). Hoy: dejo de agitar las manos sobre performance y lo mido de verdad.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  La promesa que nadie debería creerse
&lt;/h2&gt;

&lt;p&gt;Todo lenguaje que compila a binario nativo ama decir "cero overhead". Es lo más fácil de escribir en un README y lo más difícil de respaldar. Todo el pitch de Fitz es que HTTP + Postgres viven en el &lt;em&gt;core&lt;/em&gt; del lenguaje — un driver Postgres en Rust puro compilado al binario, sin &lt;code&gt;libpq&lt;/code&gt;, sin &lt;code&gt;libpython&lt;/code&gt;, sin GIL. Eso &lt;em&gt;debería&lt;/em&gt; hacerlo rápido. Pero "debería" no es un número.&lt;/p&gt;

&lt;p&gt;Por eso el repo trae un &lt;strong&gt;benchmark reproducible&lt;/strong&gt;. No un micro-loop sintético — dos boilerplates &lt;em&gt;reales&lt;/em&gt; que cualquiera puede &lt;code&gt;git clone&lt;/code&gt; y &lt;code&gt;docker compose up&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Impl&lt;/th&gt;
&lt;th&gt;Boilerplate&lt;/th&gt;
&lt;th&gt;Stack&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Fitz ORM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;api-postgres-fitz&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Driver Postgres wire puro + ORM nativo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Python&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;api-postgres-python&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fitz + &lt;code&gt;from python import&lt;/code&gt; + SQLAlchemy 2.x + psycopg2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Mismo Postgres 16, misma red Docker, mismo host, los &lt;strong&gt;mismos tres endpoints&lt;/strong&gt; con el mismo shape de JSON:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GET /users&lt;/code&gt; — lista de 50 filas&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /users/{id}&lt;/code&gt; — un read por PK&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /users&lt;/code&gt; — insert&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Solo cambia el ORM/driver detrás. Si Fitz es más rápido, es el driver — nada más se movió.&lt;/p&gt;

&lt;h2&gt;
  
  
  Los números (v0.37.12, mediana de 3 corridas)
&lt;/h2&gt;

&lt;p&gt;Hardware: Intel Core Ultra 7 155H (16 cores), 64 GB RAM, Windows 11 + Docker 29.2.1 (WSL2). 30s sostenidos a concurrencia 10, medido con &lt;a href="https://github.com/hatoo/oha" rel="noopener noreferrer"&gt;&lt;code&gt;oha&lt;/code&gt;&lt;/a&gt;. Mediana de 3 corridas (las corridas locales varían ±10% por thermals del CPU y estado del cache).&lt;/p&gt;

&lt;h3&gt;
  
  
  Cold start, imagen, memoria
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Métrica&lt;/th&gt;
&lt;th&gt;Fitz ORM&lt;/th&gt;
&lt;th&gt;Python + SQLAlchemy&lt;/th&gt;
&lt;th&gt;Ratio&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cold start (s)&lt;/td&gt;
&lt;td&gt;0.34&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.31&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~empate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tamaño de imagen&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;134 MB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;272 MB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2× más liviano&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory peak (MB)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;9.2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;52.4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5.7× más eficiente&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;El número de memoria es el que hace mirar dos veces: &lt;strong&gt;9.2 MB vs 52.4 MB&lt;/strong&gt; bajo carga sostenida. No es un snapshot idle-en-frío — es el peak sirviendo miles de requests por segundo. Fitz carga un runtime tokio + axum + el driver, y &lt;em&gt;nada más&lt;/em&gt;. SQLAlchemy arrastra un intérprete Python, la maquinaria de instancias por fila del ORM, y un connection pool custodiado por un &lt;code&gt;threading.Lock&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Cold start es un &lt;strong&gt;empate hoy&lt;/strong&gt; — y hay que ser honesto. Fitz booteaba en 0.14s, pero desde que empezó a linkear OpenTelemetry + tracing + metrics al binario HTTP, el cold start subió a ~0.34s, al lado de Python. (Es opt-out con &lt;code&gt;@server(observability=false)&lt;/code&gt; si querés recuperar los 0.14s.)&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;GET /users&lt;/code&gt; — lista de 50 filas, 30s sostenidos, c=10
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Métrica&lt;/th&gt;
&lt;th&gt;Fitz ORM&lt;/th&gt;
&lt;th&gt;Python + SQLAlchemy&lt;/th&gt;
&lt;th&gt;Speedup&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;p50 latency (ms)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.57&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;31.24&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8.75×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p95 latency (ms)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5.76&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;56.75&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;9.85×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p99 latency (ms)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8.22&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;72.39&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8.81×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Throughput (RPS)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2618&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;297&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8.81×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;GET /users/{id}&lt;/code&gt; — un read por PK, 30s sostenidos, c=10 ⭐
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Métrica&lt;/th&gt;
&lt;th&gt;Fitz ORM&lt;/th&gt;
&lt;th&gt;Python + SQLAlchemy&lt;/th&gt;
&lt;th&gt;Speedup&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;p50 latency (ms)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.74&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;21.52&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7.85×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p95 latency (ms)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.51&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;44.07&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;9.77×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p99 latency (ms)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6.44&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;61.50&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;9.55×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Throughput (RPS)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3377&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;411&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8.22×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Los read workloads — la forma típica de una API REST — quedan alrededor de &lt;strong&gt;~8× el throughput con ~8× menos latencia&lt;/strong&gt;, y la cola (p95/p99) &lt;em&gt;ensancha&lt;/em&gt; la diferencia en vez de cerrarla. La cola de Fitz es apretada porque no hay pausa de GC ni contención de GIL serializando el manejo de requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Por qué Fitz gana en reads
&lt;/h2&gt;

&lt;p&gt;El driver Postgres en Rust puro está compilado directo al binario nativo. Un request hace: parsear el frame HTTP (axum) → armar SQL parametrizado (sin malabares de strings con muchas allocations) → un round trip a Postgres → deserializar filas a structs tipados → serializar JSON. Ese es todo el hot path, y su overhead de runtime es casi nada.&lt;/p&gt;

&lt;p&gt;SQLAlchemy suma capas en cada request: compilación de SQL del lado de Python, un connection pool coordinado con un &lt;code&gt;threading.Lock&lt;/code&gt;, y convertir cada fila del resultado en una instancia del ORM con &lt;code&gt;__init__&lt;/code&gt; por fila — todo serializado por el GIL. Nada de eso es &lt;em&gt;lento&lt;/em&gt; aislado; es simplemente trabajo que Fitz no hace.&lt;/p&gt;

&lt;h2&gt;
  
  
  Por qué Python no es ridículamente lento (la parte honesta)
&lt;/h2&gt;

&lt;p&gt;SQLAlchemy 2.x está genuinamente bien optimizado, y el GIL solo bloquea &lt;em&gt;Python puro&lt;/em&gt; — no la ejecución de SQL, no el I/O. Para un workload DB-bound, el verdadero cuello de botella suele ser Postgres, no el lenguaje encima. Por eso exactamente los writes empatan:&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;POST /users&lt;/code&gt; — 100 sequential, body único por request
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Métrica&lt;/th&gt;
&lt;th&gt;Fitz ORM&lt;/th&gt;
&lt;th&gt;Python + SQLAlchemy&lt;/th&gt;
&lt;th&gt;Speedup&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;p50 latency (ms)&lt;/td&gt;
&lt;td&gt;174.69&lt;/td&gt;
&lt;td&gt;190.23&lt;/td&gt;
&lt;td&gt;1.09×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p95 latency (ms)&lt;/td&gt;
&lt;td&gt;243.97&lt;/td&gt;
&lt;td&gt;271.75&lt;/td&gt;
&lt;td&gt;1.11×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Throughput (RPS)&lt;/td&gt;
&lt;td&gt;3.28&lt;/td&gt;
&lt;td&gt;2.98&lt;/td&gt;
&lt;td&gt;1.10×&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Eso es un &lt;strong&gt;empate técnico&lt;/strong&gt; — y tengo que ser directo con el &lt;em&gt;por qué&lt;/em&gt;. Esto no mide el throughput de escritura del server; mide el cliente. El bench hace un loop &lt;code&gt;curl&lt;/code&gt; sequential con un email único por request, y en Git Bash sobre Windows cada subshell arrastra ~1s de overhead. Que la latencia per-request sea ~igual te dice que el cuello de botella es el write durable de Postgres, no el server. Medir throughput de POST honesto necesita &lt;code&gt;k6&lt;/code&gt; o &lt;code&gt;wrk+lua&lt;/code&gt; con randomización de bodies — eso es una extensión futura, no un número que voy a maquillar.&lt;/p&gt;

&lt;h2&gt;
  
  
  El bug que alguna vez hizo a Fitz 30% MÁS LENTO que Python
&lt;/h2&gt;

&lt;p&gt;La parte más instructiva de toda esta historia: &lt;strong&gt;Fitz alguna vez perdió.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;En una versión anterior, &lt;code&gt;GET /users/{id}&lt;/code&gt; tenía un p50 de &lt;strong&gt;43.70 ms&lt;/strong&gt; — como 30% más lento que SQLAlchemy sobre exactamente la misma query. Para un "binario nativo con driver puro", eso fue humillante y, honestamente, un gran empujón para profilear de verdad en vez de asumir.&lt;/p&gt;

&lt;p&gt;El culpable era el &lt;strong&gt;algoritmo de Nagle&lt;/strong&gt;. El driver mandaba los cinco mensajes del Extended Query Protocol de Postgres (Parse / Bind / Describe / Execute / Sync) como cinco &lt;code&gt;write().await&lt;/code&gt; separados. Nagle coalescía los paquetes chicos y esperaba un delayed-ACK — sumando ~40 ms a cada query parametrizada. El fix fueron dos líneas en el driver:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;set_nodelay(true)&lt;/code&gt; en el &lt;code&gt;TcpStream&lt;/code&gt; (deshabilita Nagle entre cliente y server).&lt;/li&gt;
&lt;li&gt;Batchear los cinco mensajes del protocolo en un solo &lt;code&gt;write_all&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Resultado: &lt;code&gt;GET /users/{id}&lt;/code&gt; pasó de &lt;strong&gt;43.70 ms → ~2.7 ms p50&lt;/strong&gt; — una mejora de ~16×, dando vuelta la historia de "Fitz pierde" a "Fitz gana ~8×". Estable ahí desde entonces.&lt;/p&gt;

&lt;p&gt;La lección es exactamente para lo que &lt;em&gt;sirven&lt;/em&gt; los benchmarks: no existen para que un gráfico se vea lindo, existen para cazarte siendo lento. Sin un bench reproducible, esos 40 ms seguirían ahí.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducilo vos mismo
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;benchmarks/orm-vs-sqlalchemy
bash run.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El script levanta cada boilerplate con &lt;code&gt;docker compose up -d --build&lt;/code&gt;, espera el primer 200, seedea users, bencha cada endpoint con &lt;code&gt;oha&lt;/code&gt;, muestrea memoria via &lt;code&gt;docker stats&lt;/code&gt;, y escribe un &lt;code&gt;summary.md&lt;/code&gt;. Para números publicables, corrélo tres veces y tomá la mediana — los ratios headline (5.7× memoria, ~8× reads) son estables entre corridas; solo se mueven los decimales.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo que &lt;em&gt;no&lt;/em&gt; estoy afirmando
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reads, no writes.&lt;/strong&gt; La ventaja es en workloads read-heavy. El throughput de POST es un artefacto client-side de este bench, no una medición del server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mediana, no best-case.&lt;/strong&gt; Los números absolutos se mueven con la carga de la máquina. Los &lt;em&gt;ratios&lt;/em&gt; son lo que se sostiene.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Todavía sin queries con JOINs pesados.&lt;/strong&gt; Eager loading, agregaciones y window functions tienen perfiles distintos. Hay un bench mixed-workload separado (Fitz vs Python vs Node) para el panorama más completo.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Si querés las tripas del driver — wire protocol v3.0, SCRAM-SHA-256, el connection pool — están todas en &lt;code&gt;src/db.rs&lt;/code&gt;, un solo archivo, sin &lt;code&gt;libpq&lt;/code&gt;. Ese es todo el punto: lo que lo hace rápido es algo que podés leer.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Próximo en la serie: más del stack que está incorporado en el lenguaje, y los modos de falla honestos que sigo encontrando construyendo productos reales con él.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>opensource</category>
      <category>rust</category>
      <category>postgres</category>
    </item>
    <item>
      <title>I benchmarked my language's native Postgres ORM against SQLAlchemy: ~8 faster reads, 5.7 less memory — and where it ties</title>
      <dc:creator>Martin Palopoli</dc:creator>
      <pubDate>Wed, 12 Aug 2026 09:25:05 +0000</pubDate>
      <link>https://dev.to/martin_palopoli/i-benchmarked-my-languages-native-postgres-orm-against-sqlalchemy-8x-faster-reads-57x-less-4ogn</link>
      <guid>https://dev.to/martin_palopoli/i-benchmarked-my-languages-native-postgres-orm-against-sqlalchemy-8x-faster-reads-57x-less-4ogn</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Part 16 of the &lt;strong&gt;Fitz series&lt;/strong&gt;. Fitz is a compiled language where HTTP, Postgres, JWT and WebSockets are part of the syntax. Follows Part 9 (typed ORM + migrations). Today: I stop hand-waving about performance and actually measure it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The claim nobody should take on faith
&lt;/h2&gt;

&lt;p&gt;Every language that compiles to a native binary loves to say "zero overhead." It's the easiest thing to write in a README and the hardest thing to back up. Fitz's whole pitch is that HTTP + Postgres live in the &lt;em&gt;core&lt;/em&gt; of the language — a pure-Rust Postgres driver compiled into the binary, no &lt;code&gt;libpq&lt;/code&gt;, no &lt;code&gt;libpython&lt;/code&gt;, no GIL. That should make it fast. But "should" isn't a number.&lt;/p&gt;

&lt;p&gt;So the repo ships a &lt;strong&gt;reproducible benchmark&lt;/strong&gt;. Not a synthetic micro-loop — two &lt;em&gt;actual&lt;/em&gt; boilerplates that any user can &lt;code&gt;git clone&lt;/code&gt; and &lt;code&gt;docker compose up&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Impl&lt;/th&gt;
&lt;th&gt;Boilerplate&lt;/th&gt;
&lt;th&gt;Stack&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Fitz ORM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;api-postgres-fitz&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pure Postgres wire driver + native ORM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Python&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;api-postgres-python&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fitz + &lt;code&gt;from python import&lt;/code&gt; + SQLAlchemy 2.x + psycopg2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same Postgres 16, same Docker network, same host, the &lt;strong&gt;same three endpoints&lt;/strong&gt; with the same JSON shape:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GET /users&lt;/code&gt; — list of 50 rows&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /users/{id}&lt;/code&gt; — single read by PK&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /users&lt;/code&gt; — insert&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only the ORM/driver behind them changes. If Fitz is faster, it's the driver — nothing else moved.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers (v0.37.12, median of 3 runs)
&lt;/h2&gt;

&lt;p&gt;Hardware: Intel Core Ultra 7 155H (16 cores), 64 GB RAM, Windows 11 + Docker 29.2.1 (WSL2). Sustained 30s at concurrency 10, measured with &lt;a href="https://github.com/hatoo/oha" rel="noopener noreferrer"&gt;&lt;code&gt;oha&lt;/code&gt;&lt;/a&gt;. Median of 3 runs (local runs swing ±10% with CPU thermals and cache state).&lt;/p&gt;

&lt;h3&gt;
  
  
  Cold start, image, memory
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Fitz ORM&lt;/th&gt;
&lt;th&gt;Python + SQLAlchemy&lt;/th&gt;
&lt;th&gt;Ratio&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cold start (s)&lt;/td&gt;
&lt;td&gt;0.34&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.31&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~tie&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image size&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;134 MB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;272 MB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2× leaner&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory peak (MB)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;9.2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;52.4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5.7× more efficient&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The memory number is the one that makes people do a double take: &lt;strong&gt;9.2 MB vs 52.4 MB&lt;/strong&gt; under sustained load. That's not a warm-idle snapshot — it's the peak while serving thousands of requests per second. Fitz carries a tokio runtime + axum + the driver, and that's &lt;em&gt;it&lt;/em&gt;. SQLAlchemy drags a Python interpreter, the ORM's per-row instance machinery, and a connection pool guarded by &lt;code&gt;threading.Lock&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Cold start is a &lt;strong&gt;tie now&lt;/strong&gt; — worth being honest about. Fitz used to boot in 0.14s, but since it started linking OpenTelemetry + tracing + metrics into the HTTP binary, cold start rose to ~0.34s, right next to Python. (It's opt-out with &lt;code&gt;@server(observability=false)&lt;/code&gt; if you want the 0.14s back.)&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;GET /users&lt;/code&gt; — list of 50 rows, 30s sustained, c=10
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Fitz ORM&lt;/th&gt;
&lt;th&gt;Python + SQLAlchemy&lt;/th&gt;
&lt;th&gt;Speedup&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;p50 latency (ms)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.57&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;31.24&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8.75×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p95 latency (ms)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5.76&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;56.75&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;9.85×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p99 latency (ms)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8.22&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;72.39&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8.81×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Throughput (RPS)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2618&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;297&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8.81×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;GET /users/{id}&lt;/code&gt; — single read by PK, 30s sustained, c=10 ⭐
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Fitz ORM&lt;/th&gt;
&lt;th&gt;Python + SQLAlchemy&lt;/th&gt;
&lt;th&gt;Speedup&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;p50 latency (ms)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.74&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;21.52&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7.85×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p95 latency (ms)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.51&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;44.07&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;9.77×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p99 latency (ms)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6.44&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;61.50&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;9.55×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Throughput (RPS)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3377&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;411&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8.22×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read workloads — the typical shape of a REST API — land around &lt;strong&gt;~8× the throughput at ~8× lower latency&lt;/strong&gt;, and the tail (p95/p99) actually widens the gap rather than closing it. Fitz's tail is tight because there's no GC pause and no GIL contention serializing request handling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Fitz wins the reads
&lt;/h2&gt;

&lt;p&gt;The pure-Rust Postgres driver is compiled straight into the native binary. A request does: parse the HTTP frame (axum) → build parameterized SQL (no allocation-heavy string juggling) → one round trip to Postgres → deserialize rows into typed structs → serialize JSON. That's the whole hot path, and its runtime overhead is close to nothing.&lt;/p&gt;

&lt;p&gt;SQLAlchemy adds layers on every request: SQL compilation on the Python side, a connection pool coordinated with a &lt;code&gt;threading.Lock&lt;/code&gt;, and converting each result row into an ORM instance with &lt;code&gt;__init__&lt;/code&gt; per row — all serialized by the GIL. None of that is &lt;em&gt;slow&lt;/em&gt; in isolation; it's just work Fitz simply doesn't do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Python isn't ridiculously slow (the honest part)
&lt;/h2&gt;

&lt;p&gt;SQLAlchemy 2.x is genuinely well-optimized, and the GIL only blocks &lt;em&gt;pure Python&lt;/em&gt; — not SQL execution, not I/O. For a DB-bound workload, the real bottleneck is usually Postgres, not the language on top. That's exactly why the writes tie:&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;POST /users&lt;/code&gt; — 100 sequential, unique body per request
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Fitz ORM&lt;/th&gt;
&lt;th&gt;Python + SQLAlchemy&lt;/th&gt;
&lt;th&gt;Speedup&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;p50 latency (ms)&lt;/td&gt;
&lt;td&gt;174.69&lt;/td&gt;
&lt;td&gt;190.23&lt;/td&gt;
&lt;td&gt;1.09×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p95 latency (ms)&lt;/td&gt;
&lt;td&gt;243.97&lt;/td&gt;
&lt;td&gt;271.75&lt;/td&gt;
&lt;td&gt;1.11×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Throughput (RPS)&lt;/td&gt;
&lt;td&gt;3.28&lt;/td&gt;
&lt;td&gt;2.98&lt;/td&gt;
&lt;td&gt;1.10×&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's a &lt;strong&gt;technical tie&lt;/strong&gt; — and I have to be upfront about &lt;em&gt;why&lt;/em&gt;. This isn't measuring server write throughput; it's measuring the client. The bench does a sequential &lt;code&gt;curl&lt;/code&gt; loop with a unique email per request, and in Git Bash on Windows each subshell carries ~1s of overhead. The per-request latency being ~equal tells you the bottleneck is Postgres' durable write, not the server. Measuring honest POST throughput needs &lt;code&gt;k6&lt;/code&gt; or &lt;code&gt;wrk+lua&lt;/code&gt; with body randomization — that's a future extension, not a number I'll dress up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug that once made Fitz 30% SLOWER than Python
&lt;/h2&gt;

&lt;p&gt;The most instructive part of this whole story: &lt;strong&gt;Fitz used to lose.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In an earlier version, &lt;code&gt;GET /users/{id}&lt;/code&gt; had a p50 of &lt;strong&gt;43.70 ms&lt;/strong&gt; — about 30% slower than SQLAlchemy on the exact same query. For a "native binary with a pure driver," that was humiliating and, frankly, a great forcing function to actually profile instead of assume.&lt;/p&gt;

&lt;p&gt;The culprit was &lt;strong&gt;Nagle's algorithm&lt;/strong&gt;. The driver sent the five messages of Postgres' Extended Query Protocol (Parse / Bind / Describe / Execute / Sync) as five separate &lt;code&gt;write().await&lt;/code&gt; calls. Nagle coalesced small packets and waited for a delayed-ACK — piling ~40 ms onto every parameterized query. The fix was two lines in the driver:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;set_nodelay(true)&lt;/code&gt; on the &lt;code&gt;TcpStream&lt;/code&gt; (disable Nagle between client and server).&lt;/li&gt;
&lt;li&gt;Batch all five protocol messages into a single &lt;code&gt;write_all&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Result: &lt;code&gt;GET /users/{id}&lt;/code&gt; went from &lt;strong&gt;43.70 ms → ~2.7 ms p50&lt;/strong&gt; — a ~16× improvement, flipping the story from "Fitz loses" to "Fitz wins ~8×." It's been stable there ever since.&lt;/p&gt;

&lt;p&gt;The lesson is the one benchmarks are actually &lt;em&gt;for&lt;/em&gt;: they don't exist to make a graph look good, they exist to catch you being slow. Without a reproducible bench, that 40 ms would still be there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce it yourself
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;benchmarks/orm-vs-sqlalchemy
bash run.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script spins up each boilerplate with &lt;code&gt;docker compose up -d --build&lt;/code&gt;, waits for the first 200, seeds users, benches each endpoint with &lt;code&gt;oha&lt;/code&gt;, samples memory via &lt;code&gt;docker stats&lt;/code&gt;, and writes a &lt;code&gt;summary.md&lt;/code&gt;. For publishable numbers, run it three times and take the median — the headline ratios (5.7× memory, ~8× reads) are stable across runs; only the decimals move.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm &lt;em&gt;not&lt;/em&gt; claiming
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reads, not writes.&lt;/strong&gt; The win is on read-heavy workloads. POST throughput is a client-side artifact of this bench, not a server measurement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Median, not best-case.&lt;/strong&gt; Absolute numbers shift with the machine's load. The &lt;em&gt;ratios&lt;/em&gt; are what hold.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No JOIN-heavy queries yet.&lt;/strong&gt; Eager loading, aggregations, and window functions have different profiles. There's a separate mixed-workload bench (Fitz vs Python vs Node) for the fuller picture.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want the raw driver internals — wire protocol v3.0, SCRAM-SHA-256, the connection pool — they're all in &lt;code&gt;src/db.rs&lt;/code&gt;, one file, no &lt;code&gt;libpq&lt;/code&gt;. That's the whole point: the thing that makes it fast is a thing you can read.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Next up in the series: more of the stack that's built into the language, and the honest failure modes I keep finding while building real products with it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>opensource</category>
      <category>rust</category>
      <category>postgres</category>
    </item>
  </channel>
</rss>
