<?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: Google Developer Group</title>
    <description>The latest articles on DEV Community by Google Developer Group (@gdg).</description>
    <link>https://dev.to/gdg</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F12748%2Fe3cbcad3-4749-4461-ad88-4b9b8cde89ec.png</url>
      <title>DEV Community: Google Developer Group</title>
      <link>https://dev.to/gdg</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gdg"/>
    <language>en</language>
    <item>
      <title>Architecture Documentation as a First-Class Engineering Asset</title>
      <dc:creator>Alexander Tyutin</dc:creator>
      <pubDate>Thu, 16 Apr 2026 09:49:24 +0000</pubDate>
      <link>https://dev.to/gdg/architecture-documentation-as-a-first-class-engineering-asset-4a1j</link>
      <guid>https://dev.to/gdg/architecture-documentation-as-a-first-class-engineering-asset-4a1j</guid>
      <description>&lt;p&gt;&lt;em&gt;How autonomous AI agents can generate a complete architecture snapshot of your microservices platform - while you do push-ups - and why that documentation becomes the most powerful input for your AI-driven quality pipeline.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Architectural documentation is not a chore. When colocated with your source code and fed into an AI-powered quality pipeline, it transforms static analysis from "catching typos" into "catching systemic security failures and costly infrastructure leaks." This article documents a real experiment where an autonomous AI agent generated architecture files across a multi-service Google Cloud platform - with the human engineer largely off-screen - and what happened when that documentation gave our AI Quality Gate an entirely new perspective.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  1. The "Self-Documenting Code" Problem
&lt;/h2&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.amazonaws.com%2Fuploads%2Farticles%2Fg1g9511rcfcdd3b5i8eb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg1g9511rcfcdd3b5i8eb.png" alt="Self-Documented says nothing about it in reality" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There is a persistent assumption in software engineering that well-structured code is self-explanatory. Clean functions, good variable names, and a Pylint score of 10.0/10 - surely that's enough?&lt;/p&gt;

&lt;p&gt;It is not.&lt;/p&gt;

&lt;p&gt;Code describes &lt;em&gt;how&lt;/em&gt; a system executes. Architecture documentation describes &lt;em&gt;why&lt;/em&gt; a system exists and &lt;em&gt;how&lt;/em&gt; it interacts with everything around it. Without this context layer, every automated analysis tool is operating in the dark. It sees a function, but not its role in the broader service mesh. It sees an API call, but not the security boundary it is expected to enforce.&lt;/p&gt;

&lt;p&gt;This distinction matters enormously when you introduce AI-powered tools into your engineering workflow. An LLM analyzing raw code without architectural context is like asking a senior engineer to perform a security review without access to the system design.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Generating Architecture While Doing Push-ups
&lt;/h2&gt;

&lt;p&gt;My platform runs on Google Cloud. It consists of dozens of microservices deployed on &lt;strong&gt;Cloud Run&lt;/strong&gt;, interacting via REST APIs, persisting assets to &lt;strong&gt;Google Cloud Storage&lt;/strong&gt;, and routing all AI operations through a centralized &lt;strong&gt;Vertex AI&lt;/strong&gt; gateway. A rich, well-connected system - but one where the only documentation was spread across scattered README files.&lt;/p&gt;

&lt;p&gt;I set out to change that. The goal: a standardized, machine-readable architectural snapshot for every service, committed directly to the repository.&lt;/p&gt;

&lt;p&gt;The method: &lt;strong&gt;guided autonomous agent execution&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The engineer set a direction, established the documentation standard, and then stepped back. The AI agent - powered by &lt;strong&gt;Gemini 3 Flash&lt;/strong&gt; and &lt;strong&gt;Claude Sonnet 4.6&lt;/strong&gt; running inside &lt;a href="https://antigravity.dev" rel="noopener noreferrer"&gt;Antigravity&lt;/a&gt;, an agentic AI coding assistant - took over. It autonomously inspected each service, read the source code, traced inter-service dependencies, cross-referenced existing implementations against the documentation standard, and iteratively generated structured &lt;code&gt;ARCHITECTURE.md&lt;/code&gt; files. The engineer's main activity during most of this process was physical exercise.&lt;/p&gt;

&lt;p&gt;The output was not informal notes. It was a disciplined, multi-level documentation hierarchy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;📦 platform-root
 ┣ 📜 ARCHITECTURE.md           ← Level 0: Global service mesh, topology, lifecycle status
 ┗ 📂 services
    ┣ 📂 core-ai-gateway
    ┃  ┗ 📜 ARCHITECTURE.md     ← Level 1: Security policy engine, FinOps guardrails
    ┣ 📂 orchestration-bot
    ┃  ┗ 📜 ARCHITECTURE.md     ← Level 1: Async task flow, Telegram webhook handling
    ┣ 📂 media-transcriber
    ┃  ┗ 📜 ARCHITECTURE.md     ← Level 1: Speech-to-Text pipeline, GCS asset management
    ┗ 📂 translation-engine
       ┗ 📜 ARCHITECTURE.md     ← Level 1: Structured output, multilingual routing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each document followed a strict template:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Intent&lt;/strong&gt;: The concrete business and technical reason this service exists.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Design Principles&lt;/strong&gt;: Key trade-offs - statelessness, latency targets, fallback strategies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interaction Diagram&lt;/strong&gt;: A &lt;a href="https://mermaid.js.org/" rel="noopener noreferrer"&gt;Mermaid&lt;/a&gt; graph of service-to-service flows, security boundaries, and AI provider integrations. It may be generated by the agent and automatically drawn in Gitlab.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM Context Block&lt;/strong&gt;: A precise summary optimized for consumption by automated agents and AI reviewers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The entire operation resulted in a navigable, cross-linked architecture map - built with minimal human cognitive effort (and with visualizations!)&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.amazonaws.com%2Fuploads%2Farticles%2F0pih6vrmdnzxfh9lj650.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0pih6vrmdnzxfh9lj650.png" alt="Mermaid Diagram Generated by Antigravity Agent" width="800" height="723"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The Quality Gate Awakening
&lt;/h2&gt;

&lt;p&gt;Once the documentation was committed alongside the source code, I ran a standard CI quality review using our AI-powered &lt;strong&gt;Quality Gate&lt;/strong&gt; - a service built on top of &lt;strong&gt;Gemini via Vertex AI&lt;/strong&gt;, designed to perform automated architectural and security reviews on every merge request.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💡 What is the Quality Gate, exactly?&lt;/strong&gt;&lt;br&gt;
It is not a $100,000 enterprise SaaS platform. It is a lightweight, purpose-built microservice - part of the same platform it reviews - deployed on &lt;strong&gt;Google Cloud Run&lt;/strong&gt;. It exposes a single endpoint, receives the merge request diff from the CI pipeline, constructs an LLM prompt enriched with the repository's architectural documentation, calls &lt;strong&gt;Vertex AI (Gemini)&lt;/strong&gt;, and returns a structured JSON review report.&lt;/p&gt;

&lt;p&gt;Because it runs on Cloud Run, it starts only when a review is triggered and shuts down immediately after. &lt;strong&gt;The total monthly cost for me is a few dollars&lt;/strong&gt; - a fraction of a single human code review hour. This is a practical demonstration of the Google Cloud serverless model: pay only for the compute you actually use, and use high-intelligence AI only when it adds value.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The difference was immediately visible.&lt;/p&gt;

&lt;p&gt;Previously, without architectural context, the Quality Gate was limited to code-level analysis: style consistency, common security anti-patterns, dependency versions. Useful, but shallow.&lt;/p&gt;

&lt;p&gt;With the &lt;code&gt;ARCHITECTURE.md&lt;/code&gt; files available as context, the model could see the architecture and the code simultaneously. The result was a qualitative leap: the Quality Gate shifted from a static analysis tool into a reasoning system operating at the level of system design.&lt;/p&gt;

&lt;p&gt;It identified two critical issues within minutes - issues that had existed undetected in the codebase for months.&lt;/p&gt;




&lt;h3&gt;
  
  
  Finding 1: The Distributed Tracing Blackout
&lt;/h3&gt;

&lt;p&gt;One of our routing services included middleware that explicitly stripped incoming trace headers. On the surface, this looked like a reasonable security measure to prevent external clients from injecting trace identifiers into internal systems.&lt;/p&gt;

&lt;p&gt;The Quality Gate identified it as a critical observability violation.&lt;/p&gt;

&lt;p&gt;Because the architectural documentation described the distributed tracing standard across the mesh - including the requirement for end-to-end &lt;code&gt;X-Trace-ID&lt;/code&gt; propagation compatible with &lt;strong&gt;Google Cloud Trace&lt;/strong&gt; - the model understood that stripping these headers at the boundary did not isolate a threat. It severed the trace chain entirely. In any production incident, engineers would be unable to correlate logs across services in &lt;strong&gt;Cloud Logging&lt;/strong&gt;, turning a routine debugging session into a multi-hour forensic investigation with no &lt;strong&gt;Cloud Audit Logs&lt;/strong&gt; correlation to lean on.&lt;/p&gt;

&lt;p&gt;Security intention ✓. Systemic consequence ✗. The documentation made this contradiction visible.&lt;/p&gt;




&lt;h3&gt;
  
  
  Finding 2: The Silent Storage Leak
&lt;/h3&gt;

&lt;p&gt;A media processing service was documented as intentionally skipping cleanup of temporary assets in Google Cloud Storage after each processing job. The rationale was implicit - simplicity, no failure modes from deletion errors.&lt;/p&gt;

&lt;p&gt;The Quality Gate cross-referenced this against the documented architectural principle of data minimization and least-privilege access, and flagged it as both a security and FinOps violation.&lt;/p&gt;

&lt;p&gt;The impact: user audio files - potentially containing sensitive personal information - accumulating indefinitely in cloud storage. No lifecycle policy. No deletion trigger. Silent, compounding cost growth. An expanding attack surface with each new processing request.&lt;/p&gt;

&lt;p&gt;Neither a linter nor a code reviewer scanning functions in isolation would have flagged either of these. Both findings emerged from the intersection of code behavior and architectural intent - visible only because the documentation existed.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. The ROI Case
&lt;/h2&gt;

&lt;p&gt;This experiment produced a measurable return on investment across three dimensions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Without Documentation&lt;/th&gt;
&lt;th&gt;With Documentation + AI Agent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Architecture Capture&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Senior Architect hours&lt;/td&gt;
&lt;td&gt;Agent cycle, near-zero human effort&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Review Quality&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Code-level findings&lt;/td&gt;
&lt;td&gt;System-level and policy findings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Issue Discovery Cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Post-incident or audit&lt;/td&gt;
&lt;td&gt;CI/CD pipeline (minutes, pennies)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Quality Gate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Generic, rigid enterprise tool&lt;/td&gt;
&lt;td&gt;Custom microservice, tunable per team or developer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three additional factors are worth noting specifically in the context of &lt;strong&gt;Google Cloud&lt;/strong&gt; platforms:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Vertex AI Token Efficiency&lt;/strong&gt;: When the Quality Gate is backed by a Gemini model, providing a structured &lt;code&gt;ARCHITECTURE.md&lt;/code&gt; reduces the tokens the model spends reconstructing system intent from raw code. Better context means cheaper, faster, and more accurate generation - directly impacting your AI compute costs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cloud Run Observability&lt;/strong&gt;: The distributed tracing finding described above is particularly relevant for Cloud Run-based architectures, where services are stateless and ephemeral. Without continuous trace propagation, debugging inter-service failures on Cloud Run becomes significantly harder. The documentation made this risk explicit and catchable.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Serverless Cost Model&lt;/strong&gt;: Because the Quality Gate is a Cloud Run service invoked only during CI/CD runs, there is zero idle cost. On a typical team with several merge requests per day, the entire AI-powered review pipeline costs a few dollars per month - less than a single engineering hour. This is the Google Cloud serverless model working exactly as intended: high-intelligence compute, on-demand, at minimal cost.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  5. Lessons for Platform Engineers
&lt;/h2&gt;

&lt;p&gt;The key insight from this experiment is not that AI agents write documentation faster than humans. That is expected. The key insight is that &lt;strong&gt;architecture documentation living inside the repository is a force multiplier for every automated tool that reads it&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This applies whether your automated tools are AI-powered code reviewers, compliance scanners, onboarding assistants, or infrastructure planning agents. The better the documentation, the higher the signal quality of every tool operating on top of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Practical recommendations:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Colocate documentation with code.&lt;/strong&gt; A separate wiki that drifts out of sync is noise. An &lt;code&gt;ARCHITECTURE.md&lt;/code&gt; in the service directory, updated in the same commit as the code, is signal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Establish a documentation standard.&lt;/strong&gt; A consistent template (Intent, Principles, Interaction Diagram) makes documentation machine-readable, not just human-readable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Define a lifecycle status.&lt;/strong&gt; Clearly mark deprecated or inactive services. Automated agents should not use legacy code as a reference for current standards.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use agents to generate the initial draft.&lt;/strong&gt; The cognitive overhead of starting from a blank page is real. Agents are excellent at producing a structured first pass that engineers then validate and refine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feed documentation to your CI pipeline.&lt;/strong&gt; An AI quality reviewer with architectural context is a different class of tool than one without it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build your own Quality Gate - and make it yours.&lt;/strong&gt; This is the key advantage that enterprise SaaS cannot match: flexibility. A custom Cloud Run service backed by Gemini and driven by &lt;em&gt;your&lt;/em&gt; compliance rules, &lt;em&gt;your&lt;/em&gt; architectural standards, and &lt;em&gt;your&lt;/em&gt; team conventions means every developer can have a personal reviewer that understands the exact context of the project - not a generic ruleset designed for the average of all possible codebases.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  6. Conclusion
&lt;/h2&gt;

