<?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: Ali</title>
    <description>The latest articles on DEV Community by Ali (@aelmufti).</description>
    <link>https://dev.to/aelmufti</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3936579%2Fc2c24978-0cee-4131-8ee7-2b8f9ea628ef.jpeg</url>
      <title>DEV Community: Ali</title>
      <link>https://dev.to/aelmufti</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aelmufti"/>
    <language>en</language>
    <item>
      <title>How I built aelm.dev: neo-brutalism, react-snap, and a GEO-first stack</title>
      <dc:creator>Ali</dc:creator>
      <pubDate>Fri, 15 May 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/aelmufti/how-i-built-aelmdev-neo-brutalism-react-snap-and-a-geo-first-stack-bog</link>
      <guid>https://dev.to/aelmufti/how-i-built-aelmdev-neo-brutalism-react-snap-and-a-geo-first-stack-bog</guid>
      <description>&lt;p&gt;This site is a Vite + React + TypeScript single-page app. No Next.js, no Astro, no server. I prerender each route to static HTML at build time with &lt;code&gt;react-snap&lt;/code&gt;, deploy the resulting flat files to Firebase Hosting, and call it done. The whole thing weighs less than 300 KB on first paint and consistently scores 100/100/100/100 in Lighthouse on mobile.&lt;/p&gt;

&lt;p&gt;I am writing this down because I keep seeing the same advice — "just use Next.js" — repeated as if it were the only path to good SEO. That's not true. A Vite SPA can rank perfectly well, and once you add a few GEO-specific affordances it can also get cited by ChatGPT, Perplexity, Claude, and Gemini. Here is what actually mattered.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I didn't pick Next.js
&lt;/h2&gt;

&lt;p&gt;Next.js is the right call for content-heavy sites with frequent rebuilds, dynamic OG images, edge logic, or commerce flows. My portfolio is none of those things. It is a single page that changes maybe twice a month. SSR buys me nothing here, ISR even less. I would be paying a complexity tax in exchange for capabilities I will never use.&lt;/p&gt;

&lt;p&gt;Astro was the second contender. I like Astro a lot. It would have worked. But the existing site was already React + Tailwind, and rewriting working code is a great way to introduce regressions for zero user benefit. The honest decision was: keep the stack, fix the SEO holes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerendering with react-snap
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;react-snap&lt;/code&gt; is a Puppeteer-based prerenderer. As a postbuild step it spawns a headless Chrome, navigates to every discoverable route, waits for the React tree to settle, then writes the resulting HTML back to disk. Crawlers — Googlebot, GPTBot, PerplexityBot, ClaudeBot — see fully populated DOM, including any JSON-LD I injected via &lt;code&gt;useEffect&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The trap with prerendering is animations. Anything running on a&lt;code&gt;requestAnimationFrame&lt;/code&gt; loop will burn CPU during the Puppeteer run and bloat the prerendered HTML with mid-frame state. My ASCII plasma background guards against that by reading&lt;code&gt;window. __PRERENDER_INJECTED__&lt;/code&gt; — when react-snap is the renderer, it short-circuits before allocating any state. The user sees a static, empty &lt;code&gt;&amp;lt;pre&amp;gt;&lt;/code&gt; in the prerendered HTML, then the live animation hydrates in the browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  The GEO layer
&lt;/h2&gt;

&lt;p&gt;Generative Engine Optimization is the practice of getting cited in LLM answers, not just in the SERPs. It overlaps with SEO but the priorities differ. Three things did most of the work for me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A rich Person JSON-LD block&lt;/strong&gt; , with &lt;code&gt;knowsAbout&lt;/code&gt;, &lt;code&gt;sameAs&lt;/code&gt;, &lt;code&gt;alumniOf&lt;/code&gt;, and a per-language &lt;code&gt;name&lt;/code&gt; variant. LLMs love structured data. They cite it when summarizing a person or service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An &lt;code&gt;llms.txt&lt;/code&gt; and &lt;code&gt;llms-full.txt&lt;/code&gt; at the root.&lt;/strong&gt; Short manifest at &lt;code&gt;/llms.txt&lt;/code&gt; with links to the long-form content at &lt;code&gt;/llms-full.txt&lt;/code&gt;. The latter is plain Markdown with engineering opinions, project deep-dives, and honest failures. LLMs ingest it verbatim during their crawl.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explicit AI crawler allow rules in &lt;code&gt;robots.txt&lt;/code&gt;.&lt;/strong&gt; GPTBot, ClaudeBot, PerplexityBot, Google-Extended, CCBot — each one named, each one allowed. Default-deny is the wrong choice if you want to be cited.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Structured data: what I shipped
&lt;/h2&gt;

