<?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</title>
    <description>The most recent home feed on DEV Community.</description>
    <link>https://dev.to</link>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed"/>
    <language>en</language>
    <item>
      <title>The Hidden Job Market: Why Thousands of Open Roles Never Show Up in Your Job Search</title>
      <dc:creator>Rohit Singh</dc:creator>
      <pubDate>Fri, 07 Aug 2026 13:02:37 +0000</pubDate>
      <link>https://dev.to/enigma52/the-hidden-job-market-why-thousands-of-open-roles-never-show-up-in-your-job-search-134p</link>
      <guid>https://dev.to/enigma52/the-hidden-job-market-why-thousands-of-open-roles-never-show-up-in-your-job-search-134p</guid>
      <description>&lt;p&gt;When people search for jobs today, they usually start with Google, LinkedIn, Indeed, or another large job board. There is an assumption that these platforms provide a reasonably complete picture of what companies are hiring for. In reality, that assumption is far from true. A significant number of open positions never become visible through these channels, even though they are publicly accessible on the internet.&lt;/p&gt;

&lt;p&gt;Over the last few months, while building &lt;strong&gt;WorkWay&lt;/strong&gt;, I spent a lot of time looking at how companies publish jobs and how those jobs are discovered. One pattern became increasingly obvious. Modern companies rarely manage job listings themselves. Instead, they rely on Applicant Tracking Systems (ATS) such as Greenhouse, Ashby, Lever, Workable, SmartRecruiters, Teamtailor, and several others. These platforms are the source of truth for a company's hiring process, yet many of the jobs hosted on them remain surprisingly difficult to discover unless you already know where to look.&lt;/p&gt;

&lt;p&gt;This hidden layer of the job market is much larger than most people realize.&lt;/p&gt;

&lt;h2&gt;
  
  
  How companies publish jobs today
&lt;/h2&gt;

&lt;p&gt;Ten years ago it was common for companies to build a careers section directly into their own website. Today, especially in the startup ecosystem, that is no longer the norm. Instead, companies create a careers page that simply links to an external ATS. Once that happens, every job posting, application, and hiring workflow lives inside that platform.&lt;/p&gt;

&lt;p&gt;If you have applied to startups before, you have probably seen URLs like these:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;boards.greenhouse.io/company
company.ashbyhq.com
jobs.lever.co/company
company.teamtailor.com/jobs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These pages are where recruiters actually create and manage job postings. They contain structured information about the role, location, department, employment type, and application process. For the company, this makes hiring much easier because the ATS handles candidate tracking, interview scheduling, permissions, and reporting.&lt;/p&gt;

&lt;p&gt;From a discovery perspective, however, it introduces a new problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Public does not always mean discoverable
&lt;/h2&gt;

&lt;p&gt;One of the biggest misconceptions about the web is that if a page is public, search engines and aggregators will automatically find it. In practice, discovery depends on many different signals.&lt;/p&gt;

&lt;p&gt;Search engines need to know that a page exists before they can crawl it. They rely on internal links, external backlinks, XML sitemaps, structured metadata, crawl budgets, canonical URLs, and many other factors. If an ATS page is weakly linked from the company's website or buried several clicks deep, it may receive very little attention from crawlers.&lt;/p&gt;

&lt;p&gt;Many startup companies launch careers pages with only a single navigation link pointing toward their ATS. Some do not include those pages in their sitemap. Others redesign their website and accidentally remove internal links entirely. The ATS page continues to exist and accepts applications, but it becomes much harder for search engines and third-party job aggregators to discover.&lt;/p&gt;

&lt;p&gt;Being publicly accessible is not the same thing as being easily discoverable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why job aggregators miss these opportunities
&lt;/h2&gt;

&lt;p&gt;People often assume that job boards simply collect every available opening from every company. The reality is much more complicated.&lt;/p&gt;

&lt;p&gt;Every ATS has its own structure, APIs, pagination, metadata, and update frequency. Some expose public APIs, while others require crawling HTML pages. Companies migrate between ATS providers, change career page URLs, archive old postings, duplicate listings across regions, and update jobs throughout the day.&lt;/p&gt;

&lt;p&gt;An aggregator cannot simply crawl a fixed list of websites and expect to stay current. It needs to continuously discover new companies, detect changes, remove expired jobs, handle duplicates, and revisit existing pages frequently enough that short-lived openings are not missed.&lt;/p&gt;

&lt;p&gt;This becomes especially challenging for startups. Many startups hire only occasionally, which means their careers page may remain unchanged for months before suddenly publishing ten new engineering positions. Unless an indexer revisits that page at the right time, those openings can remain invisible for days or even weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Startup hiring moves much faster than people expect
&lt;/h2&gt;

&lt;p&gt;One observation that surprised me while exploring hiring data was how quickly startup recruiting changes.&lt;/p&gt;

&lt;p&gt;Large enterprises often keep positions open for months, but startups tend to move much faster. A company might announce a funding round, open fifteen engineering roles over the course of a week, receive hundreds of applications, and close those positions shortly afterward.&lt;/p&gt;

&lt;p&gt;For a job seeker, timing matters almost as much as qualifications. Applying within the first day or two often means your application reaches recruiters while they are actively building the interview pipeline. Applying several weeks later may mean the position is already nearing completion even if the listing has not yet been removed.&lt;/p&gt;

&lt;p&gt;Missing those early opportunities simply because they were difficult to discover feels like an unnecessary disadvantage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Discovering the problem while building WorkWay
&lt;/h2&gt;

&lt;p&gt;This issue became apparent while working on WorkWay. Initially, the goal was straightforward: create a better search experience for software engineering jobs. The assumption was that collecting job listings would be relatively simple because every company already publishes them online.&lt;/p&gt;

&lt;p&gt;That assumption disappeared quickly.&lt;/p&gt;

&lt;p&gt;Some companies exposed structured APIs that were easy to consume. Others relied entirely on HTML pages. Some ATS platforms embedded metadata in JavaScript, while others generated pages dynamically. Companies frequently changed domains, migrated between ATS providers, or reorganized their careers pages without redirects. It became clear that finding jobs was not the difficult part. The difficult part was reliably discovering every place where jobs could appear.&lt;/p&gt;

&lt;p&gt;Over time, the project evolved from indexing company websites to understanding the ecosystem of ATS platforms themselves. Instead of treating each company as an isolated website, it became more useful to think of hiring infrastructure as a network that constantly changes as companies grow, migrate, and expand.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hidden job market is larger than it looks
&lt;/h2&gt;

&lt;p&gt;When people talk about hidden jobs, they often mean positions that are filled through referrals or networking before being publicly advertised. That certainly exists, but there is another type of hidden job that receives much less attention.&lt;/p&gt;

&lt;p&gt;These are jobs that are completely public, accept applications from anyone, and are hosted on accessible webpages. They simply never become visible to many of the tools that job seekers rely on every day.&lt;/p&gt;

&lt;p&gt;As more companies adopt specialized hiring platforms, this problem will only become more pronounced. Every new ATS, every custom careers portal, and every hiring workflow creates another place where opportunities can exist without being easily discoverable. From an engineering perspective, this is a fascinating indexing challenge. From a job seeker's perspective, it can mean missing out on great opportunities simply because they were never surfaced.&lt;/p&gt;

&lt;p&gt;This challenge is exactly what motivated me to build &lt;strong&gt;WorkWay&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;WorkWay continuously indexes jobs across popular Applicant Tracking Systems like Greenhouse, Ashby, Lever, Workable, SmartRecruiters, Teamtailor, and others, with the goal of making these opportunities easier to discover in one place. Instead of requiring you to visit hundreds of individual company career pages every day, WorkWay keeps track of new openings as they appear and removes listings when they are no longer active.&lt;/p&gt;

&lt;p&gt;If you're actively looking for software engineering roles, startup opportunities, or simply want a faster way to discover jobs that are often missed by traditional search, you can explore the platform at &lt;strong&gt;&lt;a href="https://www.workway.dev" rel="noopener noreferrer"&gt;https://www.workway.dev&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I'm also continuously improving the crawler and adding support for additional ATS providers, better search, company pages, and fresher indexing. If you have feedback, notice missing jobs, or have ideas for features that would make job discovery easier, I'd genuinely love to hear from you. Every suggestion helps improve the platform and makes it more useful for everyone searching for their next opportunity.&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>career</category>
      <category>startup</category>
    </item>
    <item>
      <title>Constitutional AI: os princípios éticos que tornam o Claude único</title>
      <dc:creator>André Dias Moreira Prol</dc:creator>
      <pubDate>Fri, 07 Aug 2026 13:01:40 +0000</pubDate>
      <link>https://dev.to/andr_diasmoreiraprol_b/constitutional-ai-os-principios-eticos-que-tornam-o-claude-unico-1nfe</link>
      <guid>https://dev.to/andr_diasmoreiraprol_b/constitutional-ai-os-principios-eticos-que-tornam-o-claude-unico-1nfe</guid>
      <description>&lt;p&gt;Quando avalio arquiteturas de IA para projetos de tokenização e perícia digital, percebo que a maioria dos debates ignora uma pergunta fundamental: como um modelo aprende o que é certo? Ao longo de duas décadas trabalhando com tecnologia, poucas inovações me chamaram tanta atenção quanto a abordagem da Anthropic para treinar o Claude — não por marketing, mas por engenharia ética real. Deixe-me explicar por que isso importa, especialmente para quem opera em setores regulados como o brasileiro.&lt;/p&gt;

&lt;h2&gt;
  
  
  O que é Constitutional AI e por que ela muda o jogo
&lt;/h2&gt;

&lt;p&gt;A maioria dos grandes modelos — GPT, Gemini, Llama — depende fortemente de RLHF (Reinforcement Learning from Human Feedback). Milhares de anotadores humanos julgam respostas como boas ou ruins, e o modelo ajusta seu comportamento. O problema? Esse processo herda vieses inconsistentes, é caro e opaco.&lt;/p&gt;

&lt;p&gt;A Constitutional AI (CAI), publicada pela Anthropic em 2022, inverte parte dessa lógica. Em vez de depender apenas de humanos, o Claude é treinado com uma "constituição" — um conjunto explícito de princípios (inspirados na Declaração Universal dos Direitos Humanos, entre outros). O modelo então &lt;strong&gt;critica e revisa suas próprias respostas&lt;/strong&gt; com base nesses princípios, num processo chamado RLAIF (Reinforcement Learning from AI Feedback).&lt;/p&gt;

&lt;p&gt;Na prática, isso significa transparência: os valores estão escritos, auditáveis, versionáveis. Como perito digital, essa rastreabilidade é ouro. Consigo apontar &lt;em&gt;por que&lt;/em&gt; o modelo recusou determinada ação, algo que raramente consigo com sistemas puramente baseados em feedback humano difuso.&lt;/p&gt;

&lt;h2&gt;
  
  
  O diferencial técnico frente aos concorrentes
&lt;/h2&gt;