&lt;p&gt;Architecture documentation has historically been treated as optional overhead - valuable in theory, deprioritized in practice. This experiment demonstrates that when documentation is colocated with source code, follows a consistent machine-readable standard, and is kept current with the help of autonomous agents, it becomes a critical infrastructure component.&lt;/p&gt;

&lt;p&gt;It enables automated systems to reason at the level of platform design, not just code syntax. It transforms AI-powered quality gates from expensive linters into genuine architectural advisors. And it can be generated - for an entire platform - while you are doing something else entirely.&lt;/p&gt;

&lt;p&gt;The $10,000 &lt;code&gt;ARCHITECTURE.md&lt;/code&gt; is not a metaphor. It is the estimated cost differential between finding a critical architectural flaw in a 5-minute CI review versus discovering it during a production incident, a compliance audit, or a cloud storage invoice that nobody expected.&lt;/p&gt;

&lt;p&gt;Keep your architecture documented. Keep it in the repository. Let agents maintain it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Stay standardized. Stay secure.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>security</category>
      <category>ai</category>
      <category>agents</category>
    </item>
    <item>
      <title>Using OpenCode as a fallback agent for Antigravity</title>
      <dc:creator>Alexander Tyutin</dc:creator>
      <pubDate>Wed, 15 Apr 2026 10:45:53 +0000</pubDate>
      <link>https://dev.to/gdg/using-opencode-as-a-fallback-agent-for-antigravity-37oo</link>
      <guid>https://dev.to/gdg/using-opencode-as-a-fallback-agent-for-antigravity-37oo</guid>
      <description>&lt;p&gt;Today I was confused by Antigravity errors about high load on their services. It made my work impossible even with the cheapest model Gemini 3 Flash.&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.amazonaws.com%2Fuploads%2Farticles%2Ff4mtae79mx16mj8oqolw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff4mtae79mx16mj8oqolw.png" alt="Our servers are experiencing high traffic right now, please try again in a minute" width="680" height="474"&gt;&lt;/a&gt;&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.amazonaws.com%2Fuploads%2Farticles%2Faoivttyhrl7cic2pqa53.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faoivttyhrl7cic2pqa53.png" alt="Gemini 3 Flash is not working" width="674" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Some time ago I heard something about the &lt;a href="https://opencode.a" rel="noopener noreferrer"&gt;OpenCode&lt;/a&gt;. And it was the time to try it!&lt;/p&gt;

&lt;p&gt;I've installed the opencode in my system by &lt;code&gt;brew install anomalyco/tap/opencode&lt;/code&gt; and respective extension from the marketplace.&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.amazonaws.com%2Fuploads%2Farticles%2Fqepj29llxc7r63pllxux.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqepj29llxc7r63pllxux.png" alt="Opencode extension for Antigravity" width="800" height="344"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I have a good documentation inside the repo like described in the article &lt;a href="https://dev.to/holgerleichsenring/specification-first-agentic-development-a-methodology-for-structured-traceable-ai-assisted-la"&gt;Specification-First Agentic Development: A Methodology for Structured, Traceable AI-Assisted Development&lt;/a&gt;. So the default free OpenCode model &lt;code&gt;Big Pickle&lt;/code&gt; performed planning, reviewing and coding stage well. &lt;/p&gt;

&lt;p&gt;But then I realized that it was working without taking into the account system instruction and rules which I had for Antigravity.&lt;/p&gt;

&lt;p&gt;So I've performed calls of Antigravity assurance workflows (like &lt;a href="https://dev.to/gdg/antigravity-my-approach-to-deliver-the-most-assured-value-for-the-least-money-3iip"&gt;here&lt;/a&gt; and &lt;a href="https://dev.to/gdg/ai-powered-repository-security-check-with-antigravity-workflow-5hee"&gt;here&lt;/a&gt;) right from the OpenCode chat and it performed them perfectly.&lt;/p&gt;

&lt;p&gt;As I have a lot of workflows for linting, security check of diff and the whole repo, and especially external self-made security gateway I was sure that the quality of code produced by the OpenCode was good enough and aligned with my codebase.&lt;/p&gt;

&lt;p&gt;The only thing I can mention is a redundant file was left after some iterations of testing. But it can be fixed by a good review right after MR creation.&lt;/p&gt;

&lt;p&gt;So seems the OpenCode is a good fallback for cases when Google servers are experiencing problems. Also it can by used to save tokens for some kind of tasks.&lt;/p&gt;

</description>
      <category>antigravity</category>
      <category>development</category>
      <category>ai</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Gemini Thinking: How "Brainy" Models Unexpectedly Blew My Budget</title>
      <dc:creator>Alexander Tyutin</dc:creator>
      <pubDate>Mon, 13 Apr 2026 07:29:03 +0000</pubDate>
      <link>https://dev.to/gdg/gemini-thinking-how-new-brainy-models-unexpectedly-blew-my-budget-1c85</link>
      <guid>https://dev.to/gdg/gemini-thinking-how-new-brainy-models-unexpectedly-blew-my-budget-1c85</guid>
      <description>&lt;p&gt;Recently, Google notified me that the &lt;strong&gt;Gemini 2.0&lt;/strong&gt; models I was using are retiring. This was disappointing because my &lt;a href="https://t.me/oqytu_bot" rel="noopener noreferrer"&gt;charity project for Technovation Girls&lt;/a&gt;, worked perfectly and very cheaply on those models.&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.amazonaws.com%2Fuploads%2Farticles%2Fovhd2tgryyregjlw45ht.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fovhd2tgryyregjlw45ht.png" alt="gemini-2.0 retirement email" width="800" height="569"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I had to find a replacement. While Google recommended &lt;strong&gt;Gemini 3.0&lt;/strong&gt;, those models are still in "preview". Since my project needs high stability, I chose the &lt;strong&gt;Gemini 2.5&lt;/strong&gt; family, which is already in "General Availability".&lt;/p&gt;




&lt;h3&gt;
  
  
  The Surprise: Why is it so Slow and Expensive?
&lt;/h3&gt;

&lt;p&gt;Switching was easy because I built my platform to handle model changes and fallbacks automatically. I simply updated my allowed models list and set &lt;strong&gt;gemini-2.5-flash-lite&lt;/strong&gt; as the primary choice.&lt;/p&gt;

&lt;p&gt;However, I was shocked by the results:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Requests took much longer to finish.&lt;/li&gt;
&lt;li&gt;The quality was barely better.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token usage exploded&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;I saw a massive "system overhead" in my logs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt;&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.amazonaws.com%2Fuploads%2Farticles%2F0du4cn38pcwealhz6w1o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0du4cn38pcwealhz6w1o.png" alt="Tokens usage before" width="262" height="218"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After:&lt;/strong&gt;&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.amazonaws.com%2Fuploads%2Farticles%2Fdm6zfqy0xgnrjlvkjlbt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdm6zfqy0xgnrjlvkjlbt.png" alt="Tokens usage after" width="326" height="250"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  The Cause: "Thinking" by Default
&lt;/h3&gt;

&lt;p&gt;After digging into the documentation, I found the reason: &lt;strong&gt;all Gemini 2.5 models are "thinking" models&lt;/strong&gt;. By default, they use as many tokens as possible to "reason" before answering.&lt;/p&gt;

&lt;p&gt;My project worked great without this extra thinking. The slight quality boost was not worth the massive increase in latency and cost. I had to find a way to stop the model from thinking "on my dime".&lt;/p&gt;

&lt;h3&gt;
  
  
  The Technical Hurdle
&lt;/h3&gt;

&lt;p&gt;I &lt;a href="https://docs.cloud.google.com/vertex-ai/generative-ai/docs/thinking" rel="noopener noreferrer"&gt;discovered&lt;/a&gt; that different models have different minimum "thinking budgets". Surprisingly, &lt;strong&gt;gemini-2.5-flash-lite&lt;/strong&gt; has a higher minimum budget (512 tokens) than the more powerful &lt;strong&gt;gemini-2.5-flash&lt;/strong&gt; (only 1 token!).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Min Thinking Budget&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Gemini 2.5 Flash Lite&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;512 tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Gemini 2.5 Flash&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1 token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Gemini 2.5 Pro&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;128 tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;To fix this, I had to expand my code to calculate and limit these budgets during fallbacks. I also had to handle the new text constants (MINIMAL, MEDIUM, HIGH) used by the Gemini 3.x models.&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini-2.5-flash-lite&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model_page&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;_MODEL_GEMINI_DOCS_BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/gemini/2-5-flash-lite&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_thinking&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;grounding_rag&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;grounding_google_search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;count_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;supports_thinking_level&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;supports_thinking_budget&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;min_thinking_budget&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;outputs&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini-2.5-flash&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model_page&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;_MODEL_GEMINI_DOCS_BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/gemini/2-5-flash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_thinking&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;grounding_rag&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;grounding_google_search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;count_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;supports_thinking_level&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;supports_thinking_budget&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;min_thinking_budget&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;outputs&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini-2.5-pro&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model_page&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;_MODEL_GEMINI_DOCS_BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/gemini/2-5-pro&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_thinking&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;grounding_rag&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;grounding_google_search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;count_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;supports_thinking_level&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;supports_thinking_budget&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;min_thinking_budget&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;outputs&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  The Result
&lt;/h3&gt;

&lt;p&gt;I finally switched to &lt;strong&gt;gemini-2.5-flash&lt;/strong&gt; with a strict limit of &lt;strong&gt;50 thinking tokens&lt;/strong&gt;.&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.amazonaws.com%2Fuploads%2Farticles%2F8f5mqh3wrj6ilmijncq6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8f5mqh3wrj6ilmijncq6.png" alt="Gemini thinking tokens in logs" width="296" height="246"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, response speeds are back up and costs are back down. It was a lot of unexpected work for a "simple" upgrade, but everything is running smoothly again!&lt;/p&gt;

</description>
      <category>gemini</category>
      <category>vertexai</category>
      <category>infrastructure</category>
      <category>finops</category>
    </item>
    <item>
      <title>I tried to make DevFest Ireland accessible - and ended up building a SaaS</title>
      <dc:creator>Jordan Harrison</dc:creator>
      <pubDate>Fri, 10 Apr 2026 13:18:59 +0000</pubDate>
      <link>https://dev.to/gdg/i-tried-to-make-devfest-ireland-accessible-and-ended-up-building-a-saas-1o87</link>
      <guid>https://dev.to/gdg/i-tried-to-make-devfest-ireland-accessible-and-ended-up-building-a-saas-1o87</guid>
      <description>&lt;h2&gt;
  
  
  The email I couldn't ignore
&lt;/h2&gt;

&lt;p&gt;A few months into organising DevFest Ireland 2025, I received messages from a couple of deaf developers asking if they could attend.&lt;/p&gt;

&lt;p&gt;Not in a vague "looks interesting" kind of way. They wanted to come. They wanted to sit in the talks, meet people, be part of it properly. And the question they asked was completely fair: would there be an Irish Sign Language interpreter?&lt;/p&gt;

&lt;p&gt;I didn't have a solid answer for them at the time. I thought it would be one of those things that would take a bit of organising, a few emails, some budget approval, and then get sorted. That was my naive version of it anyway.&lt;/p&gt;

&lt;p&gt;It turned out not to be like that at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trying to make it work
&lt;/h2&gt;

&lt;p&gt;Once I started looking properly, I ran into the shortage almost immediately. There simply are not enough ISL interpreters available, especially for full day events. Availability is tight, booking needs to happen early, and the logistics are harder than most people realise from the outside. For a conference, you are not just solving for one slot in a timetable. You are trying to cover a long day, with the practical reality that this kind of work cannot just be dumped onto one person and expected to somehow stretch across everything.&lt;/p&gt;

&lt;p&gt;That was the point where it stopped feeling like a normal organiser task and started feeling like a structural problem.&lt;/p&gt;

&lt;p&gt;The hardest part was that I now had real people waiting on an answer. I could not hide behind "we're looking into it" in my own head, because that still leaves someone wondering whether they can actually come.&lt;/p&gt;

&lt;h2&gt;
  
  
  It wasn't only about interpretation
&lt;/h2&gt;

&lt;p&gt;The more I sat with it, the more obvious it became that this was bigger than one accessibility request.&lt;/p&gt;

&lt;p&gt;Yes, ISL interpretation mattered. A lot. But so did transcription. And not only for deaf attendees. There are hard of hearing attendees who do not use sign language. There are people who find spoken content much easier to process when they can read along. There are neurodivergent attendees who benefit from seeing the words as well as hearing them. There are remote viewers. There are people trying to follow a technical talk delivered quickly by someone with a strong accent while half the room laughs at a reference they missed.&lt;/p&gt;

&lt;p&gt;Once I started thinking about it that way, live transcription stopped feeling like a backup option. It felt central.&lt;/p&gt;

&lt;h2&gt;
  
  
  The options were not great
&lt;/h2&gt;

&lt;p&gt;So I started talking to captioning and transcription providers.&lt;/p&gt;

&lt;p&gt;The split was basically what you would expect, but more frustrating when you are the one making the call. Human captioning looked strong. High accuracy, experienced operators, something you could trust. But it came in at the kind of price that forces a community event to think very carefully about whether it can carry it.&lt;/p&gt;