&lt;p&gt;Five JSON-LD blocks are emitted in &lt;code&gt;index.html&lt;/code&gt; at build time, plus one more injected at runtime when the recommendations section has data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Person&lt;/code&gt; with &lt;code&gt;@id&lt;/code&gt; anchored at&lt;code&gt;#person&lt;/code&gt; so other blocks can reference it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ProfilePage&lt;/code&gt; with &lt;code&gt;mainEntity&lt;/code&gt; pointing to the Person.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;WebSite&lt;/code&gt; with the canonical URL.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ProfessionalService&lt;/code&gt; + &lt;code&gt;Service&lt;/code&gt; for freelance discoverability in local search.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;FAQPage&lt;/code&gt; with the homepage FAQ, transcribed in all three languages so AI engines can quote the question/answer pairs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also emit a &lt;code&gt;SoftwareApplication ItemList&lt;/code&gt; for my personal projects. The schema community calls this "underused"; anecdotally it is the block that AI engines pick up fastest when asked about my open-source work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hreflang and three-language SEO
&lt;/h2&gt;

&lt;p&gt;The site is FR, EN, and AR. They all live at the same URL — language is a client-side context switch — which is the worst possible setup for hreflang. I am aware of this. Per-language URLs (&lt;code&gt;/en/&lt;/code&gt;, &lt;code&gt;/ar/&lt;/code&gt;) are on the roadmap, but they require either Astro or a Vite plugin that supports static per-locale builds, and I have not committed to either yet. For now the hreflang block points all three locales to &lt;code&gt;/&lt;/code&gt; with an &lt;code&gt;x-default&lt;/code&gt;, which Google accepts but treats as a weaker signal.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would do differently
&lt;/h2&gt;

&lt;p&gt;Two things, both honest mistakes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;I initially had &lt;em&gt;both&lt;/em&gt; Google Tag Manager and direct gtag.js loaded. GTM is overkill for a portfolio. I ripped GTM out and kept a single &lt;code&gt;&amp;lt;script async src="…"&amp;gt;&lt;/code&gt; for GA4. INP improved by ~80 ms on mobile.&lt;/li&gt;
&lt;li&gt;I stuffed the meta keywords for too long. Modern Google ignores them entirely; modern LLMs use the page body and structured data, not meta tags. I trimmed the list down to ~12 honest terms.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The boring conclusion
&lt;/h2&gt;

&lt;p&gt;Most "SEO advice" on the modern web is framework marketing in disguise. The actual levers are still: a fast page, a clean DOM, a sitemap, structured data that matches what the page says, an &lt;code&gt;llms.txt&lt;/code&gt;, an allow-listed &lt;code&gt;robots.txt&lt;/code&gt;, and content worth citing. Pick a stack you can ship in, then attend to these one at a time. The framework matters less than people pretend.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>geo</category>
      <category>react</category>
      <category>vite</category>
    </item>
    <item>
      <title>Why I picked Ollama + LanceDB + FastAPI for the AI Book Recommender</title>
      <dc:creator>Ali</dc:creator>
      <pubDate>Sun, 10 May 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/aelmufti/why-i-picked-ollama-lancedb-fastapi-for-the-ai-book-recommender-50</link>
      <guid>https://dev.to/aelmufti/why-i-picked-ollama-lancedb-fastapi-for-the-ai-book-recommender-50</guid>
      <description>&lt;p&gt;The AI Book Recommender is a small RAG app I built over a weekend. You type a mood — "a slow-burn detective novel set in 1970s Paris" — and it returns five semantically matched books. The whole stack runs locally: Ollama for the LLM, LanceDB for the vector store, FastAPI on the server, React on the client. No OpenAI account, no managed vector DB, no per-request cost. Repo: &lt;a href="https://github.com/aelmufti/book-recommander" rel="noopener noreferrer"&gt;github.com/aelmufti/book-recommander&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The interesting part is not the code — it is small — but the choices I made and why. RAG tutorials always reach for OpenAI + Pinecone + LangChain. None of those three were the right call for this project. Here is the reasoning, with the failure modes I avoided.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Ollama instead of OpenAI
&lt;/h2&gt;