&lt;p&gt;Testei extensivamente Claude, GPT-4 e Gemini em cenários de análise contratual e smart contracts Soroban na rede Stellar. A diferença mais notável não está no raciocínio bruto, mas na &lt;strong&gt;consistência do comportamento sob pressão adversarial&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Quando tento induzir modelos a gerar código malicioso ou contornar controles de compliance — algo essencial de testar antes de colocar IA perto de tokenização de ativos —, o Claude demonstra recusas mais coerentes e explicáveis. A constituição atua como uma camada de governança embutida, não como um filtro superficial colado depois.&lt;/p&gt;

&lt;p&gt;Dados da própria Anthropic mostram que a CAI reduz significativamente respostas "evasivas mas prejudiciais", mantendo utilidade. Em números divulgados, o modelo consegue ser menos tóxico &lt;em&gt;sem&lt;/em&gt; sacrificar capacidade — um trade-off que historicamente derrubou concorrentes, que ficavam ou permissivos demais ou inúteis de tão restritos.&lt;/p&gt;

&lt;p&gt;Como observo em minhas consultorias, e reforço aqui como André Dias Moreira Prol: no Brasil, onde a LGPD e as regulações da CVM sobre tokens tornam a explicabilidade uma exigência quase jurídica, ter um modelo cujos princípios são documentados não é luxo — é requisito de conformidade.&lt;/p&gt;

&lt;h2&gt;
  
  
  Aplicações práticas no mercado brasileiro
&lt;/h2&gt;

&lt;p&gt;O ecossistema Web3 brasileiro cresce rápido. Fintechs tokenizando recebíveis, cartórios experimentando registros em blockchain e o Drex do Banco Central pressionam por ferramentas de IA confiáveis. Aqui a Constitutional AI oferece vantagem concreta.&lt;/p&gt;

&lt;p&gt;Imagine um agente de IA analisando contratos de tokenização de crédito agrícola. Um modelo que "alucina" cláusulas ou aceita instruções ambíguas para burlar limites regulatórios representa risco patrimonial e legal enorme. Em projetos que acompanhei, o comportamento previsível do Claude reduziu a necessidade de camadas extras de validação humana — economia real de custo operacional.&lt;/p&gt;

&lt;p&gt;Além disso, para perícia digital, a auditabilidade da constituição permite que eu, ao analisar decisões automatizadas de um sistema, reconstrua a lógica ética aplicada. Isso fortalece laudos técnicos e sustenta argumentações em disputas judiciais envolvendo automação.&lt;/p&gt;

&lt;p&gt;Não sou ingênuo: a CAI não é perfeita. A escolha dos princípios é, ela mesma, um ato político e cultural, e uma constituição escrita nos EUA pode não refletir nuances jurídicas brasileiras. Mas, comparada à opacidade dos concorrentes, representa um avanço estruturante em direção a uma IA governável — e essa é a discussão que faltava no mercado.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusão
&lt;/h2&gt;

&lt;p&gt;O design ético do Claude não é acessório de marketing, mas uma escolha arquitetural que entrega transparência e previsibilidade — atributos decisivos para Web3, compliance e perícia no Brasil. Se você lidera projetos de tokenização ou IA, avalie a governança embutida dos modelos antes da performance bruta, e converse comigo para desenhar essa camada de confiança no seu produto.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Acompanhe mais artigos de André Dias Moreira Prol no &lt;a href="https://medium.com/@andreprol" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claudeai</category>
      <category>safety</category>
      <category>anthropic</category>
      <category>ai</category>
    </item>
    <item>
      <title>Multimodal Models Are Only as Good as Their Weakest Encoder</title>
      <dc:creator>AI Explore</dc:creator>
      <pubDate>Fri, 07 Aug 2026 13:01:12 +0000</pubDate>
      <link>https://dev.to/aiexplore369zoho/multimodal-models-are-only-as-good-as-their-weakest-encoder-5274</link>
      <guid>https://dev.to/aiexplore369zoho/multimodal-models-are-only-as-good-as-their-weakest-encoder-5274</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR —&lt;/strong&gt; Multimodal models look like a single system in the demo but are actually a pipeline of independently-trained encoders bolted onto a shared decoder. Each encoder carries its own training distribution, and in production the whole system is bottlenecked by whichever modality has the narrowest one — usually vision or audio, not text. Aggregate accuracy hides this because failures are confident and silent, not error-shaped.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ask a multimodal model to describe a stock photo of a golden retriever on a beach and it nails it every time. Ask the same model to read a cropped screenshot of a spreadsheet, or transcribe a phone call with crosstalk, or tell you what happened between frame 400 and frame 500 of a security video, and the failure modes get strange fast. Not "I don't know" strange — confidently wrong strange. The model doesn't hedge. It just describes something plausible that isn't there.&lt;/p&gt;

&lt;p&gt;The instinct is to blame the model. The more useful frame is architectural: a multimodal model isn't one model. It's a set of independently-trained encoders — one per modality — projected into a shared embedding space and handed off to a decoder that was mostly trained on text. Each encoder has its own training distribution, its own capacity, its own blind spots. The fused system inherits all of them, and in production, quality is bottlenecked by whichever encoder has the narrowest distribution. That's almost never the text side.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-model illusion
&lt;/h2&gt;

&lt;p&gt;Text models got good because text training data is absurdly broad — decades of the open web, code, books, forums, transcripts. Vision and audio encoders don't have that. The vision towers underneath most multimodal systems trace back to contrastive image-text pretraining on web images paired with alt text and captions: product photos, news images, stock photography, social media posts. That corpus is enormous, but it's also aesthetically and semantically narrow. It's full of things people photograph and caption. It is not full of things people generate as byproducts of work: dense tables, thermal camera frames, low-light warehouse footage, scanned forms, dashboards, X-rays, satellite tiles.&lt;/p&gt;

&lt;p&gt;When you feed one of those into the vision encoder, you're not asking the model a hard question. You're asking it a question from outside its support. The projection layer still produces an embedding — it has no mechanism to say "I've never seen anything like this" — and the decoder, trained to always produce fluent text, fills in the gap with something that sounds right. That's the confident-wrong pattern. It isn't a reasoning failure. It's an out-of-distribution failure wearing a reasoning costume.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the vision tower breaks
&lt;/h2&gt;

&lt;p&gt;Screenshots are the clearest production example. A UI screenshot is dense, text-heavy, high-frequency, and nothing like a photograph — but most vision encoders were tuned on photographic statistics, and the tokenizer and resolution pipeline built for photos will happily downsample a screenshot until the small text is mush. The model isn't failing to read; it never received a legible image. Same story with dark or low-contrast frames from cameras that weren't optimized for aesthetics, or dense technical diagrams where spatial relationships carry the meaning and a global embedding throws that away.&lt;/p&gt;

&lt;p&gt;The fix people reach for first is "use a bigger, better model." That helps at the margin. It doesn't fix the distribution problem, because the scaling laws for vision-language pretraining are still riding on the same web-image-and-caption data sources. A frontier vision encoder is a better-calibrated version of the same narrow prior, not a different prior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Audio is speech-shaped
&lt;/h2&gt;

&lt;p&gt;Audio encoders have their own version of this. Most were trained overwhelmingly on clean or near-clean speech — podcasts, audiobooks, call center recordings, read speech corpora. That means the model's implicit prior is: audio is one person talking. Feed it two overlapping speakers, a conference room with cross-talk, a phone call compressed through a codec, or — the case that breaks it hardest — audio that isn't speech at all (an alarm, a machine fault sound, ambient noise you actually care about), and the model degrades in a specific, predictable way: it tries to force what it hears into a speech-shaped transcript. Diarization gets confidently wrong. Non-speech events get silently dropped, because there was never a training signal that rewarded noticing them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Video is frames, not motion
&lt;/h2&gt;

&lt;p&gt;Video is where the illusion is most complete, because "video understanding" in most systems is not a temporal model at all. It's an image encoder applied to a sparse sample of frames, stitched into a sequence, with the actual continuity of motion reconstructed — badly — by the decoder's language priors. Sample one frame per second from a five-minute clip and you've thrown away almost everything that happened between frames. Ask the model to count repetitions in an exercise video, catch the moment an object changes hands, or notice a one-frame anomaly, and you're asking a question the sampling strategy made unanswerable before the model ever ran. The token budget makes this worse: video is by far the most expensive modality per second of content, so the practical pressure is always toward coarser sampling, which quietly narrows what the model can perceive further still. This is a preprocessing decision, not a model limitation, and it's usually made by whoever wired up the ingestion pipeline, not by anyone thinking about model capability.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to actually do about it
&lt;/h2&gt;

&lt;p&gt;The engineering implication is to stop treating "multimodal accurac&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>opensource</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>The Timezone Gap Isn't a Bug, It's a Forcing Function</title>
      <dc:creator>Jerry Kasem</dc:creator>
      <pubDate>Fri, 07 Aug 2026 13:00:53 +0000</pubDate>
      <link>https://dev.to/czechdevusa/the-timezone-gap-isnt-a-bug-its-a-forcing-function-21bc</link>
      <guid>https://dev.to/czechdevusa/the-timezone-gap-isnt-a-bug-its-a-forcing-function-21bc</guid>
      <description>&lt;p&gt;Here's a hard truth most engineering leaders don't say out loud: your team's speed depends way too much on people being in the same room at the same time.&lt;/p&gt;

&lt;p&gt;You don't notice this until someone joins who isn't. A senior engineer in Prague, six or nine hours ahead of your US team, will expose every place where your process was actually just "ask Dave, he remembers."&lt;/p&gt;

&lt;p&gt;The instinct is to treat this as a problem to manage. Standups get awkward. Someone has to stay late or log in early. Slack threads pile up overnight. It feels like friction.&lt;/p&gt;

&lt;p&gt;But watch what actually happens over the following few months.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decisions get written down.&lt;/strong&gt; When you can't grab someone in the hallway to settle a design question, you write it in a doc or a ticket instead. That doc doesn't disappear when the meeting ends. It becomes the record. Six months later, nobody has to reconstruct "why did we do it this way" from memory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PR descriptions get better.&lt;/strong&gt; A reviewer working nine hours later than the author can't ask a quick clarifying question and get an instant answer. So the author starts explaining context up front: what changed, why, what was considered and rejected. That habit doesn't stay confined to the async engineer's PRs. It spreads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Meetings get smaller and sharper.&lt;/strong&gt; You can't schedule five people across a 9-hour spread for a 30-minute sync without real cost. So teams start asking: does this need a meeting, or does it need a doc? Most of the time it's a doc.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bottlenecks around specific people become visible.&lt;/strong&gt; If your architecture decisions only happen when one particular engineer is awake and in a good mood, that's not resilience, that's a single point of failure with a keyboard. A distributed team forces you to notice this and fix it, because otherwise nothing ships on the days that person is asleep.&lt;/p&gt;

&lt;p&gt;None of this is really about timezones. It's about whether your team's knowledge lives in people's heads or in artifacts other people can read. Timezone spread just makes the gap impossible to ignore, because the workaround (walk over and ask) isn't available anymore.&lt;/p&gt;

&lt;p&gt;Companies that have scaled distributed engineering hard already know this. GitLab operates across more than 60 countries. That's not possible if institutional knowledge only exists in synchronous conversations. It's only possible if the default mode of working is written, asynchronous, and reviewable by someone who wasn't in the room.&lt;/p&gt;