&lt;p&gt;AI captioning was somewhat easier to justify on cost - although it still ran into 4 figures(!) and the quoted level of accuracy just was not good enough for a technical conference. Not with fast speakers, different accents, library names, acronyms, product terms, and all the weird ways developers talk when they are explaining something they know too well.&lt;/p&gt;

&lt;p&gt;That was the bit that kept bothering me. The choice seemed to be either spend a lot or accept something that would let people down. That is not much of a choice if the whole point is accessibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  So I built it
&lt;/h2&gt;

&lt;p&gt;At some stage, after enough comparing and researching and getting annoyed by the gap, I stopped asking who I should buy from and started asking what I actually wanted the software to do.&lt;/p&gt;

&lt;p&gt;I wanted live transcription that could keep up with technical talks. I wanted something accurate enough that a person could depend on it instead of politely pretending it was helpful. I wanted it to be affordable enough that smaller events could realistically use it.&lt;/p&gt;

&lt;p&gt;I was not thinking "this should be a SaaS." I was thinking "I need this to exist for DevFest."&lt;/p&gt;

&lt;p&gt;So I built it.&lt;/p&gt;

&lt;p&gt;It started the way a lot of things start: messy, practical, not especially glamorous. A lot of testing. A lot of fixing. A lot of checking the output and asking myself whether I would trust this if I were relying on it to follow a talk. That was the standard that mattered to me. Not whether it looked clever. Whether it actually helped.&lt;/p&gt;

&lt;h2&gt;
  
  
  From a solution for one event to VolenScribe
&lt;/h2&gt;

&lt;p&gt;That tool eventually became &lt;a href="https://volenscribe.com" rel="noopener noreferrer"&gt;VolenScribe&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;VolenScribe is the SaaS that came out of all of this. It does live AI transcription for events, and it grew directly from trying to solve this problem in a way that did not feel half baked. One of the main things I cared about from the start was accuracy, because that is where so many AI transcription products fall apart once you put them in a real room with real people, and making sure it was useful beyond English-only events. VolenScribe supports 25 spoken languages, so it can be used across the world.&lt;/p&gt;

&lt;p&gt;The AI transcription model used by VolenScribe has an average word error rate of 3.9%. That matters to me more than any vague claim about being smart or scalable or next generation or whatever else people like to say. The actual question is simpler: if someone is reading these captions, can they follow what is being said without constantly correcting the machine in their head?&lt;/p&gt;

&lt;p&gt;That is the bar.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually mattered here
&lt;/h2&gt;

&lt;p&gt;The strange thing is that I never set out to build a company around this. It came from a very specific problem, at a very specific event, because a few people asked a very reasonable question and I realised I did not have a good answer.&lt;/p&gt;

&lt;p&gt;I think that is why this has stayed with me.&lt;/p&gt;

&lt;p&gt;Accessibility is often talked about in broad, well-meaning terms, but the reality is much more direct than that. Someone wants to attend. Someone wants to follow the talk. Someone wants to feel like the event was built with them in mind too. Either they can, or they cannot.&lt;/p&gt;

&lt;p&gt;That is what all of this came down to in the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I'm still thinking about it
&lt;/h2&gt;

&lt;p&gt;I do not think organisers should have to choose between something expensive and something unreliable. I do not think accessibility should become one of those things people care about right up until the invoice lands. And I definitely do not think the answer should be "well, this is the best we could do" when the result still leaves people out.&lt;/p&gt;

&lt;p&gt;Volenscribe started because I could not find something I trusted enough to use.&lt;/p&gt;

&lt;p&gt;It just happened that solving that for DevFest Ireland 2025 turned into something other people needed too.&lt;/p&gt;

&lt;p&gt;And really, it all goes back to those first messages. A few deaf developers reached out because they wanted to be there. Everything that came after, including the software, came from taking that seriously.&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>saas</category>
      <category>showdev</category>
      <category>startup</category>
    </item>
    <item>
      <title>AI-Powered Repository Security Check with Antigravity Workflow</title>
      <dc:creator>Alexander Tyutin</dc:creator>
      <pubDate>Mon, 06 Apr 2026 09:46:09 +0000</pubDate>
      <link>https://dev.to/gdg/ai-powered-repository-security-check-with-antigravity-workflow-5hee</link>
      <guid>https://dev.to/gdg/ai-powered-repository-security-check-with-antigravity-workflow-5hee</guid>
      <description>&lt;p&gt;When teams want to "move fast and break things," security is often the first thing they forget. I've seen a lot over 15 years in the industry. My approach is simple: follow the &lt;strong&gt;Pareto Principle (80/20)&lt;/strong&gt;. You want 80% of the security results with just 20% of the work.&lt;/p&gt;

&lt;p&gt;In the AI era, that 20% of work can look like a single command. &lt;/p&gt;

&lt;p&gt;Here is how we built the &lt;a href="https://antigravity.google/docs/rules-workflows" rel="noopener noreferrer"&gt;Antigravity workflow&lt;/a&gt; that checks the whole repository for security issues in several minutes. It does not cost much and does not use up all the AI's context window.&lt;/p&gt;

&lt;p&gt;Short video demo made on a real repository:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/X0a_hwPxTS8"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  The Initial Stack
&lt;/h2&gt;

&lt;p&gt;To get a clear picture of a repository's health, one tool is not enough. We use a combination of proven, open-source scanners for the beginning:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;&lt;a href="https://github.com/gitleaks/gitleaks" rel="noopener noreferrer"&gt;Gitleaks&lt;/a&gt;&lt;/strong&gt;: To find secrets like API keys and tokens.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;&lt;a href="https://github.com/semgrep/semgrep" rel="noopener noreferrer"&gt;Semgrep&lt;/a&gt;&lt;/strong&gt;: For SCA and SAST to find bad code patterns and supply chain issues.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;&lt;a href="https://github.com/bridgecrewio/checkov" rel="noopener noreferrer"&gt;Checkov&lt;/a&gt;&lt;/strong&gt;: To check IaC security (Docker, Terraform, Kubernetes).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/google/osv-scanner" rel="noopener noreferrer"&gt;OSV-Scanner&lt;/a&gt;&lt;/strong&gt;: For SCA scan.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Inspecting their results manually takes a lot of time. And if you just send all their raw output directly to an AI, it becomes very expensive and confusing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Token Economy
&lt;/h2&gt;

&lt;p&gt;For a security review, the AI doesn't need to see every test that passed. It doesn't need to see the full abstract syntax tree. It only needs to know &lt;strong&gt;what is broken, where it is, and why it matters.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We use &lt;code&gt;jq&lt;/code&gt; to remove the extra noise. This minifying step is very important for &lt;strong&gt;Token Economy&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;To increase the token savings the command (workflow) may be ran with the cheapest Gemini 3 Flash. It is more than enough to receive a high-quality base report. Then the report may be reviewed with more powered models like Gemini 3.1 Pro.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example: Minifying Results
&lt;/h3&gt;

&lt;p&gt;Instead of a huge JSON file per tool, we make it small and simple. For example, here are the exact commands we use to make the results smaller:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  1. &lt;span class="sb"&gt;`&lt;/span&gt;jq &lt;span class="s1"&gt;'[.[] | {rule: .RuleID, file: .File, line: .StartLine}]'&lt;/span&gt; gitleaks-raw.json &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; gitleaks-min.json&lt;span class="sb"&gt;`&lt;/span&gt;
  2. &lt;span class="sb"&gt;`&lt;/span&gt;jq &lt;span class="s1"&gt;'[.results[] | {rule: .check_id, file: .path, line: .start.line, severity: .extra.severity}]'&lt;/span&gt; semgrep-raw.json &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; semgrep-min.json&lt;span class="sb"&gt;`&lt;/span&gt;
  3. &lt;span class="sb"&gt;`&lt;/span&gt;jq &lt;span class="s1"&gt;'if type=="array" then map(.results.failed_checks[]) else .results.failed_checks end | [.[]? | {rule: .check_id, file: .file_path, line: .file_line_range}]'&lt;/span&gt; checkov-raw.json &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; checkov-min.json&lt;span class="sb"&gt;`&lt;/span&gt;
  4. &lt;span class="sb"&gt;`&lt;/span&gt;jq &lt;span class="s1"&gt;'[.results[]?.packages[]?.vulnerabilities[]? | {rule: .id, file: .package.name, line: "N/A", severity: ((.database_specific.severity) // "N/A")}]'&lt;/span&gt; osv-raw.json &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; osv-min.json&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By making the data 90% smaller, the AI stays focused on real problems. This makes the check much cheaper.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;% &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-lh&lt;/span&gt; .security-artifacts | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $5, $9}'&lt;/span&gt;

6.3K checkov-min.json
2.6M checkov-rev-004-security-20260401-201110.json
2.4K gitleaks-min.json
19K gitleaks-rev-004-security-20260401-161824.json
19K gitleaks-rev-004-security-20260401-201110.json
107B osv-min-rev-001-security-20260406-110805.json
11K osv-raw-rev-001-security-20260406-110805.json
4.3K semgrep-min.json
61K semgrep-rev-004-security-20260401-161824.json
38K semgrep-rev-004-security-20260401-201110.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The "One Command" Workflow
&lt;/h2&gt;

&lt;p&gt;We put all these steps into one Antigravity slash command: &lt;code&gt;/review-security-repo&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;When we run it, the agent does exactly this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Identifies the environment&lt;/strong&gt;: Checks for tools like &lt;code&gt;semgrep&lt;/code&gt;, &lt;code&gt;gitleaks&lt;/code&gt;, &lt;code&gt;checkov&lt;/code&gt;, &lt;code&gt;osv-scanner&lt;/code&gt; and &lt;code&gt;jq&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Executes Raw Scans&lt;/strong&gt;: Runs the scanners to get raw logs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Applies Minification&lt;/strong&gt;: Uses &lt;code&gt;jq&lt;/code&gt; to strip massive metadata.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Synthesizes Findings&lt;/strong&gt;: Only reads the small files (&lt;code&gt;gitleaks-min.json&lt;/code&gt;, &lt;code&gt;semgrep-min.json&lt;/code&gt;, &lt;code&gt;checkov-min.json&lt;/code&gt;, &lt;code&gt;osv-min.json&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performs Review&lt;/strong&gt;: Checks high-risk files to find complex problems that static tools miss.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generates an Actionable Report&lt;/strong&gt;: Uses a strict Markdown structure instead of a generic summary.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Report Structure Snippet
&lt;/h3&gt;

&lt;p&gt;The workflow forces the AI to output exactly what we need, like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;### [Severity] - [Vulnerability Name/Rule ID]&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Tool Source:**&lt;/span&gt; [Semgrep / Gitleaks / Checkov / Manual Architectural Review]
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Location:**&lt;/span&gt; &lt;span class="sb"&gt;`[File Name]:[Line Number]`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Business Impact:**&lt;/span&gt; [Why this matters]
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Remediation:**&lt;/span&gt; 
  [Actionable, copy-paste code or config fix]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why This Works
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repeatability&lt;/strong&gt;: Anyone on the team can check security without being an expert.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit Trail&lt;/strong&gt;: Every raw and minified report is moved to &lt;code&gt;.security-artifacts/&lt;/code&gt; so we can track the history.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reduced Hallucinations&lt;/strong&gt;: Because we give AI only the exact scanner results and small code pieces, it gives real fixes without making things up.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Full Workflow Code
&lt;/h2&gt;

&lt;p&gt;If you want to try this yourself, here is the complete code for the &lt;code&gt;/review-security-repo&lt;/code&gt; workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Security&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;review&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;of&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;repo"&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="p"&gt;-&lt;/span&gt; Get the current branch name and current timestamp (format: YYYYMMDD-HHMMSS). Define output file as &lt;span class="sb"&gt;`security-review-[branch-name]-[timestamp].md`&lt;/span&gt;.
&lt;span class="p"&gt;-&lt;/span&gt; Check for a &lt;span class="sb"&gt;`venv`&lt;/span&gt; (or &lt;span class="sb"&gt;`.venv`&lt;/span&gt;) directory in the repository root. If found, use its binaries.
&lt;span class="p"&gt;-&lt;/span&gt; Verify if &lt;span class="sb"&gt;`semgrep`&lt;/span&gt;, &lt;span class="sb"&gt;`gitleaks`&lt;/span&gt;, &lt;span class="sb"&gt;`checkov`&lt;/span&gt;, and &lt;span class="sb"&gt;`jq`&lt;/span&gt; are installed. If missing, prompt for installation and pause until confirmed.
&lt;span class="p"&gt;-&lt;/span&gt; Execute local security scanners to capture raw audit trails:
&lt;span class="p"&gt;  1.&lt;/span&gt; &lt;span class="sb"&gt;`gitleaks detect --source . -v --report-format json --report-path gitleaks-raw.json`&lt;/span&gt;
&lt;span class="p"&gt;  2.&lt;/span&gt; &lt;span class="sb"&gt;`semgrep scan --config auto --json --output semgrep-raw.json`&lt;/span&gt;
&lt;span class="p"&gt;  3.&lt;/span&gt; &lt;span class="sb"&gt;`checkov -d . --quiet --skip-path venv -o json &amp;gt; checkov-raw.json`&lt;/span&gt;
&lt;span class="p"&gt;  4.&lt;/span&gt; &lt;span class="sb"&gt;`osv-scanner -r . --format json &amp;gt; osv-raw.json || true`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Execute &lt;span class="sb"&gt;`jq`&lt;/span&gt; to strip massive metadata, passed checks, and AST dumps, keeping only critical fields to save context tokens:
&lt;span class="p"&gt;  1.&lt;/span&gt; &lt;span class="sb"&gt;`jq '[.[] | {rule: .RuleID, file: .File, line: .StartLine}]' gitleaks-raw.json &amp;gt; gitleaks-min.json`&lt;/span&gt;
&lt;span class="p"&gt;  2.&lt;/span&gt; &lt;span class="sb"&gt;`jq '[.results[] | {rule: .check_id, file: .path, line: .start.line, severity: .extra.severity}]' semgrep-raw.json &amp;gt; semgrep-min.json`&lt;/span&gt;
&lt;span class="p"&gt;  3.&lt;/span&gt; &lt;span class="sb"&gt;`jq 'if type=="array" then map(.results.failed_checks[]) else .results.failed_checks end | [.[]? | {rule: .check_id, file: .file_path, line: .file_line_range}]' checkov-raw.json &amp;gt; checkov-min.json`&lt;/span&gt;
&lt;span class="p"&gt;  4.&lt;/span&gt; &lt;span class="sb"&gt;`jq '[.results[]?.packages[]?.vulnerabilities[]? | {rule: .id, file: .package.name, line: "N/A", severity: ((.database_specific.severity) // "N/A")}]' osv-raw.json &amp;gt; osv-min.json`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Read ONLY &lt;span class="sb"&gt;`gitleaks-min.json`&lt;/span&gt;, &lt;span class="sb"&gt;`semgrep-min.json`&lt;/span&gt;, &lt;span class="sb"&gt;`checkov-min.json`&lt;/span&gt;, &lt;span class="sb"&gt;`osv-min.json`&lt;/span&gt;. Filter out false positives based on repository context.
&lt;span class="p"&gt;-&lt;/span&gt; Analyze high-risk architectural files strictly for logical flaws and cross-service least-privilege violations that static tools cannot understand.
&lt;span class="p"&gt;-&lt;/span&gt; Generate the report in &lt;span class="sb"&gt;`security-review-[branch-name]-[timestamp].md`&lt;/span&gt;. DO NOT output generic summary tables. You MUST output an exhaustive, itemized list.
&lt;span class="p"&gt;-&lt;/span&gt; Use the following strict Markdown structure for the report:
  ## Executive Summary
  [Brief overview of the branch's security posture]
  ## Detailed Findings
  [Iterate through EVERY validated finding. For each finding, output:]
  ### [Severity] - [Vulnerability Name/Rule ID]
&lt;span class="p"&gt;  -&lt;/span&gt; &lt;span class="gs"&gt;**Tool Source:**&lt;/span&gt; [Semgrep / Gitleaks / Checkov / Manual Architectural Review]
&lt;span class="p"&gt;  -&lt;/span&gt; &lt;span class="gs"&gt;**Location:**&lt;/span&gt; &lt;span class="sb"&gt;`[File Name]:[Line Number]`&lt;/span&gt;
&lt;span class="p"&gt;  -&lt;/span&gt; &lt;span class="gs"&gt;**Business Impact:**&lt;/span&gt; [Why this matters]
&lt;span class="p"&gt;  -&lt;/span&gt; &lt;span class="gs"&gt;**Remediation:**&lt;/span&gt; 
    &lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;
&lt;/span&gt;

    [Actionable, copy-paste code or config fix]


    &lt;span class="p"&gt;```&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Create a &lt;span class="sb"&gt;`.security-artifacts/`&lt;/span&gt; directory if it does not exist. Ensure &lt;span class="sb"&gt;`.security-artifacts/`&lt;/span&gt; is appended to &lt;span class="sb"&gt;`.gitignore`&lt;/span&gt;.