&lt;p&gt;For a public-facing product with paying users, OpenAI is usually the right answer: better quality, lower latency, no hardware to manage. For a side project where the corpus is books and the user is me, the calculus flips:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cost.&lt;/strong&gt; Free to run. I do not have to think about per-token billing or rate limits while iterating.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy.&lt;/strong&gt; Reading habits are private. I do not want them in someone else's training data, full stop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offline.&lt;/strong&gt; The whole stack works on a plane. This is not a feature I expected to use, but it is genuinely useful for demos in places with bad Wi-Fi.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tradeoff is quality. Llama3 8B is good, not great, at instruction-following compared to GPT-4o-mini. For book recommendations specifically, semantic similarity matters more than narrative reasoning, so the gap is small. If I were doing agent-style chained tool calls, I would reach for a hosted frontier model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why LanceDB instead of Qdrant
&lt;/h2&gt;

&lt;p&gt;I have shipped both. For solo projects LanceDB wins on operational simplicity:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Embedded.&lt;/strong&gt; No separate service, no Docker container, no port to expose. The vector store is a directory on disk that the Python process opens directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Arrow-native.&lt;/strong&gt; Reads are zero-copy from Parquet. Indexing 50k book descriptions is faster than I expected.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3-compatible.&lt;/strong&gt; If I ever did want to move this to a server, I could put the LanceDB directory on object storage without changing the application code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Qdrant has a better filtering DSL and a real HTTP API. If I were building a multi-tenant SaaS where dozens of services hit the same vector store, I would pick Qdrant. For a single-process app running on my laptop, the extra service is operational overhead with no offsetting benefit.&lt;/p&gt;

&lt;p&gt;I left ChromaDB out entirely. Fine for tutorials, but I have hit too many corruption issues when restarting the embedded store. I do not ship Chroma to anything I care about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why FastAPI instead of LangChain
&lt;/h2&gt;

&lt;p&gt;FastAPI is the server. LangChain is not in the stack at all. Two reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The pipeline is short.&lt;/strong&gt; Embed query, top-k retrieve, format prompt, call LLM, return. That is 30 lines of plain Python. Wrapping it in &lt;code&gt;RetrievalQA&lt;/code&gt; classes obscures the data flow without adding capability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debuggability.&lt;/strong&gt; When the model returns garbage, the first question is always "what did the prompt look like?". With plain Python that is one &lt;code&gt;print()&lt;/code&gt;. With LangChain that is an excursion through three layers of abstract base classes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I do use LangChain in some client projects — usually when the agentic flow is complex enough that LangGraph genuinely earns its keep. For a five-step retrieval pipeline it is dead weight.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of the code
&lt;/h2&gt;

&lt;p&gt;The interesting endpoint is roughly:&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="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/recommend&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;recommend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RecommendRequest&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;RecommendResponse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;q_vec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mood&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;hits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;books_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q_vec&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to_list&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mood&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ollama&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;llama3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;RecommendResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;books&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;parse_picks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Embeddings are computed once, offline, on the canonical book corpus. The query path is one embedding call (sub-50 ms locally), one LanceDB search (sub-20 ms), and one Ollama generation (~120 ms on an M1 Mac). End-to-end p95 is around 200 ms, which is plenty fast for a search-style UX.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons I will reuse
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Default to embedded vector stores&lt;/strong&gt; until you have a concrete reason to spin up a service. LanceDB and SQLite-vec are both excellent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Default to plain Python orchestration&lt;/strong&gt; until the flow is complex enough to benefit from a real graph framework. For RAG specifically that point is much later than the LangChain marketing implies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache embeddings aggressively.&lt;/strong&gt; They are expensive to compute and cheap to store. If you re-embed the same corpus on every container restart, your bill (or your patience) will reflect it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full project is open source and runs locally — clone it, set up Ollama, point it at a book corpus of your choice, and you have a working semantic recommender in twenty minutes.&lt;/p&gt;