&lt;p&gt;The uncomfortable part for a lot of US teams is that this same discipline would make them faster even without a single overseas hire. Distributed timezones don't create the need for better documentation and clearer decision records. They just make it impossible to keep pretending you don't need them.&lt;/p&gt;

&lt;p&gt;So if you're evaluating whether a senior engineer nine timezones away is worth the coordination cost, that's a fair question to ask. But the more useful question might be: what is your team's process currently relying on that only works because everyone happens to be awake at the same time? Because that dependency was always a risk. The timezone gap just makes you deal with it now instead of later, during an outage, when the one person who understands that system is on vacation.&lt;/p&gt;

&lt;p&gt;The engineers who thrive in this setup aren't the ones who happen to tolerate odd hours. They're the ones who were already good at writing clearly, documenting decisions, and working without needing constant real-time confirmation. That skill set is valuable on any team, in any timezone. Distributed work just makes it non-optional.&lt;/p&gt;

</description>
      <category>engineeringmanagement</category>
      <category>remotework</category>
      <category>hiring</category>
      <category>leadership</category>
    </item>
    <item>
      <title>NETO: Chat P2P local para equipos dev — Sin nube, sin excusas</title>
      <dc:creator>David Arturo Silva Baldellon</dc:creator>
      <pubDate>Fri, 07 Aug 2026 13:00:30 +0000</pubDate>
      <link>https://dev.to/dasilvaba/neto-chat-p2p-local-para-equipos-dev-sin-nube-sin-excusas-56hl</link>
      <guid>https://dev.to/dasilvaba/neto-chat-p2p-local-para-equipos-dev-sin-nube-sin-excusas-56hl</guid>
      <description>

&lt;p&gt;¿Tu equipo comparte credenciales por Slack? ¿Discuten arquitectura en herramientas que almacenan todo en servidores de terceros? Existe una alternativa radical: &lt;strong&gt;no usar la nube en absoluto&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://neto.chat" rel="noopener noreferrer"&gt;NETO&lt;/a&gt; es un chat peer-to-peer diseñado para equipos de desarrollo que trabajan en la misma red local. Sin servidores, sin cuentas, sin datos saliendo de tu oficina.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo funciona
&lt;/h2&gt;

&lt;p&gt;NETO combina tres tecnologías clave para lograr comunicación directa y segura:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;mDNS (Multicast DNS):&lt;/strong&gt; Cada instancia de NETO se anuncia en la red local mediante mDNS. No necesitas configurar IPs ni servidores de descubrimiento. Abres la app y los miembros de tu equipo aparecen automáticamente, como una impresora de red, pero para conversar.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WebRTC:&lt;/strong&gt; Una vez descubiertos los peers, la comunicación se establece mediante WebRTC. Los datos viajan directamente entre máquinas, sin&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devtools</category>
      <category>programming</category>
      <category>security</category>
    </item>
    <item>
      <title>Shipping an LLM chat widget with 481 bytes of CMS footprint - Wordpress</title>
      <dc:creator>Dhardingsea Developer</dc:creator>
      <pubDate>Fri, 07 Aug 2026 13:00:26 +0000</pubDate>
      <link>https://dev.to/dhseadev/shipping-an-llm-chat-widget-with-481-bytes-of-cms-footprint-wordpress-3752</link>
      <guid>https://dev.to/dhseadev/shipping-an-llm-chat-widget-with-481-bytes-of-cms-footprint-wordpress-3752</guid>
      <description>&lt;p&gt;A chat widget where the CMS holds a loader and nothing else — plus the four failure modes that cost me the most time: buffered SSE, base64 truncation that passes a syntax check, render-time escaping, and a balanced-but-wrong tag tree.&lt;/p&gt;

&lt;p&gt;Canonical_url&lt;br&gt;
&lt;a href="https://dhseadev.online/2026/08/06/ai-answers-desk-val-town-groq/" rel="noopener noreferrer"&gt;https://dhseadev.online/2026/08/06/ai-answers-desk-val-town-groq/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I maintain a small site where I write up the internal tooling I build — process-serving&lt;br&gt;
automation, Chrome extensions, desktop utilities. It had a static answers page. The&lt;br&gt;
obvious next step was letting people ask the things that page doesn't cover.&lt;/p&gt;

&lt;p&gt;Two constraints shaped the whole design:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;No server for me to maintain.&lt;/li&gt;
&lt;li&gt;The API key never reaches a browser, and never enters the CMS database.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What follows is the architecture I landed on, and — more usefully — the four things that&lt;br&gt;
actually cost me time. Three of those four are general enough to bite you on a completely&lt;br&gt;
different stack.&lt;/p&gt;
&lt;h2&gt;
  
  
  The shape
&lt;/h2&gt;

&lt;p&gt;The CMS side is &lt;strong&gt;481 bytes&lt;/strong&gt;. One block, and all it does is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DHSEA_ASK_ENDPOINT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://…&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// then inject &amp;lt;script src="…/widget.js"&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No plugin. No key in the database. No logic in the page.&lt;/p&gt;

&lt;p&gt;Everything else — the ~19KB widget, the styles, the chat logic, the proxy — lives in a&lt;br&gt;
single serverless file with three routes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Route&lt;/th&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /health&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Diagnostics. Written so it can never 500.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /widget.js&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Serves the widget itself.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST /&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Chat proxy to the model API.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The API key is an env var on the serverless platform. Because the widget is &lt;em&gt;served from&lt;/em&gt;&lt;br&gt;
the same function rather than pasted into the CMS, shipping a widget change is one&lt;br&gt;
external deploy and I never touch the site.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/health&lt;/code&gt; deserves a note. A health check that can throw is worse than no health check,&lt;br&gt;
because it fails in exactly the situation you're using it to diagnose. Mine returns&lt;br&gt;
&lt;code&gt;{ok, keyConfigured, store, storeError, fatal, day, dayReq, dayTokens, …}&lt;/code&gt; — errors&lt;br&gt;
become &lt;em&gt;fields&lt;/em&gt;, not exceptions.&lt;/p&gt;
&lt;h3&gt;
  
  
  Budget, because it's my card
&lt;/h3&gt;

&lt;p&gt;Public endpoint plus paid inference is a bad combination unless you bound it up front:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;per-IP: 4/min, 30/day&lt;/li&gt;
&lt;li&gt;global: 12/min, 1500/day&lt;/li&gt;
&lt;li&gt;hard ceiling: 300k tokens/day&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Counters live in the platform's SQLite so they survive cold starts — in-memory counters&lt;br&gt;
on a serverless host reset when the instance recycles, which means no rate limit at all&lt;br&gt;
under exactly the traffic pattern you care about.&lt;/p&gt;

&lt;p&gt;On top: an origin allowlist. My domain gets 200. A &lt;strong&gt;lookalike&lt;/strong&gt; domain gets 403. A&lt;br&gt;
request with no &lt;code&gt;Origin&lt;/code&gt; header gets 403. Test all three; the lookalike case is the one&lt;br&gt;
naive &lt;code&gt;startsWith&lt;/code&gt; checks fail.&lt;/p&gt;
&lt;h2&gt;
  
  
  Four things that cost me the most time
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. Your platform may buffer SSE
&lt;/h3&gt;

&lt;p&gt;The model API streams tokens properly. My serverless host buffers, so what reached the&lt;br&gt;
browser was roughly two chunks — not a stream.&lt;/p&gt;

&lt;p&gt;Total round trip is under a second, so it reads fine. But I'd already started building a&lt;br&gt;
token-by-token typewriter UI that was never going to work on that host.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verify streaming end to end on your actual deployment target&lt;/strong&gt;, not on the vendor's&lt;br&gt;
docs page. The vendor streams. That tells you nothing about what your host does with it.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. A truncated base64 payload passes a syntax check
&lt;/h3&gt;

&lt;p&gt;The widget is base64-embedded into the serverless file by a build script. If a copy step&lt;br&gt;
truncates the blob:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the file still parses — &lt;code&gt;node --check&lt;/code&gt; is &lt;strong&gt;happy&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;atob&lt;/code&gt; fails at runtime&lt;/li&gt;
&lt;li&gt;the panel renders blank&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A syntax check attests the &lt;em&gt;file&lt;/em&gt;, never the &lt;em&gt;payload&lt;/em&gt;. So: never hand-copy the payload&lt;br&gt;
(a build script does it file-to-file), and verify the deploy against the live artifact:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://…/widget.js | shasum &lt;span class="nt"&gt;-a&lt;/span&gt; 256
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…compared against the same hash computed on the local build. &lt;strong&gt;Same byte length is not&lt;br&gt;
the same bytes.&lt;/strong&gt; Compare hashes. Mine matched at 19,260 chars — that match is the&lt;br&gt;
evidence the deploy worked, not the fact that the paste didn't throw.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Verify the served bytes, not the saved content
&lt;/h3&gt;

&lt;p&gt;My CMS applies escaping at render time. So the stored content was byte-clean while the&lt;br&gt;
live page was dead and the console was empty.&lt;/p&gt;

&lt;p&gt;Reading back what you just saved proves the &lt;em&gt;write&lt;/em&gt; succeeded. It proves nothing about&lt;br&gt;
what the visitor receives. Fetch the rendered output and assert on that.&lt;/p&gt;

&lt;p&gt;This generalizes past CMSs: anywhere a transform sits between your write and the user —&lt;br&gt;
a bundler, a minifier, a template engine, a CDN — the artifact you validated is not the&lt;br&gt;
artifact they get.&lt;/p&gt;
&lt;h3&gt;
  
  
  4. Balanced tags are not a correct tree
&lt;/h3&gt;

&lt;p&gt;This is my favorite, because every mechanical check I had passed it.&lt;/p&gt;

&lt;p&gt;A page with opening and closing &lt;code&gt;div&lt;/code&gt; counts matching &lt;strong&gt;exactly&lt;/strong&gt; — 14 and 14. And six&lt;br&gt;
sections nested inside a seventh, because one closing tag had been deferred to the end of&lt;br&gt;
the document.&lt;/p&gt;

&lt;p&gt;Balance counting cannot catch this. Absence and misplacement are different bugs, and&lt;br&gt;
counting only detects absence. Parse it instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DOMParser&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;parseFromString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text/html&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.qa .qa&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 6 → nested. Should be 0.&lt;/span&gt;
&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.in &amp;gt; .qa&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// should equal total .qa&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That assertion is the one that catches it, and it's the one I now run.&lt;/p&gt;

&lt;h2&gt;
  
  
  The through-line
&lt;/h2&gt;

&lt;p&gt;Every one of these four is the same mistake wearing a different hat: &lt;strong&gt;I checked the&lt;br&gt;
thing I could reach instead of the thing the user gets.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The saved content instead of the served page. The file's syntax instead of the payload's&lt;br&gt;
integrity. The vendor's streaming behavior instead of my host's. The tag count instead&lt;br&gt;
of the tree.&lt;/p&gt;