&lt;span class="p"&gt;-&lt;/span&gt; Move and rename both raw and minified reports to &lt;span class="sb"&gt;`.security-artifacts/`&lt;/span&gt; to preserve the complete historical audit trail:
&lt;span class="p"&gt;  -&lt;/span&gt; &lt;span class="sb"&gt;`gitleaks-raw.json`&lt;/span&gt; -&amp;gt; &lt;span class="sb"&gt;`.security-artifacts/gitleaks-raw-[branch-name]-[timestamp].json`&lt;/span&gt;
&lt;span class="p"&gt;  -&lt;/span&gt; &lt;span class="sb"&gt;`semgrep-raw.json`&lt;/span&gt; -&amp;gt; &lt;span class="sb"&gt;`.security-artifacts/semgrep-raw-[branch-name]-[timestamp].json`&lt;/span&gt;
&lt;span class="p"&gt;  -&lt;/span&gt; &lt;span class="sb"&gt;`checkov-raw.json`&lt;/span&gt; -&amp;gt; &lt;span class="sb"&gt;`.security-artifacts/checkov-raw-[branch-name]-[timestamp].json`&lt;/span&gt;
&lt;span class="p"&gt;  -&lt;/span&gt; &lt;span class="sb"&gt;`osv-raw.json`&lt;/span&gt; -&amp;gt; &lt;span class="sb"&gt;`.security-artifacts/osv-raw-[branch-name]-[timestamp].json`&lt;/span&gt;
&lt;span class="p"&gt;  -&lt;/span&gt; &lt;span class="sb"&gt;`gitleaks-min.json`&lt;/span&gt; -&amp;gt; &lt;span class="sb"&gt;`.security-artifacts/gitleaks-min-[branch-name]-[timestamp].json`&lt;/span&gt;
&lt;span class="p"&gt;  -&lt;/span&gt; &lt;span class="sb"&gt;`semgrep-min.json`&lt;/span&gt; -&amp;gt; &lt;span class="sb"&gt;`.security-artifacts/semgrep-min-[branch-name]-[timestamp].json`&lt;/span&gt;
&lt;span class="p"&gt;  -&lt;/span&gt; &lt;span class="sb"&gt;`checkov-min.json`&lt;/span&gt; -&amp;gt; &lt;span class="sb"&gt;`.security-artifacts/checkov-min-[branch-name]-[timestamp].json`&lt;/span&gt;
&lt;span class="p"&gt;  -&lt;/span&gt; &lt;span class="sb"&gt;`osv-min.json`&lt;/span&gt; -&amp;gt; &lt;span class="sb"&gt;`.security-artifacts/osv-min-[branch-name]-[timestamp].json`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Exit execution.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What’s Next?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What tools are missing from your perfect "One Command" security check?&lt;/strong&gt; Will be happy to receive opinions on how to further optimize the token economy while expanding the security coverage.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>antigravity</category>
      <category>development</category>
      <category>security</category>
    </item>
    <item>
      <title>Scaling Product Discovery: Orchestrating AI Agent Workflows with Google Opal</title>
      <dc:creator>Sandro Moreira</dc:creator>
      <pubDate>Sun, 05 Apr 2026 14:52:06 +0000</pubDate>
      <link>https://dev.to/gdg/scaling-product-discovery-orchestrating-ai-agent-workflows-with-google-opal-2982</link>
      <guid>https://dev.to/gdg/scaling-product-discovery-orchestrating-ai-agent-workflows-with-google-opal-2982</guid>
      <description>&lt;p&gt;&lt;strong&gt;Introduction: The Challenge of Relevance in Software Development&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Developing an application is one thing; creating one that users actually want to use is another. The difference lies in the depth of your understanding of the pains, desires, and unmet needs of your target audience. Too often, development begins with a solution looking for a problem.&lt;/p&gt;

&lt;p&gt;I recently embarked on a journey to optimize my product discovery process, moving from time-consuming manual methods to an AI-assisted application building workflow. My mission? To build a flow that not only generated ideas but deeply validated them against the real sentiment of the market.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Flow&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1 - The Raw Data Collection (Sources):&lt;/strong&gt; I focused on a specific topic of interest and fed NotebookLM's source feature with complex data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User Pains: Documents extracted from forums (Reddit, communities) to investigate pains unresolved by existing solutions.&lt;/li&gt;
&lt;li&gt;Market Trends: Articles and research on emerging trends (e.g., generative AI integration, specialized niches).&lt;/li&gt;
&lt;li&gt;Competitive Analysis: Detailed reviews and descriptions of competitor applications.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2- Information Cross-Referencing and Analysis:&lt;/strong&gt; NotebookLM excelled at finding connections. By asking the model to cross-reference data, I could quickly identify patterns of complaints and resource gaps in the market.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3- Generating Strategic Insights (Prompts):&lt;/strong&gt; I used structured analysis prompts to synthesize the data into actionable insights: "Cross-reference market trends with competitor features and identify a feature that is in high demand, but has low coverage in the market."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4 - The MVP Conception (Final Prompt):&lt;/strong&gt; The final step was generating a highly refined MVP prompt, ready for development:&lt;br&gt;
"Generate an MVP documentation (including Target Audience, Value Proposition, and 3 Essential Features) that addresses pain X, aligns with trend Y, and offers feature Z (low/non-existent coverage). The goal is for this MVP to be built on Gemini 3.0."&lt;/p&gt;
&lt;h2&gt;
  
  
  Scaling Discovery with Google Opal
&lt;/h2&gt;

&lt;p&gt;The initial validation phase using NotebookLM was an insightful experiment, but it still required significant manual intervention. Now, it’s time to evolve this workflow into a truly automated engine.&lt;/p&gt;

&lt;p&gt;This led me to Google Opal, the platform designed to turn complex logic into visual, multi-step AI Workflows (or mini-apps). While the concept mirrors the functionality of AI Agents - systems that take action autonomously - Opal provides the no-code workflow pipeline for orchestrating these steps reliably.&lt;/p&gt;

&lt;p&gt;In Google Opal, the goal is to create a product discovery agent pipeline that operates continuously.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1 - The Search:&lt;/strong&gt; At external sources (forums, reviews) to filter and feed new pain signals and trends.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: The Analyst:&lt;/strong&gt; Analyzes data from phase 1 against competitive data to generate strategic gaps and novel feature ideas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: The Strategist:&lt;/strong&gt; Converts the strategic report into the final assets for development and business.&lt;/p&gt;

&lt;p&gt;Here is the step-by-step breakdown of how I configured the three core agents in Google Opal, turning a simple workflow into an autonomous pipeline.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;Step 1: The Search (Data Ingestion)&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;This step is the system's eyes and ears, responsible for ingesting and pre-filtering raw market data.&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.amazonaws.com%2Fuploads%2Farticles%2F8qqd394yxfzaxiao4jgb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8qqd394yxfzaxiao4jgb.png" alt=" " width="800" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1 - Topic of Interest:&lt;/strong&gt; &lt;em&gt;UserInput&lt;/em&gt; Initially empty node where the user enters the topic of interest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2 - Search Users, Trends and Competitors:&lt;/strong&gt; &lt;em&gt;Generate&lt;/em&gt; Nodes for search with specific prompts&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.amazonaws.com%2Fuploads%2Farticles%2Fxnwqgpxe98dbvt8trr6r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxnwqgpxe98dbvt8trr6r.png" alt=" " width="463" height="326"&gt;&lt;/a&gt;&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.amazonaws.com%2Fuploads%2Farticles%2Fpac6azihjig320i4ihqt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpac6azihjig320i4ihqt.png" alt=" " width="453" height="422"&gt;&lt;/a&gt;&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.amazonaws.com%2Fuploads%2Farticles%2Fovy7pye7slqa4aebr3hb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fovy7pye7slqa4aebr3hb.png" alt=" " width="454" height="375"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3 - Users Opinion, Trends and Competitors:&lt;/strong&gt; &lt;em&gt;OutPut (Google Docs)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Each output node I specified "Save to Google Docs" and set a name for the file in Advanced Settings.&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.amazonaws.com%2Fuploads%2Farticles%2Fgqm7xf27epkwox99sn9e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgqm7xf27epkwox99sn9e.png" alt=" " width="458" height="342"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;Step 2: The Analyst (Insight Generation)&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;This is the brain of the operation, where raw data is turned into strategic intelligence.&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.amazonaws.com%2Fuploads%2Farticles%2F87bym9ekzq5ha9pzf4lw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F87bym9ekzq5ha9pzf4lw.png" alt=" " width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1 - Analyze Problems:&lt;/strong&gt; &lt;em&gt;Generate&lt;/em&gt;&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.amazonaws.com%2Fuploads%2Farticles%2Fautzuzqwkn89vulyjiep.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fautzuzqwkn89vulyjiep.png" alt=" " width="382" height="256"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2 - Strategic Analysis:&lt;/strong&gt; &lt;em&gt;Generate&lt;/em&gt;&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.amazonaws.com%2Fuploads%2Farticles%2F42r3bw3lkln04ah39aqg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F42r3bw3lkln04ah39aqg.png" alt=" " width="453" height="239"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3 - Pain Points:&lt;/strong&gt; &lt;em&gt;OutPut (Google Docs)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4 - Strategic Analysis:&lt;/strong&gt; &lt;em&gt;OutPut (Google Docs)&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;Step 3: The Strategist (MVP &amp;amp; Pitch Generation)&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The final takes the distilled strategy and turns it into deliverable assets for developers and stakeholders.&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.amazonaws.com%2Fuploads%2Farticles%2F890yojx5886xl97nndgn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F890yojx5886xl97nndgn.png" alt=" " width="800" height="512"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1 - Ideas App&lt;/strong&gt; &lt;em&gt;Generate&lt;/em&gt;&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.amazonaws.com%2Fuploads%2Farticles%2Fw9pk5lionrcjjuzi3i1w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw9pk5lionrcjjuzi3i1w.png" alt=" " width="458" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2 - MVP Recommendation&lt;/strong&gt; &lt;em&gt;Generate&lt;/em&gt;&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.amazonaws.com%2Fuploads%2Farticles%2Fkzta5515gb4rq53y6dj6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkzta5515gb4rq53y6dj6.png" alt=" " width="457" height="286"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3 - Generate Pitch&lt;/strong&gt; &lt;em&gt;Generate&lt;/em&gt;&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.amazonaws.com%2Fuploads%2Farticles%2Fxxhd3p8wq0d8rh94xj0p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxxhd3p8wq0d8rh94xj0p.png" alt=" " width="458" height="294"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The nodes are connected to other &lt;em&gt;OutPut (Google Docs)&lt;/em&gt; (Ideas, MVP Recommendation) for creating files in Google Docs and the node Pitch has type &lt;em&gt;OutPut (Google Slides)&lt;/em&gt;&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.amazonaws.com%2Fuploads%2Farticles%2Fll5dxwmujlj51x63pqjh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fll5dxwmujlj51x63pqjh.png" alt=" " width="458" height="231"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And finally, the last step where we add a node to generate the Prompt for Gemini 3, another with an HTML output showing the result of the entire flow execution.&lt;/p&gt;
&lt;h2&gt;
  
  
  Conclusion: From Idea to MVP in Minutes