</description>
      <category>rag</category>
      <category>ollama</category>
      <category>lancedb</category>
      <category>fastapi</category>
    </item>
    <item>
      <title>DuckDB for real-time dashboards: lessons from World Data Visualizer</title>
      <dc:creator>Ali</dc:creator>
      <pubDate>Sun, 03 May 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/aelmufti/duckdb-for-real-time-dashboards-lessons-from-world-data-visualizer-57ij</link>
      <guid>https://dev.to/aelmufti/duckdb-for-real-time-dashboards-lessons-from-world-data-visualizer-57ij</guid>
      <description>&lt;p&gt;World Data Visualizer is a real-time market intelligence dashboard I built between January and March 2026. It covers eleven economic sectors, tracks US Congress trading transactions, ingests RSS news with a small NLP pipeline, plots AIS oil-tanker positions on a world map, and computes a "Cortisol Gauge" market-stress indicator. The backend is Node.js. The database is DuckDB. There is no Postgres anywhere. Repo: &lt;a href="https://github.com/aelmufti/world-data-visualizer" rel="noopener noreferrer"&gt;github.com/aelmufti/world-data-visualizer&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The headline number is that aggregate queries over a few million rows complete in under 100 ms with no tuning. I want to explain why that is true, and where DuckDB is the wrong choice so you do not over-fit to my use case.&lt;/p&gt;

&lt;h2&gt;
  
  
  What DuckDB is, briefly
&lt;/h2&gt;

&lt;p&gt;DuckDB is an in-process OLAP database. It ships as a single library you link into your application — the same architectural shape as SQLite. There is no server. The "database" is either a file on disk or pure memory. Queries are vectorized and column-oriented, which is the opposite of a row-store like Postgres.&lt;/p&gt;

&lt;p&gt;For OLTP workloads — short transactions, many concurrent writers — Postgres remains the right answer. For OLAP workloads — long aggregate scans, few writers, many readers — DuckDB is faster by an order of magnitude on the same hardware, with one tenth the operational complexity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this project is OLAP-shaped
&lt;/h2&gt;

&lt;p&gt;The dashboard has three traffic patterns and they are all reads:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Periodic ingestion.&lt;/strong&gt; Every few minutes a worker pulls fresh data from upstream APIs (stocks, RSS feeds, Congress disclosures) and appends rows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aggregate queries.&lt;/strong&gt; The UI asks for the "sector-wide moving average over the past hour", which scans the relevant slice and aggregates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-time fan-out.&lt;/strong&gt; When new data lands, the server recomputes the affected aggregates once and pushes the result to every connected client over WebSocket.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Writes are batched and infrequent. Reads are heavy, repetitive, and aggregate-shaped. There is no concurrent transactional contention to speak of. This is exactly DuckDB's strong suit.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I gave up by skipping Postgres
&lt;/h2&gt;

&lt;p&gt;Honesty matters more than evangelism. The tradeoffs were real:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No concurrent writers.&lt;/strong&gt; DuckDB allows a single writer process. For my topology that is fine — the ingestion worker is the only writer — but on a system with multiple ingestion paths I would need to coordinate or pick differently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No row-level locking or transactional updates.&lt;/strong&gt; OLAP databases assume you mostly append. If your access pattern is "load row, mutate row, save row", DuckDB is going to be frustrating.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smaller ecosystem.&lt;/strong&gt; Fewer extensions, fewer dashboards-as-a-service know how to connect, fewer Stack Overflow questions answered. The DuckDB community is great but it is not the size of Postgres.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  WebSocket over polling
&lt;/h2&gt;

&lt;p&gt;The original prototype polled every five seconds from the browser. With ~50 connected clients during a market session that was ~10 requests per second to recompute the same aggregates. Switching to a single recompute-on-write + WebSocket fan-out reduced backend load by roughly an order of magnitude and eliminated the staleness window between source updates and UI updates.&lt;/p&gt;

&lt;p&gt;The implementation is conventional: &lt;code&gt;ws&lt;/code&gt; on the server, a thin client wrapper on the React side, and a small retry/backoff state machine for reconnects. Nothing exotic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I would draw the line
&lt;/h2&gt;

&lt;p&gt;I do not think DuckDB is universally better than Postgres. I think it is dramatically better for a specific shape of workload and people consistently overestimate how often their workload falls outside that shape. Three concrete heuristics:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;If you would happily run your dashboard off a nightly Parquet dump, DuckDB will do that in real time with the same query surface and almost no ops overhead.&lt;/li&gt;
&lt;li&gt;If your hot path is "for this user, atomically update their cart and decrement inventory", reach for Postgres without thinking.&lt;/li&gt;
&lt;li&gt;If you are running both shapes in one app, run both databases. Use DuckDB for the analytics surface and Postgres for the transactional surface. They cohabit fine.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The unsexy conclusion
&lt;/h2&gt;