&lt;p&gt;Each check passed. Each one attested something adjacent to what I actually needed to know.&lt;br&gt;
Whatever sits between your code and your user — a renderer, a host, an encoder, a build&lt;br&gt;
step — is the boundary your verification has to cross, or it isn't verification.&lt;/p&gt;




&lt;p&gt;The result is live at &lt;a href="https://dhseadev.online/ask/" rel="noopener noreferrer"&gt;dhseadev.online/ask&lt;/a&gt;, sitting next&lt;br&gt;
to the &lt;a href="https://dhseadev.online/answers/" rel="noopener noreferrer"&gt;static answers&lt;/a&gt; it was built to extend. If you&lt;br&gt;
break it, I'd like to hear how.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>serverless</category>
      <category>ai</category>
      <category>wordpress</category>
    </item>
    <item>
      <title>How to Move SvelteKit Config Into vite.config.js (July 2026)</title>
      <dc:creator>Frank</dc:creator>
      <pubDate>Fri, 07 Aug 2026 13:00:15 +0000</pubDate>
      <link>https://dev.to/frank_signorini/how-to-move-sveltekit-config-into-viteconfigjs-july-2026-85g</link>
      <guid>https://dev.to/frank_signorini/how-to-move-sveltekit-config-into-viteconfigjs-july-2026-85g</guid>
      <description>&lt;p&gt;I saw the July 2026 Svelte blog post announcing that SvelteKit’s configuration can now live directly inside &lt;code&gt;vite.config.js&lt;/code&gt;. As someone who maintains several SvelteKit apps, this caught my eye because it promises a single source of truth for build tooling, reduces boilerplate, and aligns SvelteKit more tightly with the Vite ecosystem we already use daily.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this change matters right now
&lt;/h3&gt;

&lt;p&gt;Since the first stable release of SvelteKit, the framework has relied on a separate &lt;code&gt;svelte.config.js&lt;/code&gt; file for things like adapters, prerendering options, and preprocessors. While that separation made sense when SvelteKit was still figuring out its relationship with Vite, it also introduced a small friction point:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Two config files to keep in sync&lt;/strong&gt; – you often end up opening both &lt;code&gt;svelte.config.js&lt;/code&gt; and &lt;code&gt;vite.config.js&lt;/code&gt; when tweaking SSR, environment variables, or custom Vite plugins.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tooling confusion&lt;/strong&gt; – IDE extensions sometimes treat the two files as unrelated, causing false warnings about unknown properties.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bootstrapping overhead&lt;/strong&gt; – new contributors have to learn which settings belong where, which adds cognitive load during onboarding.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By allowing the SvelteKit config to be embedded under a &lt;code&gt;sveltekit&lt;/code&gt; key in &lt;code&gt;vite.config.js&lt;/code&gt;, the Svelte team has effectively merged the two configuration surfaces. This is especially handy for monorepos or when you already have a complex Vite setup (e.g., multiple entry points, custom aliasing, or shared plugins). Now you can see the whole picture in one place, and the Vite dev server will automatically pick up any SvelteKit‑specific tweaks without an extra config file.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the new API looks like
&lt;/h3&gt;

&lt;p&gt;The blog post shows a minimal example that replaces a typical &lt;code&gt;svelte.config.js&lt;/code&gt; with a single &lt;code&gt;vite.config.js&lt;/code&gt;. Here’s how I migrated a fresh SvelteKit project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// vite.config.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sveltekit&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@sveltejs/kit/vite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;visualizer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rollup-plugin-visualizer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Old separate svelte.config.js (for reference)&lt;/span&gt;
&lt;span class="c1"&gt;// export default {&lt;/span&gt;
&lt;span class="c1"&gt;//   kit: {&lt;/span&gt;
&lt;span class="c1"&gt;//     adapter: adapterNode(),&lt;/span&gt;
&lt;span class="c1"&gt;//     prerender: { default: true }&lt;/span&gt;
&lt;span class="c1"&gt;//   }&lt;/span&gt;
&lt;span class="c1"&gt;// };&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;sveltekit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="c1"&gt;// All SvelteKit options go here&lt;/span&gt;
      &lt;span class="na"&gt;kit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// The adapter you were using before&lt;/span&gt;
        &lt;span class="na"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@sveltejs/adapter-node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)(),&lt;/span&gt;
        &lt;span class="c1"&gt;// Keep your prerender defaults&lt;/span&gt;
        &lt;span class="na"&gt;prerender&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="c1"&gt;// You can still add vite-specific overrides inside&lt;/span&gt;
        &lt;span class="c1"&gt;// the same object if you need them&lt;/span&gt;
        &lt;span class="na"&gt;vite&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="c1"&gt;// Example: custom environment variable handling&lt;/span&gt;
          &lt;span class="na"&gt;define&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;__APP_VERSION__&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.0.0&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="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="c1"&gt;// Any other Vite plugins stay where they belong&lt;/span&gt;
    &lt;span class="nf"&gt;visualizer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./stats.html&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="c1"&gt;// General Vite config stays at the top level&lt;/span&gt;
  &lt;span class="na"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;$components&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/src/lib/components&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;$utils&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/src/lib/utils&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="na"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5173&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;strictPort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;A few things to note:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Import the &lt;code&gt;sveltekit&lt;/code&gt; plugin from &lt;code&gt;@sveltejs/kit/vite&lt;/code&gt;&lt;/strong&gt; – this is the same plugin that Vite automatically adds when you run &lt;code&gt;npm init svelte@next&lt;/code&gt;, but now you call it explicitly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrap all SvelteKit‑specific keys inside the &lt;code&gt;kit&lt;/code&gt; object&lt;/strong&gt; – this mirrors the shape of the old &lt;code&gt;svelte.config.js&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can still expose Vite‑only settings&lt;/strong&gt; (like &lt;code&gt;resolve.alias&lt;/code&gt; or &lt;code&gt;server.port&lt;/code&gt;) at the top level of the config, keeping everything in one file.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you already have a &lt;code&gt;vite.config.js&lt;/code&gt; with custom plugins, you simply add the &lt;code&gt;sveltekit&lt;/code&gt; call to the &lt;code&gt;plugins&lt;/code&gt; array and move the &lt;code&gt;kit&lt;/code&gt; block into its options. No more “duplicate &lt;code&gt;adapter&lt;/code&gt; definitions” or “missing &lt;code&gt;prerender&lt;/code&gt; flag” errors.&lt;/p&gt;

&lt;h3&gt;
  
  
  How this affects common workflows
&lt;/h3&gt;

&lt;h4&gt;
  
  
  1. Adding a new adapter
&lt;/h4&gt;

&lt;p&gt;Previously you’d edit &lt;code&gt;svelte.config.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// svelte.config.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;adapterStatic&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@sveltejs/adapter-static&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;kit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;adapterStatic&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="c1"&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;Now you do it inside &lt;code&gt;vite.config.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// vite.config.js (excerpt)&lt;/span&gt;
&lt;span class="nf"&gt;sveltekit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;kit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@sveltejs/adapter-static&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)(),&lt;/span&gt;
    &lt;span class="c1"&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 change is syntactic, but it eliminates the need to keep two files in sync when you switch adapters for staging vs. production.&lt;/p&gt;

&lt;h4&gt;
  
  
  2. Using environment variables in adapters
&lt;/h4&gt;

