<?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>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>
    <item>
      <title>Forms, payloads, and live inputs in Fitz LiveViews</title>
      <dc:creator>Martin Palopoli</dc:creator>
      <pubDate>Tue, 11 Aug 2026 09:51:12 +0000</pubDate>
      <link>https://dev.to/martin_palopoli/forms-payloads-and-live-inputs-in-fitz-liveviews-42od</link>
      <guid>https://dev.to/martin_palopoli/forms-payloads-and-live-inputs-in-fitz-liveviews-42od</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;TL;DR — Events in &lt;a href="https://github.com/Thegreekman76/fitz-liveviews" rel="noopener noreferrer"&gt;Fitz LiveViews&lt;/a&gt; carry data three ways: a &lt;strong&gt;click payload&lt;/strong&gt; (&lt;code&gt;data-flv-value-*&lt;/code&gt;) tags a button with the value it should send; a &lt;strong&gt;form submit&lt;/strong&gt; (&lt;code&gt;data-flv-submit&lt;/code&gt;) reads the form's named inputs; and a &lt;strong&gt;live value&lt;/strong&gt; (&lt;code&gt;@input&lt;/code&gt; / &lt;code&gt;@change&lt;/code&gt;) delivers a control's current value in &lt;code&gt;payload["value"]&lt;/code&gt;. All three land in the same place — a &lt;code&gt;payload&lt;/code&gt; map your handler reads. This post builds a live name list (add / remove / count) that runs both server-rendered and as WebAssembly. &lt;em&gt;(Part 3 of the FitzLiveViews series.)&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Parts &lt;a href="https://dev.to/"&gt;1&lt;/a&gt; and &lt;a href="https://dev.to/"&gt;2&lt;/a&gt; covered the pitch and the counter. A counter only reads &lt;code&gt;+1&lt;/code&gt;/&lt;code&gt;-1&lt;/code&gt; — no data flows &lt;em&gt;in&lt;/em&gt;. Real UIs take input: text, selections, form fields. Here's how that data reaches your handlers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;payload&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Every event handler has a &lt;code&gt;payload&lt;/code&gt; in scope — a &lt;code&gt;Map&amp;lt;Str, Str&amp;gt;&lt;/code&gt;. The three mechanisms below all fill it; your handler reads it with &lt;code&gt;payload["key"]&lt;/code&gt; (guard with &lt;code&gt;payload.has("key")&lt;/code&gt;):&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Click payload — a button that carries a value
&lt;/h3&gt;