&lt;p&gt;The fastest database in production is usually the one whose access patterns match the workload, not the one with the most impressive benchmarks. For a read-heavy dashboard with batch ingestion DuckDB is, in my experience, the highest leverage-to-complexity choice on the market. World Data Visualizer is open source — clone it, point it at your own upstream feeds, and you will see the same numbers.&lt;/p&gt;

</description>
      <category>duckdb</category>
      <category>websocket</category>
      <category>react</category>
      <category>node</category>
    </item>
    <item>
      <title>Building AirAlert: an honest air-quality monitor with an Arduino UNO and three sensors that disagree</title>
      <dc:creator>Ali</dc:creator>
      <pubDate>Sun, 26 Apr 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/aelmufti/building-airalert-an-honest-air-quality-monitor-with-an-arduino-uno-and-three-sensors-that-disagree-28aj</link>
      <guid>https://dev.to/aelmufti/building-airalert-an-honest-air-quality-monitor-with-an-arduino-uno-and-three-sensors-that-disagree-28aj</guid>
      <description>&lt;p&gt;AirAlert is a small indoor air-quality monitor I built on an Arduino UNO with three sensors and a strip of LEDs. Green means the air in the room is fine. Yellow means I should think about opening a window. Red means I already should have. There is an optional NodeMCU ESP8266 that can push the readings to a server over Wi-Fi, but the default mode is offline, glanceable, and stupid in the good way. Repo: &lt;a href="https://github.com/aelmufti/AirAlert" rel="noopener noreferrer"&gt;github.com/aelmufti/AirAlert&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The thing I want to write down is not the build process. It is what I learned about consumer air-quality sensors after spending a few weeks staring at their datasheets side by side. The short version: the three sensors I used do not measure the same thing, and most online tutorials gloss over that until the readings contradict each other and someone files an issue.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three sensors, and what they actually do
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;BME680 (Bosch).&lt;/strong&gt; A four-in-one MEMS sensor: temperature, relative humidity, barometric pressure, and a gas resistance reading that Bosch's BSEC library massages into an "Indoor Air Quality" index. Temperature, humidity, and pressure are trustworthy. The IAQ index is a derived, slow-moving, vendor proprietary number. It is fine for trend lines, not for absolute claims.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MQ-135.&lt;/strong&gt; A cheap tin-dioxide resistor that changes resistance when exposed to a mixed bag of gases — ammonia, NOx, alcohol, benzene, smoke, and yes, CO₂. The number printed on its datasheet under "CO₂ sensitivity" is essentially a vibe. If a sensor that costs less than a coffee could measure CO₂ to ppm accuracy, no one would buy MH-Z19. Treat MQ-135 as a coarse "something changed in the room" detector, not a CO₂ meter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MH-Z19 (Winsen).&lt;/strong&gt; A non-dispersive infrared (NDIR) CO₂ sensor. NDIR is the actual technology used in commercial CO₂ monitors. It is selective for CO₂ in a way the MQ-135 is not. It also costs maybe ten times more. When the two sensors disagree about CO₂, MH-Z19 is right.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem this caused
&lt;/h2&gt;

&lt;p&gt;My first wiring put all three sensors on the breadboard, summed their "air quality" outputs into one score, and lit the LEDs from that score. The system behaved erratically. The MQ-135 would spike on a cup of hot coffee or a passing perfume cloud and the LED would go red while CO₂ from MH-Z19 was still in the low 500 ppm range. The score was the average of "is there a chemical anomaly in the room" and "how stuffy is it" — two different questions that deserve different answers.&lt;/p&gt;

&lt;p&gt;The fix was conceptual, not electrical. I split the indicator into two layers: a primary &lt;em&gt;ventilation&lt;/em&gt; signal driven only by MH-Z19 (because the use case is "should I open a window") and a secondary &lt;em&gt;anomaly&lt;/em&gt; signal that uses MQ-135 + the BME680 gas index. The anomaly signal does not light the headline LED; it flashes a small auxiliary one. Most of the time only the ventilation signal is doing anything, which is what I actually wanted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Calibration is the boring step you cannot skip
&lt;/h2&gt;

&lt;p&gt;MH-Z19 ships with Automatic Baseline Calibration (ABC) enabled. The assumption baked into ABC is that the sensor is exposed to outdoor air (~415 ppm CO₂) at least once every 24 hours, and it re-zeros against that minimum. In a real apartment that assumption is sometimes false. If you keep the sensor in a closed bedroom permanently it will drift up because it never sees a clean baseline.&lt;/p&gt;