&lt;/h2&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.amazonaws.com%2Fuploads%2Farticles%2Faqlsuik3ubn0bfillf48.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faqlsuik3ubn0bfillf48.png" alt=" " width="781" height="308"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1 - Prompt to App:&lt;/strong&gt; &lt;em&gt;Generate&lt;/em&gt;&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.amazonaws.com%2Fuploads%2Farticles%2Fo37cbwbn61oib96ziqhr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo37cbwbn61oib96ziqhr.png" alt=" " width="459" height="309"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2 - Prompt for Gemini 3.0:&lt;/strong&gt; &lt;em&gt;OutPut (Google Docs)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3 - Generate HTML:&lt;/strong&gt; &lt;em&gt;OutPut (Manual Layout)&lt;/em&gt;&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.amazonaws.com%2Fuploads%2Farticles%2Frvcf5kiqn7p9oq8j9fsm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frvcf5kiqn7p9oq8j9fsm.png" alt=" " width="461" height="308"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It transforms your workflow into an innovation pipeline quickly, ensuring that your next application not only meets a market need but is designed to address a real problem the market is actively complaining about.&lt;/p&gt;

&lt;p&gt;By leveraging the power of agents and workdflows, we can move from market insight to a complete, validated, and ready-to-code MVP specification in minutes, significantly de-risking the development process.&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://opal.google/_app/?flow=drive:/1Ha9NWnl-jp_602d6Ewg5EyGWqLwMQqnu&amp;amp;amp;shared&amp;amp;amp;mode=app" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;opal.google&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;To create the MVP, simply use the generated prompt and run it in the Build tab of ai.dev, letting Gemini build it for you.&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.amazonaws.com%2Fuploads%2Farticles%2Flx48wrq0zns8pv8rtqam.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flx48wrq0zns8pv8rtqam.png" alt=" " width="800" height="504"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Want to put it into production? Use the icon in the upper corner of the built app screen to send it directly to Google Cloud.&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.amazonaws.com%2Fuploads%2Farticles%2Fhcmwhypa6mnp9zlzqgnb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhcmwhypa6mnp9zlzqgnb.png" alt=" " width="800" height="302"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>startup</category>
      <category>opal</category>
    </item>
    <item>
      <title>Cross-Repository Development with Antigravity</title>
      <dc:creator>Razan Fawwaz</dc:creator>
      <pubDate>Thu, 02 Apr 2026 02:26:20 +0000</pubDate>
      <link>https://dev.to/gdg/cross-repository-development-with-antigravity-26be</link>
      <guid>https://dev.to/gdg/cross-repository-development-with-antigravity-26be</guid>
      <description>&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%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A1400%2Fformat%3Awebp%2F0%2ARLyrj2QLl6pGdLmB" 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%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A1400%2Fformat%3Awebp%2F0%2ARLyrj2QLl6pGdLmB" alt="Photo by Janis Ringli on Unsplash" width="1400" height="815"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you've been sleeping on &lt;a href="https://antigravity.google" rel="noopener noreferrer"&gt;Antigravity&lt;/a&gt;, now's the time to wake up. Google's latest IDE isn't just another code editor — it comes with a built-in AI Agent Manager, meaning you can write code, run unit tests, spin up files, and execute everything directly, all while an AI assistant handles the heavy lifting alongside you. You can even plug in MCP Servers to pass richer context to the Agent.&lt;/p&gt;

&lt;p&gt;Honestly? With Antigravity, you can kick off a task and go fishing. I'm not even joking.&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%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A1400%2Fformat%3Awebp%2F0%2AhqC3vuiSbJrXE-5R" 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%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A1400%2Fformat%3Awebp%2F0%2AhqC3vuiSbJrXE-5R" alt="Photo by Randhy Pratama on Unsplash" width="1400" height="933"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with Scaling
&lt;/h2&gt;

&lt;p&gt;Out of the box, Antigravity tends to scaffold projects using fullstack frameworks like Next.js or Vite + React. For smaller projects, that's totally fine — convenient, even.&lt;/p&gt;

&lt;p&gt;But once you start scaling, a single monorepo starts to feel like a liability. The codebase bloats, separation of concerns gets blurry, and things that should be independent start bleeding into each other. Personally, I much prefer keeping frontend and backend as separate repositories. Shoutout to &lt;a href="https://x.com/isfaaghyth" rel="noopener noreferrer"&gt;Kak Ishfa&lt;/a&gt; for pushing me to think about this more seriously.&lt;/p&gt;

&lt;p&gt;Here's the catch though — Antigravity doesn't have a native &lt;code&gt;Linked Workspace&lt;/code&gt; feature. There's no built-in way to point a single Agent at two separate repositories and have it work across both seamlessly.&lt;/p&gt;

&lt;p&gt;But there's a workaround, and it's cleaner than you'd expect.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Workaround: Agent Rules
&lt;/h2&gt;

&lt;p&gt;The trick is using &lt;code&gt;Agent Rules&lt;/code&gt; — a feature that lets you define behavior the Agent always follows, regardless of what you're asking it to do.&lt;/p&gt;

&lt;p&gt;The idea is simple: open your frontend repository in Antigravity, then write a rule that tells the Agent about your backend repository's directory path. From that point on, whenever a frontend change requires a corresponding backend update, the Agent knows exactly where to go and what to touch.&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.amazonaws.com%2Fuploads%2Farticles%2Fzgvkx8u1b3u7matdofzx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzgvkx8u1b3u7matdofzx.png" alt="captionless image" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's how I set it up: I have two repos, frontend and backend. In the frontend project, I added a rule that says — in plain language — "if any feature change requires backend work, update the backend repository too." The key detail is setting the activation mode to &lt;strong&gt;always on&lt;/strong&gt;, so the Agent checks the rules on every single interaction, not just when you explicitly tell it to.&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.amazonaws.com%2Fuploads%2Farticles%2Fl57xxodj7lhdl4upoxb0.jpeg" 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.amazonaws.com%2Fuploads%2Farticles%2Fl57xxodj7lhdl4upoxb0.jpeg" alt="captionless image" width="725" height="344"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's a rough idea of what the rule content looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are working on a frontend project located at /path/to/frontend.
There is a paired backend project located at /path/to/backend (Go, net/http, SQLite).
Whenever a frontend change requires an API or backend change, apply those changes
to the backend project as well. Always keep both repositories in sync.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple, readable, and it works.&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.amazonaws.com%2Fuploads%2Farticles%2Fqzscphv5w5yqa0far1e3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqzscphv5w5yqa0far1e3.png" alt="captionless image" width="398" height="608"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting It to the Test
&lt;/h2&gt;

&lt;p&gt;To try this out, I built a todo app — login, full CRUD, connected to a Go + net/http + SQLite backend. Both directories started completely empty.&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.amazonaws.com%2Fuploads%2Farticles%2Fo80hvbzqw5ddwymemw59.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo80hvbzqw5ddwymemw59.png" alt="captionless image" width="198" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once I submitted the prompt, Antigravity picked up the rules immediately and asked for approval before touching anything. After confirming, it installed the necessary packages and kicked off development — starting with the backend.&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.amazonaws.com%2Fuploads%2Farticles%2Fd6k35qht72enwudcc6l1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd6k35qht72enwudcc6l1.png" alt="captionless image" width="406" height="672"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With the backend done, it seamlessly transitioned to the frontend and set up Vite + React.&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.amazonaws.com%2Fuploads%2Farticles%2Fnjmstwfct7bzrlx3udeh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnjmstwfct7bzrlx3udeh.png" alt="captionless image" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When the chat stopped and the "accept all" button appeared, both projects were fully scaffolded and in sync — exactly as intended.&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.amazonaws.com%2Fuploads%2Farticles%2Fn4vghanapm6gf5ldl51e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn4vghanapm6gf5ldl51e.png" alt="captionless image" width="800" height="459"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Is this the most elegant solution? Not really — ideally Antigravity would ship a proper &lt;code&gt;Linked Workspace&lt;/code&gt; feature. But as a workaround, Agent Rules gets the job done surprisingly well. It's flexible, easy to set up, and once it's in place you barely have to think about it.&lt;/p&gt;

&lt;p&gt;If you're working with separate repositories in Antigravity, give this a shot. It might just change how you build.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;#Antigravity&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>antigravity</category>
      <category>gemini</category>
      <category>programming</category>
    </item>
    <item>
      <title>Antigravity: My Approach to Deliver the Most Assured Value for the Least Money</title>
      <dc:creator>Alexander Tyutin</dc:creator>
      <pubDate>Wed, 01 Apr 2026 05:49:34 +0000</pubDate>
      <link>https://dev.to/gdg/antigravity-my-approach-to-deliver-the-most-assured-value-for-the-least-money-3iip</link>
      <guid>https://dev.to/gdg/antigravity-my-approach-to-deliver-the-most-assured-value-for-the-least-money-3iip</guid>
      <description>&lt;p&gt;As I'm not a professional developer but a guy who needs to use automation to get things done, I follow one main rule: keep it simple. Overengineering hurts. I use the Pareto rule—spend 20% of the effort to get 80% of the result. &lt;/p&gt;

&lt;p&gt;When I use AI agents like Antigravity, my goal is not to let the AI write complex code that no one can read. My goal is to build simple, secure features fast. At the same time, I control costs by saving tokens. Here is the exact workflow I use.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Token Economy Strategy
&lt;/h2&gt;

&lt;p&gt;LLM tokens cost money. Using a smart, expensive model just to fix code spaces is not worth the cost. I change models based on how hard the task is.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;High-Tier Models:&lt;/strong&gt; They are for the big tasks: planning architecture, writing complex business logic, checking security, and counting cloud costs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low-Tier Models:&lt;/strong&gt; These folks are for simple tasks: fixing syntax errors, aligning code to Pylint, and writing standard code pieces.&lt;/li&gt;
&lt;/ul&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.amazonaws.com%2Fuploads%2Farticles%2F1cy5dvgylzf3892p588z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1cy5dvgylzf3892p588z.png" alt="Combining Models" width="800" height="570"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Task Decomposition &amp;amp; In-Repo Architecture
&lt;/h2&gt;

&lt;p&gt;Large prompts can break LLMs. If a prompt has too much text, the AI gets confused and wastes tokens. To stop this, I break every task into small, separate pieces so the AI only sees what it needs.&lt;/p&gt;

&lt;p&gt;I store all architecture plans and tasks inside the code repository (for example, &lt;code&gt;./docs&lt;/code&gt;). This keeps the instructions very close to the code for the AI.&lt;/p&gt;

&lt;p&gt;Every task I write uses this strict four-part structure:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Idea:&lt;/strong&gt; The main business or tech goal. &lt;em&gt;Why it matters:&lt;/em&gt; It proves the task is useful before I spend tokens for delivering a code to review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan:&lt;/strong&gt; The technical blueprint. &lt;em&gt;Why it matters:&lt;/em&gt; It locks down the plan, keeps security high, and stops the AI from inventing bad solutions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What Was Done:&lt;/strong&gt; A short log of the work. &lt;em&gt;Why it matters:&lt;/em&gt; It gives future AI tasks a quick summary, so the AI does not have to read every code file again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debt:&lt;/strong&gt; A list of any technical shortcuts or "crutches" used to save time. &lt;em&gt;Why it matters:&lt;/em&gt; Hidden debt ruins the project. &lt;strong&gt;Important: My custom Quality Gate checks this section. If it finds unapproved shortcuts in the code, it blocks the release completely.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  System Instructions for the AI
&lt;/h2&gt;

&lt;p&gt;To keep the AI agent aligned with the goals, I pass strict system instructions on every run. It never lets the model guess my coding standards. Here are the core rules enforced:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No Crutches:&lt;/strong&gt; Any "crutch" or technical shortcut must be approved by me. Then, the AI must document it as technical debt in the project files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No Inventing Wheels:&lt;/strong&gt; I try hard to avoid this. If a working approach already exists in another project, the AI reuses it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Learn from the Past:&lt;/strong&gt; When building a new service, the AI must check the old tech debt to avoid repeating past mistakes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simple Code Only:&lt;/strong&gt; The code structure should just use standard classes. I avoid "genius-level" extreme one-line code tricks or overwhelming structures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintainability First:&lt;/strong&gt; A middle-level, part-time developer must be able to read and maintain the code.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Core Workflow
&lt;/h2&gt;