&lt;p&gt;Tag any element with &lt;code&gt;data-flv-value-&amp;lt;key&amp;gt;="{expr}"&lt;/code&gt;, and when a &lt;code&gt;data-flv-click&lt;/code&gt; on it (or an ancestor) fires, that value rides along:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;data-flv-click=&lt;/span&gt;&lt;span class="s"&gt;"remove"&lt;/span&gt; &lt;span class="na"&gt;data-flv-value-item=&lt;/span&gt;&lt;span class="s"&gt;"{it}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;×&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;item&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;target&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;item&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;names&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;names&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;it&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nx"&gt;target&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;The delete button &lt;em&gt;knows which row it is&lt;/em&gt; because the row's value is stamped on it. No IDs threaded through a callback, no closure capture.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Form submit — the whole form at once
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;data-flv-submit="handler"&lt;/code&gt; on a &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; reads each named input into the payload on submit; &lt;code&gt;data-flv-clear&lt;/code&gt; resets a field afterward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;data-flv-submit=&lt;/span&gt;&lt;span class="s"&gt;"add"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"item"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Add a name"&lt;/span&gt; &lt;span class="na"&gt;data-flv-clear&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Add&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;item&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;n&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;item&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;names&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&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;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;payload["item"]&lt;/code&gt; is the input's value at submit time. No &lt;code&gt;preventDefault&lt;/code&gt;, no &lt;code&gt;FormData&lt;/code&gt;, no &lt;code&gt;fetch&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Live value — &lt;code&gt;@input&lt;/code&gt; / &lt;code&gt;@change&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;For a control that reports as you type or on selection, &lt;code&gt;@input&lt;/code&gt; (every keystroke) and &lt;code&gt;@change&lt;/code&gt; (on blur/selection) deliver the current value in &lt;code&gt;payload["value"]&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;input=&lt;/span&gt;&lt;span class="s"&gt;"on_name"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"{name}"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;select&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;change=&lt;/span&gt;&lt;span class="s"&gt;"on_color"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt; … &lt;span class="nt"&gt;&amp;lt;/select&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same &lt;code&gt;payload["value"]&lt;/code&gt; covers &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt;, and &lt;code&gt;&amp;lt;textarea&amp;gt;&lt;/code&gt;. On the server-rendered target this is the classic &lt;code&gt;data-flv-change&lt;/code&gt; attribute; on the client-WASM target it's the &lt;code&gt;@input&lt;/code&gt; / &lt;code&gt;@change&lt;/code&gt; decorator — same payload, same handler, so one &lt;code&gt;.fitzv&lt;/code&gt; serves both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it together — a live name list
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;component NameList {
  state {
    names: List&lt;span class="nt"&gt;&amp;lt;Str&amp;gt;&lt;/span&gt; = ["Ada", "Grace", "Margaret"]
  }

  event add() {
    if (payload.has("item")) {
      let n = payload["item"]
      if (n != "") { names.push(n) }
    }
  }

  event remove() {
    if (payload.has("item")) {
      let target = payload["item"]
      names = names.filter(fn(it) =&amp;gt; it != target)
    }
  }

  &lt;span class="nt"&gt;&amp;lt;template&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"names"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;data-flv-submit=&lt;/span&gt;&lt;span class="s"&gt;"add"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"item"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Add a name"&lt;/span&gt; &lt;span class="na"&gt;data-flv-clear&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Add&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
        {#for it in names}
          &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;{it} &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;data-flv-click=&lt;/span&gt;&lt;span class="s"&gt;"remove"&lt;/span&gt; &lt;span class="na"&gt;data-flv-value-item=&lt;/span&gt;&lt;span class="s"&gt;"{it}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;×&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
        {/for}
      &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;{names.len()} total&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;{#for it in names}&lt;/code&gt; iterates; &lt;code&gt;{it}&lt;/code&gt; interpolates each item; the remove button stamps its own value. Add a name, remove a row, watch the count — no client code, no API.&lt;/p&gt;

&lt;p&gt;Wire it into a &lt;code&gt;main.fitz&lt;/code&gt; (the same shape as the counter in &lt;a href="https://dev.to/"&gt;part 2&lt;/a&gt;) and it runs:&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&lt;/span&gt; &lt;span class="kn"&gt;import&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;live_layout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;html_response&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;diff_html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dispatch_component_events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flv_register&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;NameList&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;NameList&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NameList_render&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NameList_add&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NameList_remove&lt;/span&gt;

&lt;span class="nd"&gt;@get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&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;return&lt;/span&gt; &lt;span class="nf"&gt;html_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;live_layout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/live/names&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;names-app&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nf"&gt;component&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NameList&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;root&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@ws&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/live/names&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;socket&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="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&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;component&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NameList&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;root&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;
  &lt;span class="n"&gt;loop&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;recv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;
    &lt;span class="n"&gt;let&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dispatch_component_events&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;let&lt;/span&gt; &lt;span class="n"&gt;new_html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;component&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NameList&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;root&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;
    &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&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;new_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="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;new_html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="err"&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;new_html&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@server&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;fitz run&lt;/code&gt;, open &lt;code&gt;http://localhost:3000&lt;/code&gt;, add and remove names. (I ran exactly this — the page renders the seeded list, the count, and the wired form/buttons.) The &lt;strong&gt;client-WASM build of the same list&lt;/strong&gt; is running in the &lt;a href="https://thegreekman76.github.io/fitz-liveviews/live/embed/?c=namelist" rel="noopener noreferrer"&gt;live gallery&lt;/a&gt; — add/remove/count entirely in your browser, offline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two honest caveats
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live text inputs re-mount.&lt;/strong&gt; The current rendering model is dirty-flag + naive re-render: a state change rebuilds the component's DOM. For a &lt;code&gt;&amp;lt;select&amp;gt; @change&lt;/code&gt; that's invisible; for a live text &lt;code&gt;&amp;lt;input&amp;gt; @input&lt;/code&gt;, the field re-mounts each keystroke — the value re-binds via &lt;code&gt;value="{name}"&lt;/code&gt;, but the caret jumps to the end. Fine-grained reactivity (patch in place) is the next rendering-model step, and it's what fixes this. The value always reaches the handler reliably; that's what &lt;code&gt;@input&lt;/code&gt; guarantees today.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;String methods on WASM — now supported.&lt;/strong&gt; A case-insensitive filter (&lt;code&gt;names.filter(fn(x) =&amp;gt; q == "" or x.lower().contains(q.lower()))&lt;/code&gt;) used to be server-rendered only; as of Fitz core v0.29.8 the string methods (&lt;code&gt;.lower&lt;/code&gt; / &lt;code&gt;.contains&lt;/code&gt; / …) and logical &lt;code&gt;and&lt;/code&gt;/&lt;code&gt;or&lt;/code&gt; compile on the client-WASM target too, so the filter runs offline. The only remaining wrinkle is the caret caveat above for the &lt;em&gt;filter box itself&lt;/em&gt; while you type.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;#4+ — Building the flagship.&lt;/strong&gt; A complete admin panel in Fitz + Fitz LiveViews: cookie-based auth (Argon2id + JWT), live DataGrids querying Postgres over WebSockets, the packaged UI library, i18n, and a one-command Docker setup.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If typed events without a client framework sound good, star the &lt;a href="https://github.com/Thegreekman76/fitz-liveviews" rel="noopener noreferrer"&gt;repo&lt;/a&gt;. Next: a real app.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>rust</category>
      <category>opensource</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Forms, payloads e inputs en vivo en Fitz LiveViews</title>
      <dc:creator>Martin Palopoli</dc:creator>
      <pubDate>Tue, 11 Aug 2026 09:49:07 +0000</pubDate>
      <link>https://dev.to/martin_palopoli/forms-payloads-e-inputs-en-vivo-en-fitz-liveviews-4fmi</link>
      <guid>https://dev.to/martin_palopoli/forms-payloads-e-inputs-en-vivo-en-fitz-liveviews-4fmi</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;TL;DR — Los eventos en &lt;a href="https://github.com/Thegreekman76/fitz-liveviews" rel="noopener noreferrer"&gt;Fitz LiveViews&lt;/a&gt; llevan data de tres formas: un &lt;strong&gt;click payload&lt;/strong&gt; (&lt;code&gt;data-flv-value-*&lt;/code&gt;) marca un botón con el valor que tiene que mandar; un &lt;strong&gt;form submit&lt;/strong&gt; (&lt;code&gt;data-flv-submit&lt;/code&gt;) lee los inputs con nombre del form; y un &lt;strong&gt;valor en vivo&lt;/strong&gt; (&lt;code&gt;@input&lt;/code&gt; / &lt;code&gt;@change&lt;/code&gt;) entrega el valor actual de un control en &lt;code&gt;payload["value"]&lt;/code&gt;. Las tres caen en el mismo lado — un mapa &lt;code&gt;payload&lt;/code&gt; que tu handler lee. Este post construye una lista de nombres en vivo (agregar / quitar / contar) que corre server-rendered y como WebAssembly. &lt;em&gt;(Parte 3 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;1&lt;/a&gt; y &lt;a href="https://dev.to/"&gt;2&lt;/a&gt; cubrieron el pitch y el counter. Un counter solo lee &lt;code&gt;+1&lt;/code&gt;/&lt;code&gt;-1&lt;/code&gt; — no entra data. Las UIs reales toman input: texto, selecciones, campos de formulario. Acá está cómo esa data llega a tus handlers.&lt;/p&gt;

&lt;h2&gt;
  
  
  El &lt;code&gt;payload&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Cada handler de evento tiene un &lt;code&gt;payload&lt;/code&gt; en scope — un &lt;code&gt;Map&amp;lt;Str, Str&amp;gt;&lt;/code&gt;. Los tres mecanismos de abajo lo llenan; tu handler lo lee con &lt;code&gt;payload["clave"]&lt;/code&gt; (guardá con &lt;code&gt;payload.has("clave")&lt;/code&gt;):&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Click payload — un botón que lleva un valor
&lt;/h3&gt;

&lt;p&gt;Marcá cualquier elemento con &lt;code&gt;data-flv-value-&amp;lt;clave&amp;gt;="{expr}"&lt;/code&gt;, y cuando un &lt;code&gt;data-flv-click&lt;/code&gt; sobre él (o un ancestro) dispara, ese valor viaja:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;data-flv-click=&lt;/span&gt;&lt;span class="s"&gt;"remove"&lt;/span&gt; &lt;span class="na"&gt;data-flv-value-item=&lt;/span&gt;&lt;span class="s"&gt;"{it}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;×&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;item&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;target&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;item&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;names&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;names&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;it&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nx"&gt;target&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;El botón de borrar &lt;em&gt;sabe qué fila es&lt;/em&gt; porque el valor de la fila está estampado en él. Sin IDs pasados por un callback, sin captura de closure.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Form submit — el form entero de una
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;data-flv-submit="handler"&lt;/code&gt; en un &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; lee cada input con nombre al payload en el submit; &lt;code&gt;data-flv-clear&lt;/code&gt; limpia el campo después:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;data-flv-submit=&lt;/span&gt;&lt;span class="s"&gt;"add"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"item"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Agregar un nombre"&lt;/span&gt; &lt;span class="na"&gt;data-flv-clear&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Agregar&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;item&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;n&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;item&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;names&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&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;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;payload["item"]&lt;/code&gt; es el valor del input al momento del submit. Sin &lt;code&gt;preventDefault&lt;/code&gt;, sin &lt;code&gt;FormData&lt;/code&gt;, sin &lt;code&gt;fetch&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Valor en vivo — &lt;code&gt;@input&lt;/code&gt; / &lt;code&gt;@change&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Para un control que reporta mientras tipeás o al seleccionar, &lt;code&gt;@input&lt;/code&gt; (cada tecla) y &lt;code&gt;@change&lt;/code&gt; (al blur/selección) entregan el valor actual en &lt;code&gt;payload["value"]&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;input=&lt;/span&gt;&lt;span class="s"&gt;"on_name"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"{name}"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;select&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;change=&lt;/span&gt;&lt;span class="s"&gt;"on_color"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt; … &lt;span class="nt"&gt;&amp;lt;/select&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El mismo &lt;code&gt;payload["value"]&lt;/code&gt; cubre &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt; y &lt;code&gt;&amp;lt;textarea&amp;gt;&lt;/code&gt;. En el target server-rendered esto es el atributo clásico &lt;code&gt;data-flv-change&lt;/code&gt;; en el target client-WASM es el decorador &lt;code&gt;@input&lt;/code&gt; / &lt;code&gt;@change&lt;/code&gt; — mismo payload, mismo handler, así que un &lt;code&gt;.fitzv&lt;/code&gt; sirve a los dos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Todo junto — una lista de nombres en vivo
&lt;/h2&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;NameList&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;names&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Str&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Ada&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Grace&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Margaret&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="nx"&gt;event&lt;/span&gt; &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;item&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;n&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;item&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;names&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&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;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;item&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;target&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;item&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="nx"&gt;names&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;names&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;it&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nx"&gt;target&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;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;div&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;names&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="nx"&gt;form&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;flv&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;submit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;add&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="nx"&gt;input&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;item&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;placeholder&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Agregar un nombre&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;flv&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;clear&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;button&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;submit&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;Agregar&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;/form&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;ul&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;it&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;names&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;li&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;it&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;button&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;flv&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&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;remove&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;flv&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;{it}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;×&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="nx"&gt;li&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;for&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;/ul&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;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;names&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt; &lt;span class="nx"&gt;total&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="sr"&gt;/div&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;&lt;code&gt;{#for it in names}&lt;/code&gt; itera; &lt;code&gt;{it}&lt;/code&gt; interpola cada ítem; el botón de borrar estampa su propio valor. Agregá un nombre, quitá una fila, mirá el contador — sin código de cliente, sin API.&lt;/p&gt;

&lt;p&gt;Cablealo a un &lt;code&gt;main.fitz&lt;/code&gt; (la misma forma que el counter de la &lt;a href="https://dev.to/"&gt;parte 2&lt;/a&gt;) y corre:&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&lt;/span&gt; &lt;span class="kn"&gt;import&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;live_layout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;html_response&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;diff_html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dispatch_component_events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flv_register&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;NameList&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;NameList&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NameList_render&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NameList_add&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NameList_remove&lt;/span&gt;

&lt;span class="nd"&gt;@get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&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;return&lt;/span&gt; &lt;span class="nf"&gt;html_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;live_layout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/live/names&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;names-app&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nf"&gt;component&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NameList&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;root&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@ws&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/live/names&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;socket&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="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&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;component&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NameList&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;root&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;
  &lt;span class="n"&gt;loop&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;recv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;
    &lt;span class="n"&gt;let&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dispatch_component_events&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;let&lt;/span&gt; &lt;span class="n"&gt;new_html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;component&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NameList&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;root&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;
    &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&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;new_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="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;new_html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="err"&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;new_html&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@server&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;fitz run&lt;/code&gt;, abrís &lt;code&gt;http://localhost:3000&lt;/code&gt;, agregás y quitás nombres. (Corrí exactamente esto — la página renderiza la lista sembrada, el contador, y el form/botones cableados.) El &lt;strong&gt;build client-WASM de la misma lista&lt;/strong&gt; está corriendo en la &lt;a href="https://thegreekman76.github.io/fitz-liveviews/live/embed/?c=namelist" rel="noopener noreferrer"&gt;galería en vivo&lt;/a&gt; — agregar/quitar/contar entero en tu browser, offline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dos caveats honestos
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Los inputs de texto en vivo se re-montan.&lt;/strong&gt; El modelo de render actual es dirty-flag + naive re-render: un cambio de estado reconstruye el DOM del componente. Para un &lt;code&gt;&amp;lt;select&amp;gt; @change&lt;/code&gt; es invisible; para un &lt;code&gt;&amp;lt;input&amp;gt; @input&lt;/code&gt; en vivo, el campo se re-monta en cada tecla — el valor se re-bindea con &lt;code&gt;value="{name}"&lt;/code&gt;, pero el caret salta al final. La reactividad fine-grained (patch in-place) es el próximo paso del modelo de render, y es lo que arregla esto. El valor siempre llega al handler de forma confiable; eso es lo que &lt;code&gt;@input&lt;/code&gt; garantiza hoy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Métodos de string en WASM — ahora sí.&lt;/strong&gt; Un filtro case-insensitive (&lt;code&gt;names.filter(fn(x) =&amp;gt; q == "" or x.lower().contains(q.lower()))&lt;/code&gt;) antes era solo server-rendered; desde Fitz core v0.29.8 los métodos de string (&lt;code&gt;.lower&lt;/code&gt; / &lt;code&gt;.contains&lt;/code&gt; / …) y el &lt;code&gt;and&lt;/code&gt;/&lt;code&gt;or&lt;/code&gt; lógico compilan también en el target client-WASM, así que el filtro corre offline. Lo único que queda es el caret caveat de arriba para &lt;em&gt;la caja del filtro&lt;/em&gt; mientras tipeás.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;#4+ — Construyendo el flagship.&lt;/strong&gt; Un panel de administración completo en Fitz + Fitz LiveViews: auth por cookie (Argon2id + JWT), DataGrids en vivo consultando Postgres por WebSockets, la librería de UI empaquetada, i18n, y un setup Docker de un comando.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Si eventos tipados sin framework de cliente te suena bien, dale una estrella al &lt;a href="https://github.com/Thegreekman76/fitz-liveviews" rel="noopener noreferrer"&gt;repo&lt;/a&gt;. Lo próximo: una app real.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>rust</category>
      <category>opensource</category>
      <category>frontend</category>
    </item>
    <item>
      <title>A native frontend for a compiled language: .fitzv components that compile to WebAssembly</title>
      <dc:creator>Martin Palopoli</dc:creator>
      <pubDate>Tue, 11 Aug 2026 09:47:12 +0000</pubDate>
      <link>https://dev.to/martin_palopoli/a-native-frontend-for-a-compiled-language-fitzv-components-that-compile-to-webassembly-5cfn</link>
      <guid>https://dev.to/martin_palopoli/a-native-frontend-for-a-compiled-language-fitzv-components-that-compile-to-webassembly-5cfn</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Part 14 of the &lt;strong&gt;Fitz series&lt;/strong&gt;. Part 13 showed the dev loop for &lt;code&gt;.fitzv&lt;/code&gt; → WASM; this part is what a &lt;code&gt;.fitzv&lt;/code&gt; component actually is, and how a language that emits native binaries also emits a frontend — to WebAssembly, without adopting a JavaScript framework.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The problem: a server language needs a frontend, without importing an ecosystem
&lt;/h2&gt;

&lt;p&gt;Fitz compiles to a native binary and has HTTP/async/Postgres/JWT in the syntax. But a backend eventually needs a UI, and the usual answer is: bolt on the JavaScript ecosystem — a framework (React/Vue/Svelte), a bundler, a &lt;code&gt;node_modules&lt;/code&gt;, a second toolchain, and a second definition of every type that crosses the wire.&lt;/p&gt;

&lt;p&gt;Fitz's answer is a component format the same compiler understands, that compiles &lt;strong&gt;to WebAssembly&lt;/strong&gt; — no JS framework, no npm.&lt;/p&gt;

&lt;h2&gt;
  
  
  A component
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;.fitzv&lt;/code&gt; is a single file: state, events, a &lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt;, optional scoped styles — the Vue/Svelte shape, but it's Fitz all the way down:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;component Counter {
  state {
    count: Int = 0
  }

  event inc() { count = count + 1 }
  event dec() { count = count - 1 }

  &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"counter"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"dec"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;-&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"n"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{count}&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"inc"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;+&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;style&lt;/span&gt; &lt;span class="na"&gt;scoped&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nc"&gt;.counter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;12px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nc"&gt;.n&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;font-variant-numeric&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tabular-nums&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;style&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;{count}&lt;/code&gt; interpolates state. &lt;code&gt;@click&lt;/code&gt; wires an event. &lt;code&gt;&amp;lt;style scoped&amp;gt;&lt;/code&gt; is scoped to this component (via a hash on the class names). Build it:&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; web &lt;span class="nt"&gt;--target&lt;/span&gt; wasm-client   &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;That's the whole toolchain. No &lt;code&gt;package.json&lt;/code&gt;, no bundler config.&lt;/p&gt;

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

&lt;p&gt;There's no framework runtime in the bundle. The emitter lowers the component directly to Rust over &lt;code&gt;wasm-bindgen&lt;/code&gt; + &lt;code&gt;web-sys&lt;/code&gt;: a struct with the state fields, a &lt;code&gt;mount()&lt;/code&gt; that builds the DOM with &lt;code&gt;create_element&lt;/code&gt;/&lt;code&gt;append_child&lt;/code&gt;, event closures that mutate state, and a &lt;code&gt;render()&lt;/code&gt; that re-paints the component's subtree on a state change. It's a self-contained WASM module — the counter above is &lt;strong&gt;26 KB raw / 11.4 KB gzipped&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The template is a real DSL: interpolation (&lt;code&gt;{user.name.upper()}&lt;/code&gt; — arbitrary Fitz expressions over state), directives (&lt;code&gt;{#if}&lt;/code&gt; / &lt;code&gt;{#for}&lt;/code&gt;), events (&lt;code&gt;@click&lt;/code&gt; / &lt;code&gt;@input&lt;/code&gt;), and composition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight svelte"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;template&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
    &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;todo&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;todos&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;TodoRow&lt;/span&gt; &lt;span class="na"&gt;label=&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;remove=&lt;/span&gt;&lt;span class="s"&gt;"drop"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;&amp;lt;Child prop="v" /&amp;gt;&lt;/code&gt; passes props down; &lt;code&gt;@remove="drop"&lt;/code&gt; bubbles an event up with its payload; &lt;code&gt;&amp;lt;slot&amp;gt;&lt;/code&gt; lets a parent inject content. Composition works across files, so a component library is just imports.&lt;/p&gt;

&lt;h2&gt;
  
  
  The type is the same type
&lt;/h2&gt;

&lt;p&gt;This is the part a bolted-on framework can't give you. The &lt;code&gt;type&lt;/code&gt; your server defines is the exact one your component uses — imported from the same &lt;code&gt;.fitz&lt;/code&gt; module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// models.fitz
type Todo {
  id: Int
  title: Str
  done: Bool
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;App&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fitzv&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Todo&lt;/span&gt;
&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="n"&gt;a&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;Todo&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;template&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server compiles &lt;code&gt;Todo&lt;/code&gt; into a native struct; the component compiles it into a WASM struct; there's &lt;strong&gt;one definition&lt;/strong&gt;. No &lt;code&gt;types.ts&lt;/code&gt; that drifts from the backend, no codegen step to keep them in sync — by construction they can't diverge.&lt;/p&gt;

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

&lt;p&gt;Component models aren't new — the point is what compiles them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;React / Vue / Svelte&lt;/strong&gt; are excellent, but they're JavaScript: a framework runtime shipped to the browser, a bundler, an &lt;code&gt;npm&lt;/code&gt; tree, and a separate type story for the API boundary. &lt;code&gt;.fitzv&lt;/code&gt; compiles to WASM with none of that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Elm&lt;/strong&gt; is a lovely own-language frontend, but it compiles to JavaScript and lives only in the browser — it isn't the same language as your compiled backend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rust WASM frameworks&lt;/strong&gt; (Dioxus, Leptos) compile to WASM too, but you're adopting a framework, its macro DSL, and its toolchain on top of Rust. In Fitz the component &lt;em&gt;is&lt;/em&gt; the language, and the same &lt;code&gt;fitz&lt;/code&gt; binary builds it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One language, one compiler, one type definition — emitting a native binary for the server and a WASM bundle for the browser.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Re-render is &lt;strong&gt;naive&lt;/strong&gt;: a state change re-paints the component's subtree, not a fine-grained signal graph. It's fast enough for the common case; fine-grained reactivity is a future slice.&lt;/li&gt;
&lt;li&gt;The template supports a growing &lt;strong&gt;envelope&lt;/strong&gt; of constructs (interpolation, &lt;code&gt;{#if}&lt;/code&gt;/&lt;code&gt;{#for}&lt;/code&gt;, events, composition, slots, scoped styles); genuinely exotic patterns fall outside it for now and the compiler tells you where.&lt;/li&gt;
&lt;li&gt;The client is &lt;strong&gt;WASM-first&lt;/strong&gt;; there's no JS-vanilla target — a deliberate choice recorded when the counter came in under the 40 KB bundle gate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the frontend as a first-class part of the language: components you write in Fitz, compiled to WebAssembly, sharing types with a backend compiled to native code. Next up: how a function on that backend becomes callable from the component as if it were local — &lt;code&gt;@rpc&lt;/code&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Fitz is a compiled language with gradual typing and HTTP/async/DB as first-class citizens, compiling to native binary via Rust. Its frontend is &lt;code&gt;.fitzv&lt;/code&gt; single-file components compiled to WebAssembly — no framework, no npm. Open source.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>opensource</category>
      <category>rust</category>
      <category>webassembly</category>
    </item>
    <item>
      <title>Un frontend nativo para un lenguaje compilado: componentes .fitzv que compilan a WebAssembly</title>
      <dc:creator>Martin Palopoli</dc:creator>
      <pubDate>Tue, 11 Aug 2026 09:46:26 +0000</pubDate>
      <link>https://dev.to/martin_palopoli/un-frontend-nativo-para-un-lenguaje-compilado-componentes-fitzv-que-compilan-a-webassembly-1e3e</link>
      <guid>https://dev.to/martin_palopoli/un-frontend-nativo-para-un-lenguaje-compilado-componentes-fitzv-que-compilan-a-webassembly-1e3e</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Parte 14 de la &lt;strong&gt;serie Fitz&lt;/strong&gt;. La Parte 13 mostró el dev loop para &lt;code&gt;.fitzv&lt;/code&gt; → WASM; esta parte es qué es realmente un componente &lt;code&gt;.fitzv&lt;/code&gt;, y cómo un lenguaje que emite binarios nativos también emite un frontend — a WebAssembly, sin adoptar un framework de JavaScript.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  El problema: un lenguaje de servidor necesita frontend, sin importar un ecosistema
&lt;/h2&gt;

&lt;p&gt;Fitz compila a binario nativo y tiene HTTP/async/Postgres/JWT en la sintaxis. Pero un backend tarde o temprano necesita una UI, y la respuesta habitual es: pegar el ecosistema JavaScript — un framework (React/Vue/Svelte), un bundler, un &lt;code&gt;node_modules&lt;/code&gt;, un segundo toolchain, y una segunda definición de cada tipo que cruza el cable.&lt;/p&gt;

&lt;p&gt;La respuesta de Fitz es un formato de componentes que el mismo compilador entiende, que compila &lt;strong&gt;a WebAssembly&lt;/strong&gt; — sin framework JS, sin npm.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un componente
&lt;/h2&gt;

&lt;p&gt;Un &lt;code&gt;.fitzv&lt;/code&gt; es un solo archivo: state, events, un &lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt;, estilos scoped opcionales — la forma Vue/Svelte, pero es Fitz de punta a punta:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;component Counter {
  state {
    count: Int = 0
  }

  event inc() { count = count + 1 }
  event dec() { count = count - 1 }

  &lt;span class="nt"&gt;&amp;lt;template&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"counter"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"dec"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;-&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"n"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{count}&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"inc"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;+&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;style &lt;/span&gt;&lt;span class="na"&gt;scoped&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nc"&gt;.counter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;12px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nc"&gt;.n&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;font-variant-numeric&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tabular-nums&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;{count}&lt;/code&gt; interpola el state. &lt;code&gt;@click&lt;/code&gt; cablea un evento. &lt;code&gt;&amp;lt;style scoped&amp;gt;&lt;/code&gt; queda scopeado a este componente (vía un hash en los nombres de clase). Buildealo:&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; web &lt;span class="nt"&gt;--target&lt;/span&gt; wasm-client   &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;Ese es el toolchain entero. Sin &lt;code&gt;package.json&lt;/code&gt;, sin config de bundler.&lt;/p&gt;

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

&lt;p&gt;No hay runtime de framework en el bundle. El emisor baja el componente directo a Rust sobre &lt;code&gt;wasm-bindgen&lt;/code&gt; + &lt;code&gt;web-sys&lt;/code&gt;: un struct con los campos de state, un &lt;code&gt;mount()&lt;/code&gt; que construye el DOM con &lt;code&gt;create_element&lt;/code&gt;/&lt;code&gt;append_child&lt;/code&gt;, closures de evento que mutan el state, y un &lt;code&gt;render()&lt;/code&gt; que re-pinta el subtree del componente ante un cambio de state. Es un módulo WASM autocontenido — el contador de arriba pesa &lt;strong&gt;26 KB raw / 11.4 KB gzipped&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;El template es un DSL de verdad: interpolación (&lt;code&gt;{user.name.upper()}&lt;/code&gt; — expresiones Fitz arbitrarias sobre el state), directivas (&lt;code&gt;{#if}&lt;/code&gt; / &lt;code&gt;{#for}&lt;/code&gt;), eventos (&lt;code&gt;@click&lt;/code&gt; / &lt;code&gt;@input&lt;/code&gt;), y composición:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;template&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
    {#for todo in todos}
      &lt;span class="nt"&gt;&amp;lt;TodoRow&lt;/span&gt; &lt;span class="na"&gt;label=&lt;/span&gt;&lt;span class="s"&gt;"{todo.title}"&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;remove=&lt;/span&gt;&lt;span class="s"&gt;"drop"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    {/for}
  &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;&amp;lt;Child prop="v" /&amp;gt;&lt;/code&gt; pasa props hacia abajo; &lt;code&gt;@remove="drop"&lt;/code&gt; burbujea un evento hacia arriba con su payload; &lt;code&gt;&amp;lt;slot&amp;gt;&lt;/code&gt; deja que un padre inyecte contenido. La composición funciona entre archivos, así que una librería de componentes es solo imports.&lt;/p&gt;

&lt;h2&gt;
  
  
  El tipo es el mismo tipo
&lt;/h2&gt;

&lt;p&gt;Esta es la parte que un framework pegado por encima no te puede dar. El &lt;code&gt;type&lt;/code&gt; que define tu server es exactamente el que usa tu componente — importado del mismo módulo &lt;code&gt;.fitz&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// models.fitz
type Todo {
  id: Int
  title: Str
  done: Bool
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// App.fitzv
from models import Todo
// ... un List&amp;lt;Todo&amp;gt; en el state, {todo.title} en el template
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El server compila &lt;code&gt;Todo&lt;/code&gt; a un struct nativo; el componente lo compila a un struct WASM; hay &lt;strong&gt;una sola definición&lt;/strong&gt;. Sin un &lt;code&gt;types.ts&lt;/code&gt; que se desincroniza del backend, sin un paso de codegen para mantenerlos en sync — por construcción no pueden divergir.&lt;/p&gt;

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

&lt;p&gt;Los modelos de componentes no son nuevos — la cuestión es qué los compila:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;React / Vue / Svelte&lt;/strong&gt; son excelentes, pero son JavaScript: un runtime de framework mandado al browser, un bundler, un árbol &lt;code&gt;npm&lt;/code&gt;, y una historia de tipos aparte para el borde de la API. &lt;code&gt;.fitzv&lt;/code&gt; compila a WASM sin nada de eso.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Elm&lt;/strong&gt; es un hermoso frontend de lenguaje propio, pero compila a JavaScript y vive solo en el browser — no es el mismo lenguaje que tu backend compilado.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frameworks WASM de Rust&lt;/strong&gt; (Dioxus, Leptos) compilan a WASM también, pero estás adoptando un framework, su DSL de macros, y su toolchain sobre Rust. En Fitz el componente &lt;em&gt;es&lt;/em&gt; el lenguaje, y el mismo binario &lt;code&gt;fitz&lt;/code&gt; lo buildea.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Un lenguaje, un compilador, una definición de tipo — emitiendo un binario nativo para el server y un bundle WASM para el browser.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;El re-render es &lt;strong&gt;naive&lt;/strong&gt;: un cambio de state re-pinta el subtree del componente, no un grafo de señales fine-grained. Alcanza para el caso común; la reactividad fine-grained es un slice futuro.&lt;/li&gt;
&lt;li&gt;El template soporta un &lt;strong&gt;envelope&lt;/strong&gt; creciente de constructos (interpolación, &lt;code&gt;{#if}&lt;/code&gt;/&lt;code&gt;{#for}&lt;/code&gt;, eventos, composición, slots, estilos scoped); los patrones genuinamente exóticos quedan afuera por ahora y el compilador te dice dónde.&lt;/li&gt;
&lt;li&gt;El cliente es &lt;strong&gt;WASM-first&lt;/strong&gt;; no hay target JS-vanilla — una decisión deliberada registrada cuando el contador entró bajo el gate de bundle de 40 KB.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ese es el frontend como parte de primera clase del lenguaje: componentes que escribís en Fitz, compilados a WebAssembly, compartiendo tipos con un backend compilado a código nativo. Lo que sigue: cómo una función de ese backend se vuelve llamable desde el componente como si fuera local — &lt;code&gt;@rpc&lt;/code&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Fitz es un lenguaje compilado con tipado gradual, HTTP/async/DB como ciudadanos de primera clase, que compila a binario nativo vía Rust. Su frontend son componentes single-file &lt;code&gt;.fitzv&lt;/code&gt; compilados a WebAssembly — sin framework, sin npm. Código abierto.&lt;/em&gt;&lt;/p&gt;

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