&lt;p&gt;Either you periodically air the room and trust ABC, or you turn ABC off and do manual zero calibration outdoors every few months. For a hobby device I left ABC on and added a comment in the firmware so future-me would know why readings might creep.&lt;/p&gt;

&lt;p&gt;MQ-135 also needs a long burn-in. The datasheet says 24 hours powered before the readings stabilize. Mine took closer to 48. Plan a power-cycle window before you start interpreting the numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I made the Wi-Fi optional
&lt;/h2&gt;

&lt;p&gt;The minimal viable monitor is three LEDs. It does not need to live on the network, it does not need an app, and it does not need to upload sensor data about my breathing to anyone. The ESP8266 uplink path is there because I wanted to graph long-term trends, but it is opt-in: leave the NodeMCU unconnected and the UNO runs the same firmware and lights the same LEDs. Choosing offline-by-default for a sensor in your bedroom is the polite design.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would change today
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Drop the MQ-135. Its readings rarely added information that the BME680 gas index did not already cover, and it muddied the calibration story.&lt;/li&gt;
&lt;li&gt;Move from an Arduino UNO to a single ESP32. The UNO + ESP8266 combo predates my familiarity with the ESP32, and a single board with Wi-Fi and Bluetooth on-chip is simpler than two boards that have to agree.&lt;/li&gt;
&lt;li&gt;Log to flash, not just to the network. If you ever want to reason about overnight CO₂ in your bedroom, having three months of local data beats whatever was on a dashboard you forgot to look at.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Hobby sensor projects fail the same way: they trust their cheapest sensor as if it were calibrated lab equipment, average unrelated signals into one number, and ship a green/yellow/red light that says everything is fine while the air is not. AirAlert is mainly a study in not doing that. Pick one signal you actually care about, route it through the sensor that actually measures it, and reserve everything else for the secondary display.&lt;/p&gt;

</description>
      <category>arduino</category>
      <category>iot</category>
      <category>embedded</category>
      <category>sensors</category>
    </item>
    <item>
      <title>Mood Tracker: one slider per day, Supabase RLS, and the discipline of not shipping features</title>
      <dc:creator>Ali</dc:creator>
      <pubDate>Sun, 19 Apr 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/aelmufti/mood-tracker-one-slider-per-day-supabase-rls-and-the-discipline-of-not-shipping-features-3fp6</link>
      <guid>https://dev.to/aelmufti/mood-tracker-one-slider-per-day-supabase-rls-and-the-discipline-of-not-shipping-features-3fp6</guid>
      <description>&lt;p&gt;Mood Tracker is the smallest useful daily-log app I could justify building. One slider, one to ten, once a day. The rest of the surface area exists to keep that one input honest: Supabase for auth and storage, Chart.js for trends, a streak counter, CSV export, dark mode, and a PWA install path so it feels like the icons next to it on a phone home screen. Repo: &lt;a href="https://github.com/aelmufti/mood-tracker" rel="noopener noreferrer"&gt;github.com/aelmufti/mood-tracker&lt;/a&gt;. Live demo: &lt;a href="https://mood-tracker-ac8d8.web.app/dashboard/" rel="noopener noreferrer"&gt;mood-tracker-ac8d8.web.app&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a single-number rating
&lt;/h2&gt;

&lt;p&gt;Multi-axis mood scales — separate sliders for energy, anxiety, focus, sleep — are more scientifically defensible. They are also the reason every mood-tracking app I have tried got abandoned by week three. Each extra input is a chance for the user to skip the day entirely. A single slider has no answer you can refuse to give.&lt;/p&gt;

&lt;p&gt;The number is imprecise, but it is consistently imprecise. Comparing today to last Tuesday is meaningful because both days went through the same one-slider funnel. That consistency is worth more than per-axis fidelity for the actual use case, which is "look back over a quarter and ask whether something was off".&lt;/p&gt;

&lt;h2&gt;
  
  
  Picking Supabase over Firebase
&lt;/h2&gt;

&lt;p&gt;Boîte à Livre, an earlier project of mine, used Firebase Realtime Database and that worked fine. For Mood Tracker I wanted a relational shape (one row per user per day, indexed by date) and proper SQL. Supabase is Postgres-as-a-service with auth and an auto-generated REST/RPC layer on top. The Postgres part is the part I actually wanted; the auth and API are bonuses.&lt;/p&gt;