&lt;p&gt;Every feature goes through a step-by-step process. I'm trying to keep security and simplicity as the main focus at each step.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Plan &amp;amp; The Plan Review
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;Using a High-Tier Model.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Plan:&lt;/strong&gt; Defining the code structure, the security rules, the cost limits, etc. I make sure not to add to old technical debt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review:&lt;/strong&gt; I look at the plan with a "fresh eye." I do not start coding until the plan is clear with main code snippets planned.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Code &amp;amp; Code Review
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;Using a Low or Mid-Tier Model for code and Mid or High-Tier Model for review.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Code:&lt;/strong&gt; Implement the code exactly as planned. Use clear classes and avoid complex, one-line code tricks. A middle-level developer must be able to maintain it easily.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review:&lt;/strong&gt; Make sure the code matches the rest of the project. I prefer another "person" to check it before I call it done.&lt;/li&gt;
&lt;/ul&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.amazonaws.com%2Fuploads%2Farticles%2Fykhd3l9aeogbmard3itc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fykhd3l9aeogbmard3itc.png" alt="Local Workflow" width="800" height="714"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Lint &amp;amp; Quality Gate
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;Using Free External Tools &amp;amp; A Custom Nanoservice.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lint:&lt;/strong&gt; I do not pay LLMs to fix missing spaces. I use free tools like &lt;code&gt;autopep8&lt;/code&gt;, &lt;code&gt;ruff&lt;/code&gt;, and &lt;code&gt;pylint&lt;/code&gt; to save tokens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quality Gate:&lt;/strong&gt; I built a simple nanoservice using the Vertex API. It checks the code changes against the &lt;code&gt;main&lt;/code&gt; branch. It works like an automatic review from the CTO, CISO, and CFO. It checks every line for good architecture, proper security access, and cost impact before the code goes to production. &lt;strong&gt;Why is it so important?&lt;/strong&gt; The Quality Gate is not overwhelmed by the full chat history inside the IDE. Its "fresh eye" often finds architectural and coding flaws that were missed by the IDE models, even after 6 to 9 rounds of review.&lt;/li&gt;
&lt;/ul&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.amazonaws.com%2Fuploads%2Farticles%2Fe6z0v40pupvxl3w2lj2f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe6z0v40pupvxl3w2lj2f.png" alt="Quality Gate at Work" width="800" height="453"&gt;&lt;/a&gt;&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.amazonaws.com%2Fuploads%2Farticles%2Fw4uxzhcfoe69tdzouboe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw4uxzhcfoe69tdzouboe.png" alt="Full Workflow" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;AI coding is not magic. In my experience, it requires a strict testing gate, smart model swapping, and simple design. By owning the process and letting the AI act as a typist, it is possible to ship secure code fast. I share this approach for an open discussion on how we can build better automation.&lt;/p&gt;

</description>
      <category>antigravity</category>
      <category>development</category>
      <category>automation</category>
      <category>responsibleai</category>
    </item>
    <item>
      <title>Managing Secret For Your Golang Apps With The GCP Secret Manager</title>
      <dc:creator>Razan Fawwaz</dc:creator>
      <pubDate>Wed, 01 Apr 2026 05:02:19 +0000</pubDate>
      <link>https://dev.to/gdg/managing-secret-for-your-golang-apps-with-the-gcp-secret-manager-3glj</link>
      <guid>https://dev.to/gdg/managing-secret-for-your-golang-apps-with-the-gcp-secret-manager-3glj</guid>
      <description>&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%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A1400%2Fformat%3Awebp%2F0%2A6M2UM2lycBT1iEMp" 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%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A1400%2Fformat%3Awebp%2F0%2A6M2UM2lycBT1iEMp" alt="Photo by Aneta Pawlik on Unsplash" width="1400" height="933"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;While developing a serverless application and having a secret key in JSON format, I always looked at how we store that file securely. We can’t save the JSON file in our public repository, right? ☠️&lt;/p&gt;

&lt;p&gt;Since I plan to deploy the application using Google Cloud Run, I’ve found that Google Cloud has a &lt;a href="https://cloud.google.com/security/products/secret-manager" rel="noopener noreferrer"&gt;Secret Manager&lt;/a&gt; service!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Store API keys, passwords, certificates, and sensitive data&lt;/p&gt;

&lt;p&gt;Secret Manager is a secure and convenient storage system for API keys, passwords, certificates, and other sensitive data&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Google Cloud&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;With Secret Manager, we can store the credentials to the Secret Manager and integrate our application to “take” the credentials from the Secret Manager.&lt;/p&gt;

&lt;p&gt;There are two ways to store the secret, we can use the console or CLI, for this time I will use the CLI.&lt;/p&gt;

&lt;p&gt;For example, I have a simple Golang app that has a file called secret-key.json for authentication to database services. I want to deploy the application using Cloud Run and using the secret key that is provided so the service can communicated to the database.&lt;/p&gt;

&lt;p&gt;I already downloaded the key and am ready to put it on Secret Manager. First of all, make sure your devices are already authenticated with your Google Cloud account.&lt;/p&gt;

&lt;p&gt;You can log in to your account by using this command&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud auth login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;After that, set to using your project&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud config &lt;span class="nb"&gt;set &lt;/span&gt;project &amp;lt;PROJECT_ID&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The next step is we should enable the API of the Secret Manager service. You can use this command to enable it.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud services &lt;span class="nb"&gt;enable &lt;/span&gt;secretmanager.googleapis.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;If the message returns “Operation …….. finished successfully” now we’re ready to go 🚀&lt;/p&gt;

&lt;p&gt;Now, we’re heading to the directory where we store the secret key, or you can use any directory and set it the path on the command, here is the command that we will use&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud secrets create &amp;lt;SECRET_NAME&amp;gt; &lt;span class="nt"&gt;--data-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;SECRET_FILE_PATH&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The image below shows the results if we already created the secret in Secret Manager.&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.amazonaws.com%2Fuploads%2Farticles%2Fcwo79ixwik105hjmmkr7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcwo79ixwik105hjmmkr7.png" alt="captionless image" width="800" height="120"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Or you can check it from the Google Cloud Console and go to the Secret Manager page.&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.amazonaws.com%2Fuploads%2Farticles%2Fva34acvluw2vbuw2zxg2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fva34acvluw2vbuw2zxg2.png" alt="captionless image" width="800" height="386"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now this is the Go code looks alike, since we just need to read the data, the process on this code is just to retrieve the secret. You should fill out the &lt;code&gt;GOOGLE_CLOUD_PROJECT_NUMBER&lt;/code&gt; you can hardcode it or use OS Env.&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;main&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;"context"&lt;/span&gt;
 &lt;span class="s"&gt;"log"&lt;/span&gt;
 &lt;span class="n"&gt;secretsmanager&lt;/span&gt; &lt;span class="s"&gt;"cloud.google.com/go/secretmanager/apiv1"&lt;/span&gt;
 &lt;span class="s"&gt;"cloud.google.com/go/secretmanager/apiv1/secretmanagerpb"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="n"&gt;projectNumber&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"GOOGLE_CLOUD_PROJECT_NUMBER"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="n"&gt;secretName&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SECRET_NAME"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
 &lt;span class="n"&gt;client&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="n"&gt;secretsmanager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&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;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="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatalf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"failed to setup client: %v"&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;defer&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
 &lt;span class="n"&gt;accessRequest&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;secretmanagerpb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AccessSecretVersionRequest&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"projects/"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;projectNumber&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"/secrets/"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;secretName&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"/versions/latest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="p"&gt;}&lt;/span&gt;
 &lt;span class="n"&gt;result&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="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AccessSecretVersion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;accessRequest&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;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="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatalf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"failed to access secret version: %v"&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="n"&gt;secret&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Payload&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"secret: %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;secret&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;While running it locally, don’t forget to authenticate the program with the Google Cloud Project by using this command. For more reference, you can check &lt;a href="https://cloud.google.com/docs/authentication/set-up-adc-local-dev-environment" rel="noopener noreferrer"&gt;this out&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud auth application-default login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Then, you can run the program using &lt;code&gt;go run main.go&lt;/code&gt; and here we go, this is the secret that we stored it before!&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.amazonaws.com%2Fuploads%2Farticles%2Fbcs8gcon4hxziz44zpy6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbcs8gcon4hxziz44zpy6.png" alt="captionless image" width="800" height="149"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you want to deploy it to Cloud Run, you can try this code&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;main&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;"context"&lt;/span&gt;
 &lt;span class="s"&gt;"fmt"&lt;/span&gt;
 &lt;span class="s"&gt;"log"&lt;/span&gt;
 &lt;span class="s"&gt;"net/http"&lt;/span&gt;
 &lt;span class="s"&gt;"os"&lt;/span&gt;
 &lt;span class="n"&gt;secretsmanager&lt;/span&gt; &lt;span class="s"&gt;"cloud.google.com/go/secretmanager/apiv1"&lt;/span&gt;
 &lt;span class="s"&gt;"cloud.google.com/go/secretmanager/apiv1/secretmanagerpb"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="n"&gt;projectNumber&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"GOOGLE_CLOUD_PROJECT_NUMBER"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="n"&gt;secretName&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SECRET_NAME"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
 &lt;span class="n"&gt;client&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="n"&gt;secretsmanager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&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;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="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatalf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"failed to setup client: %v"&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;defer&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
 &lt;span class="n"&gt;accessRequest&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;secretmanagerpb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AccessSecretVersionRequest&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"projects/"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;projectNumber&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"/secrets/"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;secretName&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"/versions/latest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="p"&gt;}&lt;/span&gt;
 &lt;span class="n"&gt;result&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="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AccessSecretVersion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;accessRequest&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;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="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatalf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"failed to access secret version: %v"&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="n"&gt;secret&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Payload&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandleFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&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;Fprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Your secret is: %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="p"&gt;})&lt;/span&gt;
 &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ListenAndServe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;":8080"&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;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/razanfawwaz" rel="noopener noreferrer"&gt;
        razanfawwaz
      &lt;/a&gt; / &lt;a href="https://github.com/razanfawwaz/go-gcp-secret" rel="noopener noreferrer"&gt;
        go-gcp-secret
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Go Apps using Secret Manager
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;go-gcp-secret&lt;/h1&gt;

&lt;/div&gt;

&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
  &lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/razanfawwaz/go-gcp-secret" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;Don’t forget to put the Env Variables while setting up the Cloud Run deployment.&lt;/p&gt;

&lt;p&gt;If the deployment succeeds, you can try to call the “/” endpoint, and it will return the secret key that we need.&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.amazonaws.com%2Fuploads%2Farticles%2Fweh7y89oa4xdw8wjice1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fweh7y89oa4xdw8wjice1.png" alt="captionless image" width="800" height="117"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Additionally, if the Cloud Run needs access to the Secret Manager, you can go to the Secret Manager and give access to your Cloud Run Service Account by clicking &lt;code&gt;Add Principal&lt;/code&gt;.&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.amazonaws.com%2Fuploads%2Farticles%2F3pwjnblr6eee1ag2xybv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3pwjnblr6eee1ag2xybv.png" alt="captionless image" width="800" height="386"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That’s all! 🚀 Thanks for reading!&lt;/p&gt;

</description>
      <category>secretmanager</category>
      <category>gcp</category>
      <category>cloudrun</category>
      <category>programming</category>
    </item>
    <item>
      <title>Scaling your productivity with spec docs in your IDE - Anti Gravity.</title>
      <dc:creator>Matthew Christiansen</dc:creator>
      <pubDate>Thu, 26 Mar 2026 21:49:41 +0000</pubDate>
      <link>https://dev.to/gdg/teaching-gemini-to-scale-your-workflow-in-anti-gravity-om2</link>
      <guid>https://dev.to/gdg/teaching-gemini-to-scale-your-workflow-in-anti-gravity-om2</guid>
      <description>&lt;p&gt;Google’s Anti Gravity is built on a simple premise: remove friction so developers can stay in "the flow." But the real unlock isn’t just the IDE itself, it’s how you architect your interactions with the AI inside it. While this article is relevant for any IDE you are using with AI, if you aren't using it yet, I would recommend you take a look at Anti Gravity. &lt;/p&gt;

&lt;p&gt;What I am doing with my IDE and AI now:&lt;br&gt;
I’ve started moving away from "chatting" with AI. Instead, I’ve been creating small .md files that act as Instruction Layers for Gemini. This simple shift turns an LLM from a conversational assistant into a predictable, modular part of my technical stack.&lt;/p&gt;

&lt;p&gt;The Core Concept: &lt;/p&gt;

&lt;p&gt;Prompts as Configuration&lt;br&gt;
Instead of re-explaining my requirements every time I open a terminal, I treat prompts like Angular Components. I create a simple Markdown file that tells Gemini exactly how to behave for a specific task. There are also versions of doing this in certain tools branded as workflows, skills, rules, etc but the premise really is just writing something down easily referencable by your ide to regularly bring into your AI prompts without re-writing that will point your LLM in the right direction when helping with code.&lt;/p&gt;

&lt;p&gt;When you find yourself prompting the same thing multiple times, whether in Anti Gravity or any integrated IDE, you shouldn't be typing; you should be referencing a spec and updating it.&lt;/p&gt;

&lt;p&gt;An "Angular-Style" Instruction File:&lt;/p&gt;