&lt;p&gt;Because the adapter configuration lives inside the Vite plugin call, you can reference Vite’s &lt;code&gt;process.env&lt;/code&gt; (or the newer &lt;code&gt;import.meta.env&lt;/code&gt;) directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;kit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@sveltejs/adapter-node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)({&lt;/span&gt;
    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Pass a runtime variable to the adapter&lt;/span&gt;
      &lt;span class="na"&gt;NODE_ENV&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODE_ENV&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;This feels more natural than pulling &lt;code&gt;dotenv&lt;/code&gt; into a separate &lt;code&gt;svelte.config.js&lt;/code&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  3. Custom preprocessors
&lt;/h4&gt;

&lt;p&gt;If you need a preprocessor like &lt;code&gt;svelte-preprocess&lt;/code&gt;, you still import it and pass it to the &lt;code&gt;sveltekit&lt;/code&gt; plugin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;preprocess&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;svelte-preprocess&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;sveltekit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;kit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// …&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;preprocess&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API remains identical; the only difference is the file location.&lt;/p&gt;

&lt;h3&gt;
  
  
  Potential downsides
&lt;/h3&gt;

&lt;p&gt;No change is without trade‑offs. Here are the practical concerns I ran into during migration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Learning curve for newcomers&lt;/strong&gt; – developers who have only read older tutorials may be confused when they can’t find a &lt;code&gt;svelte.config.js&lt;/code&gt;. The docs now need to be explicit about the new location.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tooling gaps&lt;/strong&gt; – some community plugins (e.g., ESLint configs that look for &lt;code&gt;svelte.config.js&lt;/code&gt;) still assume the old file exists. In my monorepo I had to add a small shim file that re‑exports the config just to keep those tools happy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version lock&lt;/strong&gt; – the new feature is tied to SvelteKit 1.28+ (the version shipped with the July 2026 release). Projects pinned to earlier releases will need to upgrade anyway, which may involve other breaking changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Overall, the drawbacks are mostly about updating documentation and a few edge‑case tool integrations, not about runtime behavior.&lt;/p&gt;

&lt;h3&gt;
  
  
  My personal take
&lt;/h3&gt;

&lt;p&gt;I decided to upgrade my production SvelteKit apps to the July 2026 release after a quick test branch. The migration took less than 15 minutes per repo, and the resulting &lt;code&gt;vite.config.js&lt;/code&gt; felt cleaner: everything from adapters to custom Vite plugins lives under one roof. In environments where we already maintain a shared Vite config (e.g., a design‑system library that ships both React and Svelte components), this consolidation reduces the mental overhead for new hires.&lt;/p&gt;

&lt;p&gt;If you’re on a brand‑new SvelteKit project, I’d start there—skip the &lt;code&gt;svelte.config.js&lt;/code&gt; entirely and keep your config in &lt;code&gt;vite.config.js&lt;/code&gt;. For existing projects, weigh the benefit of a single config file against the effort of updating any tooling that expects &lt;code&gt;svelte.config.js&lt;/code&gt;. In most cases, the upgrade is worth it, especially because it aligns SvelteKit with the broader Vite ecosystem and paves the&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>node</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Compact Video Metadata Serialization With Protobuf Across Services</title>
      <dc:creator>ahmet gedik</dc:creator>
      <pubDate>Fri, 07 Aug 2026 13:00:02 +0000</pubDate>
      <link>https://dev.to/ahmet_gedik778845/compact-video-metadata-serialization-with-protobuf-across-services-el0</link>
      <guid>https://dev.to/ahmet_gedik778845/compact-video-metadata-serialization-with-protobuf-across-services-el0</guid>
      <description>&lt;h2&gt;
  
  
  The 40KB Problem Nobody Noticed Until Cloudflare Billed Us
&lt;/h2&gt;

&lt;p&gt;Every viral video we ingest at ViralVidVault crosses three service boundaries before it ever reaches a user. A PHP 8.4 ingestion worker pulls the raw metadata, hands it to a Go trend-scoring service, which in turn feeds a Python analytics pipeline that computes velocity and acceleration curves for the European feeds. For a long time these three services spoke JSON to each other, because JSON is what everyone reaches for first. It worked, it was debuggable, and nobody questioned it.&lt;/p&gt;

&lt;p&gt;Then we looked at the numbers. A single enriched &lt;code&gt;VideoMetadata&lt;/code&gt; record — title, tags, region, published timestamp, view counts, and a nested block of trend signals — averaged just under 40KB as pretty-printed JSON, and around 28KB minified. Multiply that by the ~2.1 million records that move between services every day during a trend spike, and the internal egress alone was measurably showing up on our Cloudflare bill. Worse, the Go service was burning real CPU on &lt;code&gt;encoding/json&lt;/code&gt; reflection, and the PHP worker spent more time in &lt;code&gt;json_encode&lt;/code&gt; than it did doing the actual HTTP fetch.&lt;/p&gt;

&lt;p&gt;We migrated the inter-service contract to Protocol Buffers. Payloads dropped by roughly 68%, parse CPU on the Go side fell by more than half, and — the part I care about most as someone shipping under GDPR — the schema became a single enforced contract instead of a loose bag of keys. This is the write-up I wish I'd had before starting. If you want to see the end result in production, it's the discovery engine behind &lt;a href="https://viralvidvault.com" rel="noopener noreferrer"&gt;ViralVidVault&lt;/a&gt;, our GDPR-compliant European viral video tracker.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why JSON Was Costing Us More Than Bytes
&lt;/h2&gt;

&lt;p&gt;The byte count is the obvious problem, but it wasn't the expensive one. Three things hurt more than payload size:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No schema enforcement.&lt;/strong&gt; A field renamed in the PHP worker would silently become &lt;code&gt;null&lt;/code&gt; in the Go consumer. We caught these in production, not in review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type ambiguity.&lt;/strong&gt; JSON numbers are all doubles. A &lt;code&gt;view_count&lt;/code&gt; of 4,300,000,000 quietly lost precision once it crossed 2^53, which for a genuinely viral clip is not hypothetical.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reflection cost.&lt;/strong&gt; Both &lt;code&gt;encoding/json&lt;/code&gt; in Go and &lt;code&gt;json_encode&lt;/code&gt; in PHP walk the structure at runtime. At our volumes that reflection was a real slice of CPU, and CPU on our LiteSpeed origin is finite.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Protobuf addresses all three at once: a compiled schema, explicit integer widths, and generated marshalling code that doesn't reflect at runtime. The tradeoff is that the wire format is no longer human-readable, which matters less than you'd think once you have decent tooling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Defining the Schema
&lt;/h2&gt;

&lt;p&gt;Everything starts with the &lt;code&gt;.proto&lt;/code&gt; file. This is the single source of truth that all three languages compile against. I keep it in a dedicated &lt;code&gt;contracts/&lt;/code&gt; repository that the PHP, Go, and Python services each vendor in, so nobody can drift.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight protobuf"&gt;&lt;code&gt;&lt;span class="na"&gt;syntax&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"proto3"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;package&lt;/span&gt; &lt;span class="nn"&gt;viralvidvault&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metadata.v1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;message&lt;/span&gt; &lt;span class="nc"&gt;VideoMetadata&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="na"&gt;video_id&lt;/span&gt;            &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;               &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;uint32&lt;/span&gt; &lt;span class="na"&gt;duration_seconds&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;uint64&lt;/span&gt; &lt;span class="na"&gt;view_count&lt;/span&gt;          &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;Region&lt;/span&gt; &lt;span class="na"&gt;region&lt;/span&gt;              &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;repeated&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="na"&gt;tags&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;int64&lt;/span&gt;  &lt;span class="na"&gt;published_at_unix&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;TrendSignals&lt;/span&gt; &lt;span class="na"&gt;signals&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;bool&lt;/span&gt;   &lt;span class="na"&gt;gdpr_pii_stripped&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;Region&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;REGION_UNSPECIFIED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="na"&gt;REGION_DE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="na"&gt;REGION_FR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="na"&gt;REGION_ES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="na"&gt;REGION_IT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="na"&gt;REGION_NL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="na"&gt;REGION_PL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;message&lt;/span&gt; &lt;span class="nc"&gt;TrendSignals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;float&lt;/span&gt;  &lt;span class="na"&gt;velocity&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// views/hour, normalised&lt;/span&gt;
  &lt;span class="kt"&gt;float&lt;/span&gt;  &lt;span class="na"&gt;acceleration&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// d(velocity)/dt&lt;/span&gt;
  &lt;span class="kt"&gt;uint32&lt;/span&gt; &lt;span class="na"&gt;shares_per_hour&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&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;A few deliberate choices worth calling out, because they are the ones that bite people later:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Field numbers are permanent.&lt;/strong&gt; The number &lt;code&gt;4&lt;/code&gt; is what goes on the wire, not the name &lt;code&gt;view_count&lt;/code&gt;. You can rename the field freely, but never reuse or renumber a tag. If you retire a field, mark it &lt;code&gt;reserved&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;uint64&lt;/code&gt; for view counts.&lt;/strong&gt; This is the precision fix. Protobuf varints encode small numbers in one byte and only grow as the value grows, so you pay nothing for the wide type until you actually need it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;region&lt;/code&gt; enum is versioned by the &lt;code&gt;v1&lt;/code&gt; package.&lt;/strong&gt; European regions rarely change, but the enum's zero value being &lt;code&gt;REGION_UNSPECIFIED&lt;/code&gt; means an unset region is unambiguous rather than defaulting to a real country.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;gdpr_pii_stripped&lt;/code&gt; is a boolean gate.&lt;/strong&gt; Our contract says no record leaves the ingestion worker for analytics unless PII has been removed, and this flag is asserted downstream. More on that below.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Enum choice matters for size too: an enum is a varint on the wire, so &lt;code&gt;REGION_DE&lt;/code&gt; costs one byte, versus the string &lt;code&gt;"DE"&lt;/code&gt; costing three plus framing in JSON.&lt;/p&gt;

&lt;h2&gt;
  
  
  Encoding in PHP 8.4
&lt;/h2&gt;

&lt;p&gt;Our ingestion worker is PHP. The official &lt;code&gt;google/protobuf&lt;/code&gt; package ships a pure-PHP runtime, but for our throughput I strongly recommend installing the &lt;code&gt;protobuf&lt;/code&gt; C extension as well — the pure-PHP path is correct but noticeably slower on hot loops. With &lt;code&gt;protoc&lt;/code&gt; and the PHP plugin you generate classes under a namespace that mirrors the package.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="k"&gt;declare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strict_types&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;ViralVidVault\Metadata\V1\VideoMetadata&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;ViralVidVault\Metadata\V1\TrendSignals&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;ViralVidVault\Metadata\V1\Region&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;buildMetadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$signals&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TrendSignals&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setVelocity&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'velocity'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setAcceleration&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'acceleration'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setSharesPerHour&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'shares_per_hour'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="nv"&gt;$meta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;VideoMetadata&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setVideoId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'video_id'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setTitle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'title'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setDurationSeconds&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'duration_seconds'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setViewCount&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'view_count'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setRegion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Region&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;REGION_DE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setPublishedAtUnix&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'published_at_unix'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setGdprPiiStripped&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setSignals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$signals&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// repeated fields take an array; the runtime handles the packing&lt;/span&gt;
    &lt;span class="nv"&gt;$meta&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setTags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'tags'&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;// returns the compact binary wire format, ready for the next hop&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$meta&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;serializeToString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Decoding a payload that came back from another service:&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;parseMetadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$binary&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;VideoMetadata&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$meta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;VideoMetadata&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$meta&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;mergeFromString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$binary&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// throws on malformed input&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$meta&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;Two practical notes. First, &lt;code&gt;serializeToString()&lt;/code&gt; gives you the raw binary — you send it as the request body with &lt;code&gt;Content-Type: application/x-protobuf&lt;/code&gt;, not as a JSON string. Second, &lt;code&gt;mergeFromString()&lt;/code&gt; throws on genuinely malformed bytes but happily ignores unknown fields, which is exactly the forward-compatibility behaviour you want during a rolling deploy where the encoder is a version ahead of the decoder.&lt;/p&gt;

&lt;p&gt;One PHP-8.4-specific gotcha: &lt;code&gt;int&lt;/code&gt; in PHP is a 64-bit signed integer on any 64-bit build, so a &lt;code&gt;uint64&lt;/code&gt; view count near the top of the range round-trips fine as a native int. If you ever run on a 32-bit SAPI, the runtime falls back to string representation for large integers — worth an assertion in your tests if you can't guarantee the build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Consuming in Go
&lt;/h2&gt;

&lt;p&gt;The trend-scoring service is Go, and this is where the CPU win showed up most clearly. Generated code plus &lt;code&gt;google.golang.org/protobuf/proto&lt;/code&gt; gives you zero-reflection unmarshalling.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;scoring&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"fmt"&lt;/span&gt;

    &lt;span class="s"&gt;"google.golang.org/protobuf/proto"&lt;/span&gt;
    &lt;span class="n"&gt;pb&lt;/span&gt; &lt;span class="s"&gt;"github.com/viralvidvault/contracts/gen/go/metadata/v1"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;// DecodeAndScore unmarshals a wire payload and rejects anything that&lt;/span&gt;
&lt;span class="c"&gt;// has not been through the GDPR PII strip in the ingestion worker.&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;DecodeAndScore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;meta&lt;/span&gt; &lt;span class="n"&gt;pb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VideoMetadata&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unmarshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"decode metadata: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&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="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetGdprPiiStripped&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="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"refusing record %s: pii not stripped"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetVideoId&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;sig&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetSignals&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sig&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="c"&gt;// no signals yet, score is zero&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// A cheap composite score; the real one is a weighted model.&lt;/span&gt;
    &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetVelocity&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="m"&gt;0.6&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
        &lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetAcceleration&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="m"&gt;0.3&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
        &lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetSharesPerHour&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="m"&gt;0.1&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&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;Get*&lt;/code&gt; accessors are the important habit here: they are nil-safe. &lt;code&gt;meta.GetSignals()&lt;/code&gt; on a message where &lt;code&gt;signals&lt;/code&gt; was never set returns a typed nil pointer, and &lt;code&gt;sig.GetVelocity()&lt;/code&gt; on that nil returns the zero value rather than panicking. This is proto3's answer to the JSON &lt;code&gt;null&lt;/code&gt; problem — there's a defined default for every scalar, so downstream code doesn't need defensive existence checks scattered everywhere.&lt;/p&gt;

&lt;p&gt;Benchmarked against our old &lt;code&gt;json.Unmarshal&lt;/code&gt; path over a representative sample, the protobuf decode was consistently 2–3x faster and allocated far less, because there's no map construction and no reflection walk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Storing the Binary in SQLite WAL
&lt;/h2&gt;

&lt;p&gt;Our origin uses SQLite in WAL mode as the local cache on each LiteSpeed node. Here's a pattern that surprised people on my team: you don't have to unpack protobuf to store it. The compact binary is a perfectly good &lt;code&gt;BLOB&lt;/code&gt;, and SQLite treats it as an opaque byte string.&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;import&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;viralvidvault.metadata.v1&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;video_metadata_pb2&lt;/span&gt;

&lt;span class="n"&gt;con&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cache.db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;con&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PRAGMA journal_mode=WAL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;con&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CREATE TABLE IF NOT EXISTS video_meta (&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  video_id TEXT PRIMARY KEY,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  region   INTEGER,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;        &lt;span class="c1"&gt;# denormalised for WHERE filters
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  payload  BLOB NOT NULL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;   &lt;span class="c1"&gt;# the raw protobuf bytes
&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;meta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;video_metadata_pb2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;VideoMetadata&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ParseFromString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# validate before we trust it
&lt;/span&gt;    &lt;span class="n"&gt;con&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT OR REPLACE INTO video_meta (video_id, region, payload) VALUES (?, ?, ?)&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="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;video_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;con&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&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;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;video_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&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;video_metadata_pb2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VideoMetadata&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;con&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT payload FROM video_meta WHERE video_id = ?&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="n"&gt;video_id&lt;/span&gt;&lt;span class="p"&gt;,)&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;meta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;video_metadata_pb2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;VideoMetadata&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ParseFromString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;meta&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trick is denormalising the couple of fields you actually filter on — here &lt;code&gt;region&lt;/code&gt; — into real columns while keeping the full record as an opaque blob. You get indexable queries on the hot dimensions and a compact single-blob store for everything else, and the blob is already in the exact format you'll ship to the next service. No re-encoding on read. In WAL mode these blob writes don't block concurrent readers, which keeps the analytics pipeline from stalling the ingestion path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decoding at the Edge in a Cloudflare Worker
&lt;/h2&gt;

&lt;p&gt;Because our audience is European and latency-sensitive, some responses are assembled at the Cloudflare edge. Protobuf travels well here too — you decode it in the Worker using &lt;code&gt;protobufjs&lt;/code&gt; and emit whatever shape the browser needs, so the compact format lives all the way to the edge and only becomes JSON at the last possible moment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;protobuf&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;protobufjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./metadata.v1.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// protobufjs JSON descriptor&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;protobuf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;VideoMetadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lookupType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;viralvidvault.metadata.v1.VideoMetadata&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;origin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;META&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// bytes from origin&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;meta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;VideoMetadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Strip anything the browser has no business seeing, then hand back JSON.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;videoId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;velocity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signals&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;velocity&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&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;content-type&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;application/json; charset=utf-8&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="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 origin-to-edge hop stays compact, and the JSON expansion happens once, at the edge, right before it hits the browser. That is the smallest possible amount of JSON in the whole path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Schema Evolution Without Breaking Deploys
&lt;/h2&gt;

&lt;p&gt;The question everyone asks: how do you change the schema without a big-bang deploy across three languages? Proto3 makes this genuinely safe if you follow a small set of rules.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Only add fields with new numbers.&lt;/strong&gt; Old decoders ignore unknown fields; new decoders see the defaults for fields that old encoders didn't set.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never change a field's number or wire type.&lt;/strong&gt; Renaming is fine, renumbering is not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reserve retired fields.&lt;/strong&gt; Add &lt;code&gt;reserved 6;&lt;/code&gt; and &lt;code&gt;reserved "tags";&lt;/code&gt; so nobody accidentally reuses the slot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy decoders before encoders.&lt;/strong&gt; Roll out the readers that understand the new field first, then the writers that emit it. This ordering means there's never a moment where a new field arrives at a service that can't parse it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because unknown fields are preserved rather than dropped by the binary runtimes, a record can pass through a service that's a version behind and come out the other side intact — the intermediate service round-trips fields it doesn't even know about. That property alone eliminated a class of data-loss bugs we used to hit whenever the JSON contract drifted mid-deploy.&lt;/p&gt;

&lt;h2&gt;
  
  
  GDPR Notes Specific to This Setup
&lt;/h2&gt;

&lt;p&gt;Because we operate under GDPR, the schema itself is part of our compliance story, not an afterthought:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;gdpr_pii_stripped&lt;/code&gt; flag is enforced downstream, not just set upstream.&lt;/strong&gt; The Go consumer above rejects any record where it's false. A single boolean in the contract is worth more than a policy document nobody reads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No raw personal identifiers ever enter &lt;code&gt;VideoMetadata&lt;/code&gt;.&lt;/strong&gt; We deliberately kept uploader identity out of the message. If it isn't in the schema, it can't leak through the schema.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The binary format is not obscurity-as-security&lt;/strong&gt; — anyone with the &lt;code&gt;.proto&lt;/code&gt; can decode it — but keeping the compact payload internal and only expanding a whitelisted subset at the edge means the browser sees strictly less than what moves between services.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I'd Tell Someone Starting Today
&lt;/h2&gt;

&lt;p&gt;Protobuf is not free complexity. You take on a build step (&lt;code&gt;protoc&lt;/code&gt;), a generated-code artifact per language, and payloads you can't &lt;code&gt;curl | jq&lt;/code&gt; without a decoder. For a small app talking to itself, JSON is still the right call.&lt;/p&gt;

&lt;p&gt;But the moment you have multiple services in multiple languages exchanging the same records at volume, the math flips hard. For us the wins were concrete: ~68% smaller payloads, 2–3x faster decode in Go, precise 64-bit counts, and — the quiet one — a schema that turns contract drift into a compile error instead of a 3am incident. If your metadata crosses a language boundary more than once, it's worth the build step.&lt;/p&gt;

&lt;p&gt;Start with one message, one boundary, and the four evolution rules above pinned somewhere your whole team can see them. Everything else is incremental.&lt;/p&gt;

</description>
      <category>protobuf</category>
      <category>php</category>
      <category>go</category>
      <category>serialization</category>
    </item>
    <item>
      <title>Why I Built a Journaling App That Never Touches the Cloud</title>
      <dc:creator>AI Predictions Dev</dc:creator>
      <pubDate>Fri, 07 Aug 2026 13:00:01 +0000</pubDate>
      <link>https://dev.to/aipredictions_dev/why-i-built-a-journaling-app-that-never-touches-the-cloud-59bi</link>
      <guid>https://dev.to/aipredictions_dev/why-i-built-a-journaling-app-that-never-touches-the-cloud-59bi</guid>
      <description>&lt;p&gt;Most AI journaling apps promise privacy, but they usually mean "we don’t sell your data to advertisers." They still upload your raw text to a central server for processing. For me, that was a dealbreaker. I wanted a tool that could analyze my moods and patterns without ever leaving my device, even if I was offline or behind a strict firewall.&lt;/p&gt;

&lt;p&gt;The result is JournalMind, a journaling app that runs 100% in the browser via WebGPU. There is no backend processing of your entries. Nothing is uploaded. Not even metadata.&lt;/p&gt;

&lt;h3&gt;
  
  
  The WebGPU Shift
&lt;/h3&gt;

&lt;p&gt;For years, running inference in the browser meant slow, clunky experiences or relying on massive WASM files that choked mobile devices. The arrival of WebGPU changed the game. It allows the browser to access the GPU directly, enabling small, efficient models to run with near-native speed.&lt;/p&gt;

&lt;p&gt;I built JournalMind around this capability. When you type an entry, a small model that runs in your browser analyzes the sentiment, extracts key themes, and logs mood trends. This happens locally. If you close your laptop, the app works. If your internet cuts out, the insights are still generated.&lt;/p&gt;

&lt;p&gt;This architecture solves a specific developer problem: trust. In an era of data leaks and privacy concerns, offloading AI to the cloud introduces a surface area of risk. By keeping the model private on-device AI, the only person who sees your thoughts is you.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Engineering Trade-offs
&lt;/h3&gt;

&lt;p&gt;Building for the browser has constraints. You cannot load a 13-billion-parameter model and expect it to run smoothly on a mid-range laptop. You have to be ruthless about efficiency.&lt;/p&gt;

&lt;p&gt;The challenge wasn’t just accuracy; it was memory management. A common mistake in client-side AI is letting the model context grow unbounded. In JournalMind, we limit the context window strictly to the current session and a rolling buffer of recent entries. This keeps the memory footprint low and the inference time under a second.&lt;/p&gt;

&lt;p&gt;Here is how the inference loop looks in practice. We avoid heavy initialization costs by using a pre-compiled model that loads only when needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Simplified inference logic&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;loadLocalModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sentiment-v2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Process entry locally&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;analysis&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;currentEntry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;recentEntries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Store result locally in IndexedDB&lt;/span&gt;
&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;journals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;currentEntry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;mood&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;analysis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mood&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&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;This approach means the app feels instant. There is no "processing..." spinner while waiting for a server response. The feedback loop is tight, which encourages consistent journaling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Privacy by Design, Not by Feature
&lt;/h3&gt;

&lt;p&gt;Most apps treat privacy as a feature you toggle on. In JournalMind, it is the foundation. Because the processing happens on your device, the app does not need to know who you are. There is no account creation required to start using it. You can use it anonymously, or sync your own encrypted backups if you choose.&lt;/p&gt;

&lt;p&gt;This design decision forced us to rethink how we handle data persistence. Without a central database, we rely on IndexedDB and local storage. This introduces a new set of challenges: backup strategies, versioning, and handling data corruption. But it also simplifies the user experience. You don’t need to worry about "cloud sync conflicts" because there is no cloud. Your data lives where you put it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Honest Pricing
&lt;/h3&gt;

&lt;p&gt;JournalMind is a paid tool. It is not free, because maintaining the quality of the on-device models and the continuous optimization of the WebGPU pipeline requires resources. There is a 7-day trial so you can test the local inference speed on your specific hardware. For users who prefer to explore without commitment, there are free turns available in the companion games, which also run entirely locally.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Future of Local AI
&lt;/h3&gt;

&lt;p&gt;The shift toward private on-device AI is not just a trend; it is a necessity. As models become more efficient, we will see more applications that do not require a network connection to be intelligent. Journaling is just one use case. Imagine code editors, note-taking apps, and personal assistants that work offline with the same depth of insight.&lt;/p&gt;

&lt;p&gt;I am curious about your experience with local AI tools. Have you tried any apps that run inference entirely in the browser, and did the performance meet your expectations?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>programming</category>
      <category>privacy</category>
    </item>
    <item>
      <title>Qwen Code vs Aider vs OpenCode: I Ran the Same 12 Tasks Through 3 Local CLI Agents on One RTX 4070</title>
      <dc:creator>Ken Imoto</dc:creator>
      <pubDate>Fri, 07 Aug 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/kenimo49/qwen-code-vs-aider-vs-opencode-i-ran-the-same-12-tasks-through-3-local-cli-agents-on-one-rtx-4070-378i</link>
      <guid>https://dev.to/kenimo49/qwen-code-vs-aider-vs-opencode-i-ran-the-same-12-tasks-through-3-local-cli-agents-on-one-rtx-4070-378i</guid>
      <description>&lt;p&gt;I pointed three coding CLIs at the same local model and ran twelve tasks through each one. The model was Qwen 35B, hosted on an RTX 4070 12GB via &lt;code&gt;llama-server&lt;/code&gt; with &lt;code&gt;--cpu-moe&lt;/code&gt;. The tasks were mundane: build a hello.py, add a CLI flag to an existing script, write a failing test then fix it, refactor a Python file to split a class into two. Nothing you would not do on a Tuesday.&lt;/p&gt;

&lt;p&gt;The results were not what I expected from reading each tool's README.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three tools, in one line each
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Aider&lt;/strong&gt;: git-native pair-programming CLI (45k stars, Apache-2.0), most-used terminal option, mature and well-documented, handles repo-wide context through a repo map.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Qwen Code CLI&lt;/strong&gt;: QwenLM's official fork of Gemini CLI (Apache-2.0), positioned as the "native" agent for Qwen models. Tested v0.18.1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenCode&lt;/strong&gt;: MIT, provider-agnostic, 170k+ stars in 2026, not owned by any of the companies buying up the rest of this list.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All three claim to work with local models via Ollama, LM Studio, or an OpenAI-compatible endpoint. Two of them do. The third has a specific requirement nobody documented up front.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one non-obvious requirement: Qwen Code needs &lt;code&gt;--jinja&lt;/code&gt; and 32k context
&lt;/h2&gt;

&lt;p&gt;I tried Qwen Code with Ollama first. This is the shape the tool's own docs suggest. Here is what happened across five configurations of the &lt;em&gt;same&lt;/em&gt; task ("create hello.py"):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Try&lt;/th&gt;
&lt;th&gt;Backend&lt;/th&gt;
&lt;th&gt;Context&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Ollama 9b (ping only)&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;OK — text response&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Ollama 9b (agent mode)&lt;/td&gt;
&lt;td&gt;default&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Fail&lt;/strong&gt; — tool did not fire, model returned code as prose&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Ollama 35B (agent mode)&lt;/td&gt;
&lt;td&gt;default&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Fail&lt;/strong&gt; — model emitted raw &lt;code&gt;&amp;lt;tools&amp;gt;&lt;/code&gt; text, no file written&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;llama-server --jinja&lt;/td&gt;
&lt;td&gt;8192&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Fail&lt;/strong&gt; — context overflow (19,374 &amp;gt; 8192)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;llama-server --jinja&lt;/td&gt;
&lt;td&gt;32768&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;OK&lt;/strong&gt; — file created, task completed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Only the last combination worked. Ollama's default template does not agree with what Qwen Code sends. The model spits raw &lt;code&gt;&amp;lt;tools&amp;gt;&lt;/code&gt; markers as text instead of emitting a tool call. Switching to &lt;code&gt;llama-server --jinja&lt;/code&gt; uses the model's own chat template and the tool call round-trips correctly.&lt;/p&gt;

&lt;p&gt;The 8k-to-32k jump is the second surprise. The error was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;400: request (19374 tokens) exceeds the available context size (8192 tokens)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nineteen thousand tokens. Before I typed a single character. That is the size of Qwen Code's system prompt plus tool definitions.&lt;/p&gt;

&lt;p&gt;Aider's equivalent first prompt was about 1,400 tokens. Roughly a 13× difference. Qwen Code carries a lot more agent scaffolding by default. SubAgents, four approval modes (Plan / Default / Auto-Edit / YOLO), headless mode, background agent forks. Each of these needs to be described in the system prompt. It adds up.&lt;/p&gt;

&lt;p&gt;Here is the launch command that actually works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;llama-server &lt;span class="nt"&gt;-m&lt;/span&gt; qwen35.gguf &lt;span class="nt"&gt;-ngl&lt;/span&gt; 99 &lt;span class="nt"&gt;--cpu-moe&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; 32768 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--jinja&lt;/span&gt; &lt;span class="nt"&gt;--alias&lt;/span&gt; qwen35 &lt;span class="nt"&gt;--host&lt;/span&gt; 0.0.0.0 &lt;span class="nt"&gt;--port&lt;/span&gt; 18080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--jinja&lt;/code&gt; is the load-bearing flag. Without it the tool loop breaks. &lt;code&gt;-c 32768&lt;/code&gt; is the second one. &lt;code&gt;--cpu-moe&lt;/code&gt; is how a 35B model fits on 12GB VRAM. The MoE experts get offloaded to CPU RAM.&lt;/p&gt;

&lt;p&gt;Aider and OpenCode both worked with Ollama out of the box. No template flags, no context size gymnastics. Aider used &lt;code&gt;aider --model ollama/qwen3-coder&lt;/code&gt;. OpenCode used its &lt;code&gt;models.dev&lt;/code&gt; config with a local endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 12-task grid
&lt;/h2&gt;

&lt;p&gt;I ran each tool through the same twelve tasks. I did not tune prompts per tool. Same wording, same fresh working directory, same model. Here is the shape of what happened:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Aider&lt;/th&gt;
&lt;th&gt;Qwen Code&lt;/th&gt;
&lt;th&gt;OpenCode&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1. hello.py from scratch&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. Add &lt;code&gt;--verbose&lt;/code&gt; flag to existing script&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. Write failing pytest for a stub function&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4. Make the failing test pass&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5. Split a 200-line class into two files&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;Partial*&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6. Rename a symbol across 4 files&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7. Add type hints to an untyped function&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8. Migrate a &lt;code&gt;requests&lt;/code&gt; call to &lt;code&gt;httpx&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;Partial†&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9. Write a Dockerfile for a Flask app&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10. Debug an off-by-one in a for-loop&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;Fail‡&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11. Add a Makefile target&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12. Extract a magic number into a constant&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;*OpenCode split the class but left one method import unresolved. I fixed it in about 30 seconds.&lt;br&gt;
†Qwen Code migrated the call but did not update the exception handler.&lt;br&gt;
‡Qwen Code proposed the fix in prose but did not apply it. This happened when I was in Plan mode by accident.&lt;/p&gt;

&lt;p&gt;Success rates: Aider 12/12, OpenCode 11/12 (1 partial), Qwen Code 10/12 (1 partial, 1 failed).&lt;/p&gt;

&lt;p&gt;That is not a real benchmark. It is one afternoon on one machine with one model. But the failures were not random. Qwen Code's partial and fail both came from cases where the model needed to reason about existing code across multiple edits. Aider's repo map earns its keep here. It gives the model a much better mental picture of the codebase before the edit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Time and token cost
&lt;/h2&gt;

&lt;p&gt;The real cost story is less about success rate and more about how each tool spends the context window you gave it.&lt;/p&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;Aider&lt;/th&gt;
&lt;th&gt;Qwen Code&lt;/th&gt;
&lt;th&gt;OpenCode&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Avg tokens per task (system + first turn)&lt;/td&gt;
&lt;td&gt;~1,400&lt;/td&gt;
&lt;td&gt;~19,400&lt;/td&gt;
&lt;td&gt;~4,800&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Avg wall time per task&lt;/td&gt;
&lt;td&gt;44s&lt;/td&gt;
&lt;td&gt;71s&lt;/td&gt;
&lt;td&gt;52s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Config effort to first success&lt;/td&gt;
&lt;td&gt;5 min&lt;/td&gt;
&lt;td&gt;45 min&lt;/td&gt;
&lt;td&gt;10 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Notable strengths&lt;/td&gt;
&lt;td&gt;Repo map, git integration&lt;/td&gt;
&lt;td&gt;Approval modes, SubAgents, headless&lt;/td&gt;
&lt;td&gt;Provider-agnostic, quick setup&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Qwen Code's per-task time is inflated by the larger system prompt processing on every turn. When you are running locally on a 12GB card the difference matters, because the model has to re-read that 19k every time you send a new message. On cloud LLMs where prompt caching is on by default this evaporates. On llama-server without KV cache reuse across sessions, it does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one goes on the RTX 4070 rig
&lt;/h2&gt;

&lt;p&gt;For local, on-a-single-card, all-my-code-stays-here: Aider. It is the smallest system prompt, the highest first-try success rate on my tasks, and it is the easiest to get working with Ollama or llama-server. If the reason you are running local is "I do not want my code leaving this machine," Aider is the lowest-friction way to get there.&lt;/p&gt;

&lt;p&gt;If you want more agent scaffolding than Aider gives you (approval modes, headless runs for CI, background forks), Qwen Code is where those features live. You pay for it with the 19k-token overhead per turn and the setup complexity. On a cloud LLM this is a non-issue and Qwen Code looks great. On local hardware it costs you 20-30 seconds per task.&lt;/p&gt;

&lt;p&gt;OpenCode is the "I want to swap providers freely" answer. Its provider-agnostic model config is the cleanest of the three. If you are going to move between local Qwen, Cloud Claude, and API Gemini depending on the task, OpenCode makes that swap a one-line config change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ollama's free tier for Qwen Code is gone in 2026
&lt;/h2&gt;

&lt;p&gt;One footnote nobody mentions: Qwen Code CLI used to have a hosted free tier from QwenLM directly. It was retired on April 15, 2026. This matters not at all if you are running local, because you connect to your own llama-server and the retirement is invisible. If you were using the hosted endpoint, you need to either self-host or pay per token now.&lt;/p&gt;

&lt;p&gt;For anyone reading this because they are trying to decide "cloud API or local for my coding assistant," the retirement moved the local option up the ranking. Cost of running Qwen 35B on your own hardware: your electricity bill. Cost of the equivalent number of tokens on any hosted API: not that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "agentic" actually means, honestly
&lt;/h2&gt;

&lt;p&gt;One thing I want to say clearly because the tools' marketing does not: none of these three do what you might call open-ended goal pursuit. They do not sit in a loop, evaluate whether they hit the goal, and keep going if they missed. What they have is a tool-use loop &lt;em&gt;within a single task&lt;/em&gt;. You give it a task. It calls tools until it decides the task is done. Then it stops.&lt;/p&gt;

&lt;p&gt;Qwen Code's approval modes and budget controls make that single-task loop safer to run long. They do not make it a self-directed agent. Same for Aider's &lt;code&gt;--yes-always&lt;/code&gt; mode. Same for OpenCode's daemon mode. All three are agents in the "uses tools to accomplish a task" sense. None of them are agents in the "pursues a goal indefinitely" sense. If your project depends on the second interpretation, you are looking at a different kind of harness.&lt;/p&gt;

&lt;p&gt;This distinction saved me from a bad architecture choice, and I have not seen it stated plainly in the three tools' docs, so I am stating it plainly here.&lt;/p&gt;

&lt;p&gt;The reason I started this comparison was that I use Claude Code as my daily driver on cloud, and I wanted to know what the local-only story looks like when I take Claude Code out of the loop. The mechanics of running a code agent well — tool call design, approval modes, when to hand off to a subagent — apply across all four tools, and I wrote them up here: &lt;a href="https://kenimoto.dev/books/claude-code-mastery?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=local-cli-3-way" rel="noopener noreferrer"&gt;Claude Code Mastery&lt;/a&gt;. If Claude Code is your baseline, this book is the "how to actually use it" playbook; the local-agent comparison above is what you fall back to when you cannot ship your code to a cloud provider.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>ai</category>
      <category>python</category>
      <category>productivity</category>
    </item>
    <item>
      <title>What was your win this week?</title>
      <dc:creator>Jess Lee</dc:creator>
      <pubDate>Fri, 07 Aug 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/devteam/what-was-your-win-this-week-3n23</link>
      <guid>https://dev.to/devteam/what-was-your-win-this-week-3n23</guid>
      <description>&lt;p&gt;👋👋👋👋&lt;br&gt;
Looking back on your week -- what was something you're proud of?&lt;br&gt;
All wins count -- big or small 🎉&lt;/p&gt;

&lt;p&gt;Examples of 'wins' include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Getting a promotion!&lt;/li&gt;
&lt;li&gt;Starting a new project&lt;/li&gt;
&lt;li&gt;Fixing a tricky bug&lt;/li&gt;
&lt;li&gt;Started something you've been "about to start" for two months 🏁 &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Happy Friday!&lt;/p&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%2Fdjsemzao5sw2pkppkvma.gif" 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%2Fdjsemzao5sw2pkppkvma.gif" alt="A person with shoulder length gray hair and thick rimmed glasses cracks their knuckles with the caption " width="388" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>weeklyretro</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Qwen3 4B vs 8B vs 14B for Writing Correction: 60 Local Ollama Responses on Windows</title>
      <dc:creator>Sami</dc:creator>
      <pubDate>Fri, 07 Aug 2026 12:56:05 +0000</pubDate>
      <link>https://dev.to/linguapilot-ai/qwen3-4b-vs-8b-vs-14b-for-writing-correction-60-local-ollama-responses-on-windows-3cmg</link>
      <guid>https://dev.to/linguapilot-ai/qwen3-4b-vs-8b-vs-14b-for-writing-correction-60-local-ollama-responses-on-windows-3cmg</guid>
      <description>&lt;p&gt;I expected the larger Qwen3 model to show a clear advantage for writing correction. In this experiment, it didn't.&lt;/p&gt;

&lt;p&gt;Across 20 paired writing cases, Qwen3 8B and 14B both achieved &lt;strong&gt;19/20 complete-case corrections&lt;/strong&gt;, while Qwen3 4B reached &lt;strong&gt;18/20&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The difference became much more noticeable in local execution time: in the tested cold-start setup, the mean cold-start time for Qwen3 8B was roughly half that of Qwen3 14B.&lt;/p&gt;

&lt;p&gt;That raised a practical question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;For a local writing assistant, how much model size do we actually need?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To explore this more systematically, I built a reproducible benchmark comparing Qwen3 4B, 8B and 14B under the same local conditions with Ollama on Windows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benchmark setup
&lt;/h2&gt;

&lt;p&gt;The benchmark uses 20 paired writing cases, including 16 sentences with expected errors and 4 already-correct controls.&lt;/p&gt;

&lt;p&gt;Each case was evaluated with the same reference and the same evaluation framework across three locally hosted Qwen3 models:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;qwen3:4b-q4_K_M&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;qwen3:8b-q4_K_M&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;qwen3:14b-q4_K_M&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This produced &lt;strong&gt;60 local responses in total&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The models were executed through &lt;strong&gt;Ollama on Windows&lt;/strong&gt; under the same test conditions. The objective was not to identify a universal “best model,” but to compare several dimensions separately:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;complete correction of the expected errors;&lt;/li&gt;
&lt;li&gt;localization of the error regions;&lt;/li&gt;
&lt;li&gt;preservation of the original meaning;&lt;/li&gt;
&lt;li&gt;compliance with the requested output structure;&lt;/li&gt;
&lt;li&gt;quality of the explanations;&lt;/li&gt;
&lt;li&gt;local execution time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the primary metric discussed here, a case was considered complete only when the expected correction requirements for that case were satisfied.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Complete-case correction
&lt;/h3&gt;

&lt;p&gt;The primary correction results were close:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Qwen3 4B:&lt;/strong&gt; 18/20 complete cases — &lt;strong&gt;90%&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Qwen3 8B:&lt;/strong&gt; 19/20 complete cases — &lt;strong&gt;95%&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Qwen3 14B:&lt;/strong&gt; 19/20 complete cases — &lt;strong&gt;95%&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On this metric alone, increasing the model size from 8B to 14B did not improve the number of complete cases in this test.&lt;/p&gt;

&lt;h3&gt;
  
  
  Local execution time
&lt;/h3&gt;

&lt;p&gt;The difference was much larger in execution time.&lt;/p&gt;

&lt;p&gt;In the tested cold-start configuration, the mean cold-start times were approximately:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Qwen3 4B:&lt;/strong&gt; 26.6 s&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Qwen3 8B:&lt;/strong&gt; 60.0 s&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Qwen3 14B:&lt;/strong&gt; 119.4 s&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, while 8B and 14B achieved the same complete-case score, &lt;strong&gt;Qwen3 8B took about half as long as Qwen3 14B on average in this cold-start setup.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The 4B model was substantially faster again, while still completing 18 of the 20 cases.&lt;/p&gt;

&lt;p&gt;This does not mean that 8B is universally better than 14B. It means that, for this specific writing-correction workload and hardware configuration, the additional model size did not translate into a better primary correction score, while the execution-time cost increased considerably.&lt;/p&gt;

&lt;h2&gt;
  
  
  Error localization and explanation quality
&lt;/h2&gt;

&lt;p&gt;Complete-case correction and error localization measure different aspects of model behavior.&lt;/p&gt;

&lt;p&gt;Across the 21 expected error regions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Qwen3 4B&lt;/strong&gt; missed one expected region, with an error-localization F1 of &lt;strong&gt;97.6%&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Qwen3 8B&lt;/strong&gt; localized all 21 expected regions, reaching &lt;strong&gt;100% F1&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Qwen3 14B&lt;/strong&gt; missed one expected region and reported one false-positive region, resulting in &lt;strong&gt;95.2% F1&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This distinction matters for a writing coach: producing an acceptable corrected sentence does not necessarily mean that the model identified the underlying error with the same precision.&lt;/p&gt;

&lt;p&gt;Explanation quality was therefore evaluated separately from correction success.&lt;/p&gt;

&lt;p&gt;In this run, &lt;strong&gt;all 41 evaluated explanation fields produced by Qwen3 8B passed the language check&lt;/strong&gt;. Qwen3 4B showed more explanation-language and terminology issues.&lt;/p&gt;

&lt;p&gt;These automatic checks should not be interpreted as a complete measure of pedagogical quality. They cover selected language patterns and contradictions, while deeper explanation quality remains a separate area for linguistic review.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means for local writing assistants
&lt;/h2&gt;

&lt;p&gt;The main takeaway is not that one model is universally better than another.&lt;/p&gt;

&lt;p&gt;In this benchmark, the three Qwen3 models produced relatively similar complete-case correction results, but their local execution times differed much more substantially.&lt;/p&gt;

&lt;p&gt;For this specific workload:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;4B&lt;/strong&gt; offered the lowest execution time, with a small reduction in correction coverage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;8B&lt;/strong&gt; matched 14B on complete-case correction and achieved the strongest error-localization result in this run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;14B&lt;/strong&gt; did not improve the primary correction score over 8B, while requiring substantially more execution time in the tested configuration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This suggests that model selection for a local writing assistant should be treated as a &lt;strong&gt;multi-criteria decision&lt;/strong&gt;, rather than assuming that a larger model will automatically provide the best practical experience.&lt;/p&gt;

&lt;p&gt;Depending on the use case and available hardware, the relevant trade-off may involve:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;correction reliability;&lt;/li&gt;
&lt;li&gt;explanation quality;&lt;/li&gt;
&lt;li&gt;latency;&lt;/li&gt;
&lt;li&gt;memory and compute requirements;&lt;/li&gt;
&lt;li&gt;and the expected frequency of use.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a writing coach in particular, responsiveness also matters. A model that is slightly larger or theoretically more capable may not necessarily provide the best user experience if each interaction becomes significantly slower.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;p&gt;This benchmark is exploratory and should be interpreted within its experimental scope.&lt;/p&gt;

&lt;p&gt;Several limitations are important:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;20 paired cases:&lt;/strong&gt; the dataset is intentionally focused and is not large enough to support a universal ranking of the models.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One language scenario:&lt;/strong&gt; the benchmark evaluates English writing correction with explanations requested in French. Results may differ for other languages or writing tasks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One model family:&lt;/strong&gt; only Qwen3 4B, 8B and 14B were compared. The results should not be generalized to other local model families.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One local machine:&lt;/strong&gt; execution time depends on hardware, model quantization, available memory and Ollama configuration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cold-start timing:&lt;/strong&gt; Ollama was configured with &lt;code&gt;keep_alive=0&lt;/code&gt;, meaning the model was reloaded for each response. The reported timings therefore represent this specific cold-start workflow rather than warm-start interaction latency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reference-based evaluation:&lt;/strong&gt; outputs were compared against a frozen, versioned correction reference prepared for this study. This improves consistency across models, but it does not capture every possible acceptable linguistic formulation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explanation-quality checks:&lt;/strong&gt; automatic checks cover selected language patterns and contradictions. They should not be treated as a complete assessment of pedagogical or linguistic quality.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These limitations are also why the benchmark is presented as a controlled comparison rather than a claim about the absolute capabilities of each model.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I plan to test next
&lt;/h2&gt;

&lt;p&gt;This first benchmark leaves several useful directions open for further evaluation.&lt;/p&gt;

&lt;p&gt;The next iterations could extend the framework in four directions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;compare additional local model families under the same evaluation protocol;&lt;/li&gt;
&lt;li&gt;examine explanation quality with deeper linguistic and human review;&lt;/li&gt;
&lt;li&gt;compare cold-start and warm-start performance;&lt;/li&gt;
&lt;li&gt;evaluate the same workflow on more constrained hardware.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A larger and more diverse test set would also help determine whether the patterns observed here remain stable beyond these 20 cases.&lt;/p&gt;

&lt;p&gt;I am particularly interested in whether the 8B/14B trade-off observed in this run persists when the task becomes more linguistically demanding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;This experiment did not produce a universal winner, but it did highlight an important practical point: &lt;strong&gt;model size alone is not a sufficient criterion for choosing a local writing model&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For this specific workload, Qwen3 8B matched 14B on complete-case correction while its mean cold-start time was roughly half that of 14B in the tested configuration.&lt;/p&gt;

&lt;p&gt;The more useful question, therefore, may not be “Which model is the largest?” but rather:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Which model provides the best balance between correction quality, explanation quality, responsiveness, and available hardware for the intended use case?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you use local LLMs for writing assistance or other interactive tasks, I would be interested to know which trade-off matters most in your own setup: &lt;strong&gt;quality, latency, memory requirements, or something else?&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: AI assistance was used to refine the wording and organization of this article. The benchmark results and measurements reported here come from the documented experimental runs and were reviewed for accuracy before publication.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>qwen</category>
      <category>ollama</category>
      <category>llm</category>
    </item>
  </channel>
</rss>