&lt;p&gt;The single feature that paid for the migration was &lt;strong&gt;Row-Level Security&lt;/strong&gt;. The policy is one line per table: &lt;code&gt;using (auth.uid() = user_id) with check (auth.uid() = user_id)&lt;/code&gt;. That clause is the actual authorization boundary. The client SDK can ask Postgres for "all moods", and the database will only return the caller's rows. You cannot accidentally leak another user's data by writing the wrong filter in the client, because the database refuses to return it.&lt;/p&gt;

&lt;p&gt;Compare that to Firebase rules, which are a separate JSON DSL you maintain out-of-band from your data model. RLS lives next to the table it protects and uses the same language you query with. For a one-developer project that is a meaningful reduction in places-where-things-can-go-wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The PWA install path, and when not to nag
&lt;/h2&gt;

&lt;p&gt;PWAs can prompt their own install via the &lt;code&gt;beforeinstallprompt&lt;/code&gt; event. Most apps that catch this event immediately show a banner on the user's first visit. That is the wrong instinct. The user has not yet decided whether your app is worth a home-screen slot.&lt;/p&gt;

&lt;p&gt;Mood Tracker waits until the user has logged at least three days before surfacing the install affordance, and then it is a quiet line in the settings panel, not a modal. The conversion rate is lower than a banner would deliver, but the ones who do install are people who have actually used the thing. That feels like the honest number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Streaks: honest tool or engagement bait?
&lt;/h2&gt;

&lt;p&gt;A consecutive-days streak is one of the oldest patterns in habit apps and one of the most criticized. The criticism is usually that streaks turn a low-stakes habit into a high- stakes commitment, and that the fear of breaking a long streak becomes the reason for the entry rather than the actual introspection.&lt;/p&gt;

&lt;p&gt;I kept the streak counter, but I made two choices that I think keep it on the right side of that line:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The streak does not appear on the input screen.&lt;/strong&gt; You see it on the stats page. The act of logging your day should not be coloured by "do not break the streak".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There is no penalty for missing a day.&lt;/strong&gt; The streak resets to one, not to zero, and there is no paywall/notification/"keep your streak" pressure. It is a piece of information, not a leash.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  CSV export, because lock-in is not a feature
&lt;/h2&gt;

&lt;p&gt;The export button dumps every row the current user owns into a flat CSV. No date filters, no premium tier, no API key. If you decide you would rather use Notion or a spreadsheet, you can leave and take your data with you. This is the cheapest possible respect-the-user feature and most apps still do not do it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is missing on purpose
&lt;/h2&gt;

&lt;p&gt;No notifications, no notes field, no tags, no multi-axis ratings, no social sharing, no AI summaries. I have an opinion about each of those and the opinion is "this app is not better for adding it." A 12-second daily interaction does not need a roadmap. The discipline of not shipping the next feature is, more than anything, what kept the app usable for me over months instead of weeks.&lt;/p&gt;

</description>
      <category>react</category>
      <category>supabase</category>
      <category>pwa</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Boîte à Livre: mapping free neighborhood libraries with Leaflet, OpenStreetMap, and Firebase</title>
      <dc:creator>Ali</dc:creator>
      <pubDate>Sun, 12 Apr 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/aelmufti/boite-a-livre-mapping-free-neighborhood-libraries-with-leaflet-openstreetmap-and-firebase-37a9</link>
      <guid>https://dev.to/aelmufti/boite-a-livre-mapping-free-neighborhood-libraries-with-leaflet-openstreetmap-and-firebase-37a9</guid>
      <description>&lt;p&gt;Boîte à Livre is a community map for &lt;em&gt;boîtes à livres&lt;/em&gt;: the small wooden cabinets people set up outside their houses or in parks in France, full of secondhand books anyone can take or leave. They exist everywhere in the country and there is no single registry. This app is a registry: pan the map, see boxes near you, tap one to see what other users have noted about it, add a new one if you spot it on a walk. Repo: &lt;a href="https://github.com/aelmufti/boitealivre" rel="noopener noreferrer"&gt;github.com/aelmufti/boitealivre&lt;/a&gt;. Live: &lt;a href="https://boitesauxlivres.web.app/" rel="noopener noreferrer"&gt;boitesauxlivres.web.app&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Leaflet, not Mapbox or Google Maps
&lt;/h2&gt;