&lt;p&gt;Encapsulated: A clearly defined purpose.&lt;br&gt;
Reusable: Use it across any feature branch.&lt;br&gt;
Consistent: Standardized output every single time.&lt;/p&gt;

&lt;p&gt;Example: pr-assistant.md&lt;/p&gt;

&lt;h1&gt;
  
  
  PR Comment Assistant
&lt;/h1&gt;

&lt;p&gt;Goal: &lt;br&gt;
Draft and post high-quality GitHub PR comments via the git CLI.&lt;/p&gt;

&lt;p&gt;Rules:&lt;br&gt;
Keep feedback actionable, professional, and concise.&lt;br&gt;
Call out uncertainty explicitly; no speculation.&lt;/p&gt;

&lt;p&gt;Structure: &lt;br&gt;
Context → Suggestion → Reasoning.&lt;/p&gt;

&lt;p&gt;Execution:&lt;br&gt;
Output via git CLI; do not use inline IDE comments.&lt;br&gt;
Treat comments as notes for reviewers and future contributors.&lt;/p&gt;

&lt;p&gt;Why This Works (Anti Gravity in Practice):&lt;br&gt;
Without structure, you suffer from Prompt Drift. Your instructions get lazy, the output becomes inconsistent, and you repeat yourself constantly.&lt;/p&gt;

&lt;p&gt;By using .md instruction files, you achieve Cognitive Offloading:&lt;/p&gt;

&lt;p&gt;Predictability: &lt;br&gt;
Gemini follows the spec, not the "vibe" of your last message.&lt;br&gt;
Efficiency: &lt;br&gt;
You stay focused in your IDE, invoking capabilities rather than brainstorming prompts.&lt;br&gt;
Scalability: &lt;br&gt;
These files live in your repo. They evolve with your codebase, they’re version-controlled, and they can be shared across your entire team.&lt;br&gt;
Angular as the Language of Focus:&lt;br&gt;
This approach mirrors core Angular principles almost perfectly:&lt;br&gt;
Separation of Concerns: &lt;br&gt;
Each .md file has one specific job. Reusability: The same instruction file works across different PRs and projects.&lt;br&gt;
Consistency: &lt;br&gt;
You get standardized outputs, every time. Instead of just "talking" to an AI, you are composing it into your development environment.&lt;/p&gt;

&lt;p&gt;Closing Thought&lt;br&gt;
The real power of Anti Gravity within an Angular codebase isn’t just writing code faster, it’s reducing the mental overhead of the development process.&lt;/p&gt;

&lt;p&gt;When you pair a high-performance IDE with small, well-designed instruction files, you get something bigger than an assistant. You get a custom-built, automated collaborator.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>antigravity</category>
      <category>angular</category>
      <category>gdg</category>
    </item>
    <item>
      <title>When Zero‑Width Isn’t Zero: How I Found and Fixed a Vulnerability</title>
      <dc:creator>Karol Wrótniak</dc:creator>
      <pubDate>Wed, 28 Jan 2026 16:01:56 +0000</pubDate>
      <link>https://dev.to/gdg/when-zero-width-isnt-zero-how-i-found-and-fixed-a-vulnerability-16hi</link>
      <guid>https://dev.to/gdg/when-zero-width-isnt-zero-how-i-found-and-fixed-a-vulnerability-16hi</guid>
      <description>&lt;p&gt;When you set a max length on a form field or API, you expect it to hold. But what if a four-character string could secretly carry 10,000 extra bytes of invisible data, crashing your database or bypassing your validation? That was the vulnerability I found and fixed in the popular JavaScript library validator. It was a subtle bug involving Unicode Variation Selectors that allowed attackers to inject massive payloads while still passing length checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;The isLength function is simple on the surface: count characters and compare to a limit. But in a Unicode world, “character” is tricky. You need to match the perceived length (what the user sees) with the storage length (what your database handles), or you risk truncation, performance issues, and, as I found, critical bypasses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unicode and the illusion of length
&lt;/h2&gt;

&lt;p&gt;JavaScript strings use &lt;a href="https://pl.wikipedia.org/wiki/UTF-16" rel="noopener noreferrer"&gt;UTF‑16&lt;/a&gt;. Some visible “characters” span two code units (surrogate pairs). isLength adjusts for that. It treats a base character plus a &lt;a href="https://en.wikipedia.org/wiki/Variation_Selectors_(Unicode_block)" rel="noopener noreferrer"&gt;Variation Selector&lt;/a&gt; as one perceived character. That’s because the selector only changes the presentation.&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;// Emoji still count as one perceived character&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;smile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;smile&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;// 2 (UTF-16 code units)&lt;/span&gt;
&lt;span class="c1"&gt;// With isLength, it's treated as 1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The attack vector: Variation selectors gone wild
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://jeffkreeftmeijer.com/unicode-variation-selectors/" rel="noopener noreferrer"&gt;Variation Selectors&lt;/a&gt; (&lt;code&gt;U+FE0F&lt;/code&gt;, &lt;code&gt;U+FE0E&lt;/code&gt;) tweak how a base character displays. They aren’t content on their own. The old logic subtracted all selectors. So one can pad a short string with thousands of them while still passing the check with a low max.&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;// Apparent 4 chars, but thousands of extra selectors&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s1"&gt;uFE0F&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&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;// 10,004 (UTF-16)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What are unicode variation selectors?
&lt;/h2&gt;

&lt;p&gt;These are zero‑width code points (U+FE0E for text and U+FE0F for emoji among the others) that modify the presentation of the character that immediately precedes them. They change appearance, not meaning. A base character plus a selector is meant to count as one perceived character.&lt;/p&gt;

&lt;p&gt;As of &lt;a href="https://www.unicode.org/versions/Unicode17.0.0/" rel="noopener noreferrer"&gt;Unicode 17.0&lt;/a&gt;, using them to choose emoji vs. text for many legacy &lt;a href="https://en.wikipedia.org/wiki/Dingbat" rel="noopener noreferrer"&gt;dingbat&lt;/a&gt; bases is being phased out. The new emojis have separate code points assigned for each style. For example, the emoji 🪯 (U+1FAAF KHANDA) has a dedicated emoji code point. The older dingbat-style symbol is ☬ (U+262C ADI SHAKTI). Variation selectors still exist and work for bases that support them. But modern additions increasingly prefer distinct code points over selector‑based presentation. Unicode provides the &lt;a href="https://www.unicode.org/emoji/charts/emoji-variants.html" rel="noopener noreferrer"&gt;emoji presentation sequences&lt;/a&gt; chart.&lt;/p&gt;

&lt;p&gt;Here are some examples (text vs. emoji):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Heart: ❤︎ (text, U+2764 U+FE0E) vs ❤️ (emoji, U+2764 U+FE0F)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Airplane: ✈︎ (text, U+2708 U+FE0E) vs ✈️ (emoji, U+2708 U+FE0F)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Snowman: ☃︎ (text, U+2603 U+FE0E) vs ☃️ (emoji, U+2603 U+FE0F)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Writing hand: ✍︎ (text, U+270D U+FE0E) vs ✍️ (emoji, U+270D U+FE0F)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Telephone: ☎︎ (text, U+260E U+FE0E) vs ☎️ (emoji, U+260E U+FE0F)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Console example:&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;// Base character: Heavy Black Heart&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;heart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s1"&gt;u2764&lt;/span&gt;&lt;span class="dl"&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;heartText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;heart&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s1"&gt;uFE0E&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// request text style&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;heartEmoji&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;heart&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s1"&gt;uFE0F&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// request emoji style&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;heart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;heartText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;heartEmoji&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;//  ❤︎ &lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default, (with no selector), presentation differs by platform, browser, and font. Some show emoji by default; others prefer text. If you need a specific look, add U+FE0E (text) or U+FE0F (emoji). On the web, you can use the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/font-variant-emoji" rel="noopener noreferrer"&gt;font-variant-emoji&lt;/a&gt; CSS property.&lt;/p&gt;

&lt;p&gt;Important: A selector only makes sense right after a compatible base. Multiple selectors don’t stack or change meaning. ❤︎\uFE0F\uFE0F doesn’t become “more emoji.” Only the first base+selector pair affects the presentation. All the extras are stray code points.&lt;/p&gt;

&lt;p&gt;For validation, ignore only one base+selector pair. Count stray or repeated selectors toward length. Otherwise, a tiny word can hide kilobytes and waste CPU. You can find more information about the issue in the Snyk Vulnerability database under &lt;a href="https://security.snyk.io/vuln/SNYK-JS-VALIDATOR-13653476" rel="noopener noreferrer"&gt;SNYK-JS-VALIDATOR-13653476&lt;/a&gt; entry.&lt;/p&gt;

&lt;h2&gt;
  
  
  The surgical fix: Counting only valid pairs
&lt;/h2&gt;

&lt;p&gt;The change is precise: subtract only base+selector pairs instead of every selector. Old: &lt;code&gt;/(\uFE0F|\uFE0E)/g&lt;/code&gt;. New: &lt;code&gt;/[^\uFE0F\uFE0E][\uFE0F\uFE0E]/g&lt;/code&gt;. &lt;code&gt;[^…]&lt;/code&gt; means “not these,” which forces a preceding non‑selector base. So stray or repeated selectors get counted. This distinction is the key to the fix.&lt;/p&gt;

&lt;p&gt;Effective length: &lt;code&gt;str.length — surrogatePairs — basePlusSelectorPairs&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;// Padding with stray selectors now fails the max check&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s1"&gt;uFE0F&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;isLength(payload, { max: 4 }):&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;isLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt; &lt;span class="c1"&gt;// false&lt;/span&gt;

&lt;span class="c1"&gt;// A valid base+selector pair still counts as one perceived char&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;basePair&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;A&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s1"&gt;uFE0F&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;isLength(basePair, { max: 1 }):&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;isLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;basePair&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="p"&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;// true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Disclosure and timeline
&lt;/h2&gt;

&lt;p&gt;I followed a responsible disclosure process. I reported the issue, prepared a minimal PoC, and worked with the maintainers on a fix. The maintainers merged the patch, published a release, and then the advisory went live.&lt;/p&gt;

&lt;p&gt;I reported the issue along with a &lt;a href="https://github.com/validatorjs/validator.js/pull/2616" rel="noopener noreferrer"&gt;proposed fix&lt;/a&gt; on October 18, 2025. The maintainers merged a pull request on November 5. Finally, &lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2025-12758" rel="noopener noreferrer"&gt;CVE-2025–12758&lt;/a&gt; was published on November 26. More info and links on the &lt;a href="https://github.com/advisories/GHSA-vghf-hv5q-vc2g" rel="noopener noreferrer"&gt;GitHub Security Advisory&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;The key takeaway remains: Variation Selectors are modifiers, not content. The bug allowed them to be used as invisible padding to bypass max-length checks. After the fix only valid base-and-selector pairs pass the length check. It ensures all stray or repeated selectors are counted toward the actual length.&lt;/p&gt;

&lt;p&gt;Please update to &lt;code&gt;validator@13.15.22&lt;/code&gt; or newer. When working with string length, always measure what you are storing, not just what users see.&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>javascript</category>
      <category>node</category>
      <category>vulnerabilities</category>
    </item>
    <item>
      <title>A Practical Guide to Flutter Accessibility - Part 1: The Basics</title>
      <dc:creator>Karol Wrótniak</dc:creator>
      <pubDate>Fri, 07 Nov 2025 12:08:20 +0000</pubDate>
      <link>https://dev.to/gdg/a-practical-guide-to-flutter-accessibility-part-1-the-basics-4bf1</link>
      <guid>https://dev.to/gdg/a-practical-guide-to-flutter-accessibility-part-1-the-basics-4bf1</guid>
      <description>&lt;p&gt;Learn how to build accessible Flutter apps using built-in tools. Fix common issues like screen reader incompatibility, confusing structure, and unclear state changes to create inclusive, user-friendly apps.&lt;/p&gt;

&lt;p&gt;Building accessible Flutter apps isn’t as complicated as you might think. Most accessibility problems happen because screen readers can’t understand your controls. Your information structure confuses users. You’re not communicating state changes. This guide will fix these problems using Flutter’s built-in accessibility tools.&lt;/p&gt;

&lt;p&gt;You’ll start by making silent controls speak to screen readers with proper labels and hints. Assistive technology users will get the same information as everyone else.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this matters to you as a developer
&lt;/h3&gt;

&lt;p&gt;Accessibility isn’t just about doing the right thing (though that matters too). It’s about building better software. When you add proper semantic information to your widgets, you help more than screen reader users. You create clearer code that’s easier to test, debug, and maintain. Many accessibility improvements make the experience better for everyone — better focus management, clearer state communication.&lt;/p&gt;

&lt;h3&gt;
  
  
  The legal reality
&lt;/h3&gt;

&lt;p&gt;Let’s talk about the elephant in the room: legal requirements. The EU Accessibility Act requires digital services to be accessible by 2025. Fines reach up to 4% of annual revenue for non-compliance. In the US, the ADA doesn’t specify technical standards. Courts reference WCAG guidelines in lawsuits. Companies like Target, Netflix, and Domino’s have faced million-dollar settlements over inaccessible apps. This isn’t some distant possibility — it’s happening right now.&lt;/p&gt;

&lt;h2&gt;
  
  
  The good news about common problems
&lt;/h2&gt;

&lt;p&gt;The most common Flutter accessibility problems are simple to fix. An IconButton without a label? Silent to screen readers. You’ll fix that using Flutter’s Semantics and MergeSemantics widgets.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Semantics widget
&lt;/h2&gt;