&lt;p&gt;For a hobby project with no budget and no business model, the cost structure of commercial map SDKs is the wrong shape. Mapbox and Google both have generous free tiers, but the tiers are sized for traffic patterns I do not want to think about, and a single viral moment on Reddit can leave you holding a bill.&lt;/p&gt;

&lt;p&gt;Leaflet is a few-kilobyte open-source map renderer that talks to any tile server. Point it at the public OpenStreetMap tile servers and you have a working map with no account, no key, no quota dashboard. The features I gave up — vector tiles, smooth 3D pitching, turn-by-turn — are not features anyone needs to find a book box.&lt;/p&gt;

&lt;p&gt;For production traffic, the right move is to host your own tile server (or use a service like Stadia / MapTiler) rather than hammer the OSM public tiles, which are meant for development. I am within the polite limits for now; if usage grew I would swap in a self-hosted tile pipeline before the Open Street Map foundation rate-limited me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Firebase Realtime Database is fine here
&lt;/h2&gt;

&lt;p&gt;Firebase Realtime Database (RTDB) is the older of the two Firebase databases. Most new projects reach for Firestore instead, and that is usually correct. RTDB has earned its reputation for awkward queries.&lt;/p&gt;

&lt;p&gt;For this app though, the data shape is one flat tree of points, each with coordinates, a name, and an array of user notes. Total dataset size is small (low thousands of documents). Reads are dominated by a single query — "everything in this bounding box" — and that query is cheap even if you just download the whole tree on first load and filter client-side, which is what Boîte à Livre does.&lt;/p&gt;

&lt;p&gt;RTDB's strengths are exactly fitted to that pattern: a single document subscription, instant push when a new box is added, no schema migration to do when the data shape evolves. If the dataset grew by an order of magnitude I would move to Firestore (or Postgres + PostGIS, frankly). At the current scale RTDB is the smaller solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  The moderation problem
&lt;/h2&gt;

&lt;p&gt;Any "anyone can add a pin to a map" app eventually meets the same three failure modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Spam pins.&lt;/strong&gt; Someone drops a pin in the middle of the ocean labelled "test", and never comes back to delete it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Misplaced pins.&lt;/strong&gt; Honest user, wrong address autocomplete, pin lands one street over.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pins for things that are not boxes.&lt;/strong&gt; The local bookstore, a personal bookshelf, an obsolete pile of magazines.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I did not solve moderation, and I want to be honest about that. What I did was small:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pins added by a brand-new account are flagged as unverified, and rendered with a different icon, until at least one other user confirms them.&lt;/li&gt;
&lt;li&gt;Every pin can be reported by any user. A small number of reports moves the pin to a hidden queue.&lt;/li&gt;
&lt;li&gt;I review that queue manually. For a project at this scale it is a few entries a month — totally manageable as a solo project, totally unsustainable if traffic 100×'d.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The honest answer to "how does this scale to a national registry" is "it does not, without a real moderation pipeline". Wikipedia-style trust graphs, neighbourhood moderators, machine-vision validation of submitted photos — any of those is a real piece of work and none of them are cheap.&lt;/p&gt;

&lt;h2&gt;
  
  
  The PWA story is simpler than I expected
&lt;/h2&gt;

&lt;p&gt;The app installs to a phone like a native app. The whole offline path is: service worker caches the JS bundle, IndexedDB caches the last-seen pin set, and the map degrades to "your last sync" when offline. None of that needed a framework — it is a couple of hundred lines of plain service-worker code.&lt;/p&gt;

&lt;p&gt;The single nuance is that Leaflet tiles do not cache cleanly in a service worker without help, because the tile URLs are infinite (every zoom × every coordinate pair). The pragmatic fix is to cache the tiles in the area the user has actually looked at, with a small LRU. Anything more clever than that is over-engineering for the actual usage.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this project was really for
&lt;/h2&gt;

&lt;p&gt;Honestly, the project is mostly an excuse. The product is a directory of book boxes. The reason I built it is that the boîte à livre near my place changes its contents every couple of days, I wanted to share that habit with friends, and an app was a more durable way to do that than a group chat. The engineering choices in this note all flow from that scale: a few thousand boxes, a few hundred users, a single maintainer. Picking tools that match that scale, instead of the scale you wish you had, is most of what makes a hobby project shippable.&lt;/p&gt;

</description>
      <category>leaflet</category>
      <category>openstreetmap</category>
      <category>firebase</category>
      <category>pwa</category>
    </item>
  </channel>
</rss>