&lt;p&gt;Think of the Semantics widget as an invisible layer of sticky notes for assistive technology. You wrap your existing UI and provide properties. These describe what the thing is (label, role). They show what it displays (value). They explain what it’ll do (hint). They communicate what state it’s in (flags like selected, hidden, header, button, enabled, liveRegion). You can expose actions (onTap, onLongPress, onScroll) so screen reader users can interact with your interface. You can group multiple visual widgets into a single logical unit.&lt;/p&gt;

&lt;p&gt;You won’t use Semantics everywhere — Flutter’s smart about defaults for Material widgets. But you’ll add or override it when something’s silent, chatty, or missing important state information. The goal? Give just enough structured metadata so a user hears “Add to cart, button, selected” instead of a confusing jumble of unrelated text bits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Giving a voice to silent icons
&lt;/h2&gt;

&lt;p&gt;Here’s our first problem. You’ve got an IconButton that looks fine. For a screen reader, it’s a silent mystery. Wrap it with Semantics and give it a clear label (what it is) and optional hint (what happens when you tap it).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Without Semantics: announces only "button" because the icon has no text.&lt;/span&gt;
&lt;span class="n"&gt;IconButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;icon:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Icon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Icons&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;local_drink&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nl"&gt;onPressed:&lt;/span&gt; &lt;span class="n"&gt;_incrementCounter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Logs a glass but screen reader doesn't know.&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// With Semantics: announces "Log water, button" then the hint on explore.&lt;/span&gt;
&lt;span class="n"&gt;Semantics&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;label:&lt;/span&gt; &lt;span class="s"&gt;'Log water'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;              &lt;span class="c1"&gt;// What this control represents&lt;/span&gt;
  &lt;span class="nl"&gt;hint:&lt;/span&gt; &lt;span class="s"&gt;'Adds one glass to today&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s"&gt;s total'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// What happens on activation.&lt;/span&gt;
  &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;IconButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;icon:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Icon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Icons&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;plus_one&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nl"&gt;onPressed:&lt;/span&gt; &lt;span class="n"&gt;_incrementCounter&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;Keep labels short and specific. Don’t repeat role words like “button” — the system adds that for you. Use value when the visual text alone would be confusing out of context (like an isolated number that means nothing without context).&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding other semantic properties
&lt;/h2&gt;

&lt;p&gt;Many other semantic properties solve specific accessibility problems. liveRegion makes dynamic content announce when it changes — perfect for status updates or form validation messages. The isHeader flag tells screen readers that text is a page or section heading. This helps users navigate by jumping between sections. We can’t cover every semantic property in one post. You’ll learn about these and others as we work through real examples. For the complete rundown, check out the &lt;a href="https://api.flutter.dev/flutter/widgets/Semantics-class.html" rel="noopener noreferrer"&gt;official Semantics documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Platform differences to watch
&lt;/h2&gt;

&lt;p&gt;Not all semantic properties work the same way across platforms. Some platforms support certain properties. Others might behave differently. For example, headingLevel only works on Flutter web. Native iOS supports heading levels. Flutter doesn’t wire this through on iOS (&lt;a href="https://github.com/flutter/flutter/issues/155928" rel="noopener noreferrer"&gt;there’s an open issue&lt;/a&gt; in the Flutter tracker). Android doesn’t even have the concept of heading levels.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coming from Android development
&lt;/h2&gt;

&lt;p&gt;Do you use Jetpack Compose? Flutter’s Semantics widget works like Compose’s semantics modifier. Both use the same basic idea — wrap UI elements and add metadata like contentDescription (Flutter’s label), role information, and state flags. The main difference is syntax. Flutter uses named parameters in a widget wrapper. Compose uses a modifier with lambda configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coming from iOS development
&lt;/h2&gt;

&lt;p&gt;SwiftUI’s accessibility system maps to Flutter’s approach. Flutter’s label property is like SwiftUI’s .accessibilityLabel() modifier. hint maps to .accessibilityHint(). State flags like isHeader correspond to .accessibilityAddTraits(.isHeader). All three frameworks — Flutter, &lt;a href="https://developer.android.com/jetpack/compose/semantics" rel="noopener noreferrer"&gt;Jetpack Compose&lt;/a&gt;, and &lt;a href="https://developer.apple.com/documentation/swiftui/view-accessibility" rel="noopener noreferrer"&gt;SwiftUI&lt;/a&gt; — follow the same core principle: decorate UI elements with semantic metadata. Once you get the concept, switching between platforms becomes easier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing your semantics on device
&lt;/h2&gt;

&lt;p&gt;Let’s see what happens when you run the IconButton example on an Android device with TalkBack enabled. The screen recording below shows the app running on an emulator and the Android Ally plugin in Android Studio. This gives you a real-time control of accessibility settings.&lt;/p&gt;

&lt;p&gt;&lt;iframe src="https://player.vimeo.com/video/1134569751" width="710" height="399"&gt;
&lt;/iframe&gt;
&lt;br&gt;
Android emulator with TalkBack and Android Ally plugin.&lt;/p&gt;
&lt;h2&gt;
  
  
  The double announcement problem
&lt;/h2&gt;

&lt;p&gt;When you swipe through the interface with TalkBack (Android’s built-in screen reader), you’ll notice something weird. Both the Semantics wrapper and the underlying IconButton get announced as separate nodes. TalkBack first reads “Log water, button, Adds one glass to today’s total” from your custom semantics. Then it announces the icon button itself as a separate focusable element.&lt;/p&gt;

&lt;p&gt;This creates a confusing experience for screen reader users. They hear the same control twice but with different information. The &lt;a href="https://plugins.jetbrains.com/plugin/7299-android-ally" rel="noopener noreferrer"&gt;Android Ally plugin&lt;/a&gt; helps you catch these issues. It shows the accessibility tree structure alongside your app. You can grab it from Android Studio’s plugin marketplace to see how assistive technologies interpret your interface.&lt;/p&gt;
&lt;h2&gt;
  
  
  Using Accessibility Scanner
&lt;/h2&gt;

&lt;p&gt;Google’s Accessibility Scanner app catches this exact problem. The scanner analyzes your app’s interface and flags accessibility issues. It finds duplicate content descriptions, missing labels, and poor contrast ratios. When you run it on our IconButton example, it spots the redundant semantic information. It suggests consolidating everything into a single, clear description.&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.amazonaws.com%2Fuploads%2Farticles%2Feabeixfbnyh5et559yo9.jpeg" 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.amazonaws.com%2Fuploads%2Farticles%2Feabeixfbnyh5et559yo9.jpeg" alt="Accessibility scanner detecting duplicate content descriptions." width="720" height="1600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can download &lt;a href="https://play.google.com/store/apps/details?id=com.google.android.apps.accessibility.auditor" rel="noopener noreferrer"&gt;Accessibility Scanner&lt;/a&gt; from the Google Play Store. Run it on your device or emulator. It’s useful during development because it gives you the same feedback that real users with disabilities would get. You don’t have to enable TalkBack yourself.&lt;/p&gt;
&lt;h2&gt;
  
  
  Testing on iOS
&lt;/h2&gt;

&lt;p&gt;On iOS, Apple’s &lt;a href="https://developer.apple.com/documentation/accessibility/accessibility-inspector" rel="noopener noreferrer"&gt;Accessibility Inspector&lt;/a&gt; does the same thing. This built-in developer tool comes with Xcode. It lets you inspect the accessibility tree of your Flutter app running on iOS Simulator or a physical device. When you run the inspector on our problematic IconButton example, it reveals the same issue. You see duplicate semantic nodes that would confuse VoiceOver users.&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.amazonaws.com%2Fuploads%2Farticles%2Fax88h9qz75ie8d6ttigp.jpeg" 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.amazonaws.com%2Fuploads%2Farticles%2Fax88h9qz75ie8d6ttigp.jpeg" alt="Apple Accessibility Inspector showing duplicate semantic elements." width="800" height="739"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can find the Accessibility Inspector in Xcode under Developer Tools. Or launch it from your Applications/Utilities folder if you’ve got Command Line Tools installed. Unlike the Android tools, Accessibility Inspector works with Flutter apps in iOS Simulator. This makes it easy to catch accessibility issues during development without needing a physical device.&lt;/p&gt;

&lt;p&gt;Why does this double announcement happen? You wrapped the IconButton without telling Flutter to treat them as a single semantic unit. Let’s fix this by learning how to group related elements.&lt;/p&gt;
&lt;h2&gt;
  
  
  Fixing double announcements with MergeSemantics
&lt;/h2&gt;

&lt;p&gt;The solution to our double announcement problem is simpler than you might expect. We need to tell Flutter to merge the semantic information from our custom Semantics wrapper with the underlying IconButton. This creates a single focusable element. The MergeSemantics widget does this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Fixed: Now announces as a single element.&lt;/span&gt;
&lt;span class="n"&gt;MergeSemantics&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Semantics&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;label:&lt;/span&gt; &lt;span class="s"&gt;'Log a glass of water'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     
    &lt;span class="nl"&gt;hint:&lt;/span&gt; &lt;span class="s"&gt;'Adds one glass to today&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s"&gt;s total'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;IconButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;icon:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Icon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Icons&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;plus_one&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nl"&gt;onPressed:&lt;/span&gt; &lt;span class="n"&gt;_incrementCounter&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;When you wrap multiple widgets with MergeSemantics, Flutter combines all their semantic properties into a single accessibility node. Screen readers now announce “Log a glass of water, button, Adds one glass to today’s total” as one cohesive unit. They don’t treat each wrapper as a separate element.&lt;/p&gt;

&lt;p&gt;The MergeSemantics widget is handy when you’ve got compound controls. Think of a button with an icon and text. Or a custom card with multiple interactive elements that should be treated as one logical unit. Without it, screen readers navigate to each semantic layer. This creates confusion for users who expect related elements to be grouped together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cross-platform grouping concepts
&lt;/h2&gt;

&lt;p&gt;If you’re working with SwiftUI, this concept maps to the .accessibilityElement(children: .combine) view modifier. This merges accessibility information from child views into a single element. In Jetpack Compose, you get grouping using the mergeDescendants = true parameter in the semantics modifier. All three frameworks recognize that visual hierarchy doesn’t match logical accessibility structure. Multiple UI elements should be announced as one cohesive unit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the fixed implementation
&lt;/h2&gt;

&lt;p&gt;Now when you test the corrected code with TalkBack or VoiceOver, you’ll hear a single, clear announcement. No more confusing double reading we had before. The screen recording below shows the same setup as earlier. The app runs with both the Android Ally plugin and Accessibility Scanner. This time it displays the clean, merged semantic structure.&lt;/p&gt;

&lt;p&gt;&lt;iframe src="https://player.vimeo.com/video/1134570595" width="710" height="399"&gt;
&lt;/iframe&gt;
&lt;br&gt;
MergeSemantics fixing the double announcement issue.&lt;/p&gt;

&lt;p&gt;Notice how the accessibility tree now shows just one focusable element instead of those duplicate nodes you saw earlier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Confirming results on iOS
&lt;/h2&gt;

&lt;p&gt;On iOS, the Accessibility Inspector confirms the same clean result. When you run the corrected MergeSemantics code through Apple’s accessibility tools, the inspector shows a single, merged semantic node. No duplicate announcements or structural issues.&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.amazonaws.com%2Fuploads%2Farticles%2Fv50d22dazdbjxuiugof7.jpeg" 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.amazonaws.com%2Fuploads%2Farticles%2Fv50d22dazdbjxuiugof7.jpeg" alt="iOS Accessibility Inspector showing the clean, merged semantic structure." width="800" height="652"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Both TalkBack and the Accessibility Scanner confirm that those redundant announcements are gone. This creates a much better experience for screen reader users.&lt;/p&gt;

&lt;p&gt;For more details on semantic grouping options, check out the &lt;a href="https://api.flutter.dev/flutter/widgets/MergeSemantics-class.html" rel="noopener noreferrer"&gt;official MergeSemantics documentation&lt;/a&gt; and the broader &lt;a href="https://api.flutter.dev/flutter/widgets/Semantics-class.html" rel="noopener noreferrer"&gt;Semantics class reference&lt;/a&gt;. These cover all available properties and grouping strategies.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you’ve accomplished
&lt;/h2&gt;

&lt;p&gt;You’ve now tackled the most common Flutter accessibility problems using the core toolkit. Silent controls like IconButton now speak with proper labels and hints. Those confusing double announcements? Cleaned up with MergeSemantics. Your app’s semantic structure makes sense to screen readers. You’ve got the tools to test and verify your changes work across both Android and iOS.&lt;/p&gt;

&lt;p&gt;These basic semantic properties — solve the vast majority of the accessibility issues you’ll run into in Flutter apps. But there’s more to building inclusive experiences. In Part 2 of this series, you’ll dive into advanced interaction patterns like custom semantic actions for complex gestures.You’ll explore how to handle dynamic content announcements and make custom widgets accessible to assistive technologies.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at&lt;/em&gt; &lt;a href="https://www.thedroidsonroids.com/blog/flutter-accessibility-guide-part-1" rel="noopener noreferrer"&gt;&lt;em&gt;thedroidsonroids.com&lt;/em&gt;&lt;/a&gt; &lt;em&gt;on November 7, 2025.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>flutter</category>
      <category>ios</category>
      <category>a11y</category>
    </item>
  </channel>
</rss>
