<?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: UK</title>
    <description>The latest articles on DEV Community by UK (@utokazu).</description>
    <link>https://dev.to/utokazu</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4162737%2F2d9132a2-e148-439e-8cb4-7b760d181db1.png</url>
      <title>DEV Community: UK</title>
      <link>https://dev.to/utokazu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/utokazu"/>
    <language>en</language>
    <item>
      <title>When Incident Reports Say 'Found Traces of Unauthorized Access', What Are They Actually Looking At?</title>
      <dc:creator>UK</dc:creator>
      <pubDate>Wed, 21 Oct 2026 09:57:20 +0000</pubDate>
      <link>https://dev.to/utokazu/when-incident-reports-say-found-traces-of-unauthorized-access-what-are-they-actually-looking-at-3lb4</link>
      <guid>https://dev.to/utokazu/when-incident-reports-say-found-traces-of-unauthorized-access-what-are-they-actually-looking-at-3lb4</guid>
      <description>&lt;h2&gt;
  
  
  If Your App Gets Attacked, How Would You Even Notice?
&lt;/h2&gt;

&lt;p&gt;Almost every week, we see security disclosure statements that include a familiar line:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Following an investigation, we confirmed traces of unauthorized data access by a third party."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every time I read articles like that, a natural question comes to mind: &lt;strong&gt;"When they say 'confirmed traces', what are they actually looking at to determine that?"&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;"If my own service were attacked, how would I notice?"&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"How do you differentiate malicious scrapers from normal users?"&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you don’t have a dedicated infrastructure or SecOps team, not knowing how to verify facts during a potential incident causes unnecessary anxiety.&lt;/p&gt;

&lt;p&gt;Digging through the official documentation for Supabase and Vercel, I organized &lt;strong&gt;how I check for traces of suspicious access in a production Next.js + Supabase stack, and what defense layers I keep in place.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  1. A Traffic Spike Does NOT Equal Data Leak
&lt;/h2&gt;

&lt;p&gt;Imagine opening your Supabase dashboard one day and seeing a sharp, tower-like spike in &lt;strong&gt;Requests&lt;/strong&gt; or &lt;strong&gt;Egress (Data Transfer)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Your heart might skip a beat: &lt;em&gt;"Is our data being scraped right now?!"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;However, &lt;strong&gt;a traffic surge does not necessarily mean external data theft.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  ① High Chance It's a Self-Inflicted Bug
&lt;/h3&gt;

&lt;p&gt;Most traffic spikes in small projects aren't external attacks—they are internal issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Client-Side Mistakes&lt;/strong&gt;: Infinite &lt;code&gt;useEffect&lt;/code&gt; dependency loops or overly aggressive polling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Large Asset Delivery&lt;/strong&gt;: Serving uncompressed, raw videos or high-res images directly from Supabase Storage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Heavy Queries&lt;/strong&gt;: Full-table scans repeatedly running due to missing database indexes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database Migrations / Backups&lt;/strong&gt;: Bulk syncs during active development.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Always start by asking: &lt;em&gt;"Did we push a buggy commit recently?"&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  ② What If It Really Is an External Request Flood?
&lt;/h3&gt;

&lt;p&gt;Even if thousands of requests are hammering your endpoints from an external IP, &lt;strong&gt;it’s still too early to assume data was leaked.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is where &lt;strong&gt;RLS (Row-Level Security)&lt;/strong&gt; acts as your primary shield.&lt;/p&gt;

&lt;p&gt;When RLS is configured properly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Even if an unauthenticated attacker hits &lt;code&gt;https://&amp;lt;project&amp;gt;.supabase.co/rest/v1/feedbacks?select=*&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;The database returns &lt;strong&gt;an empty array &lt;code&gt;[]&lt;/code&gt;&lt;/strong&gt; with zero unauthorized rows.&lt;/li&gt;
&lt;li&gt;Crucially, PostgREST returns this as HTTP &lt;strong&gt;&lt;code&gt;200 OK&lt;/code&gt; (empty content)&lt;/strong&gt; rather than a 403 error.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words: &lt;strong&gt;Your request count can surge into the tens of thousands while zero user records leave your database (protected by RLS).&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Knowing this keeps you calm:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Is it an internal infinite loop?&lt;/li&gt;
&lt;li&gt;If external, did the responses actually contain payload data?&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  2. Practical Steps to Verify "Traces of Unauthorized Access"
&lt;/h2&gt;

&lt;p&gt;Regardless of how cloud dashboard UIs evolve over time, there are &lt;strong&gt;three universal traces&lt;/strong&gt; you should cross-examine across any database or web API:&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Cross-Examine the "3 Core Traces"
&lt;/h3&gt;

&lt;h4&gt;
  
  
  1. The Gap Between "Request Count" and "Data Transfer (Egress)"
&lt;/h4&gt;

&lt;p&gt;This is the most critical comparison:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Requests spike, but Egress stays flat&lt;/strong&gt;: Attackers are hitting your endpoints, but RLS/auth is returning empty arrays &lt;code&gt;[]&lt;/code&gt;. &lt;strong&gt;No data payload is leaving your database.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Requests spike, and Egress surges proportionally&lt;/strong&gt;: Real data rows are actively streaming out. This is your immediate red alert.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  2. Request Paths and Parameters
&lt;/h4&gt;

&lt;p&gt;Inspect the incoming request logs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Are requests targeting internal routes that normal users never navigate to directly?&lt;/li&gt;
&lt;li&gt;Are they appending greedy dump parameters like &lt;code&gt;?select=*&lt;/code&gt; or mass-fetching endpoints?&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  3. Skewed HTTP Status Codes
&lt;/h4&gt;

&lt;p&gt;Normal organic traffic is overwhelmingly &lt;code&gt;200 OK&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;401 Unauthorized&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;403 Forbidden&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;429 Too Many Requests&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A rapid burst of &lt;strong&gt;400-level errors in a short window&lt;/strong&gt; indicates an automated script or vulnerability scanner probing your endpoints for authentication holes.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 2: How to Identify "Suspicious Access"
&lt;/h3&gt;

&lt;p&gt;When checking unfamiliar client IPs using lookup tools:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Verification Point&lt;/th&gt;
&lt;th&gt;Regular Human Users&lt;/th&gt;
&lt;th&gt;Malicious Scanners / Bots&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Network (ASN / Org)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Consumer ISPs, mobile carriers (Comcast, AT&amp;amp;T, Docomo, etc.)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Cloud / Data Centers (AWS, DigitalOcean, Hetzner, OVH, etc.)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;User-Agent&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Chrome, Safari, standard mobile apps&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;python-requests&lt;/code&gt;, &lt;code&gt;curl&lt;/code&gt;, &lt;code&gt;Go-http-client&lt;/code&gt;, or empty&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Request Frequency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Natural human intervals (a few seconds to minutes)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Dozens to hundreds of calls per second&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Traffic Spread&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Natural page-to-page navigation flows&lt;/td&gt;
&lt;td&gt;Carpet-bombing a single endpoint repeatedly&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If a data center IP running a Python script is hammering a private API endpoint dozens of times per second, it is almost certainly an automated scanner.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Advance Preparation: Automated Alerting
&lt;/h2&gt;

&lt;p&gt;Manually checking dashboard logs every day isn't realistic when building solo. Setting up minimal baseline alerts ensures you get notified before small spikes become disasters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vercel Alert Notifications&lt;/strong&gt;:
Enable threshold alerts in &lt;code&gt;Settings &amp;gt; Notifications&lt;/code&gt; (and Spend Management) so you get notified when approaching usage limits or during security events.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supabase Billing Email &amp;amp; Over-Quota Alerts&lt;/strong&gt;:
Ensure your organization billing contact is set to an email inbox you actually monitor, so quota limit warnings never go unread.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  4. Multi-Layer Defense in Case of a Surge
&lt;/h2&gt;

&lt;p&gt;Here is the defensive hierarchy to keep in mind when traffic spikes occur:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[External Traffic / Scanners]
       │
       ▼
【Layer 1: Vercel WAF】
・Automated bot detection &amp;amp; blocking
・Flip on "Attack Mode" during sustained floods
       │
       ▼
【Layer 2: Supabase RLS】
・Even if requests hit the DB, unprivileged rows return empty []
       │
       ▼
【Layer 3: Spend Cap (Billing Guardrail)】
・Limits API overages to prevent runaway credit card bills
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  1. Vercel's Attack Mode (The Emergency Brake)
&lt;/h3&gt;

&lt;p&gt;Located under your project's &lt;strong&gt;&lt;code&gt;Security&lt;/code&gt; (or &lt;code&gt;Firewall&lt;/code&gt;)&lt;/strong&gt; tab:&lt;br&gt;
If you notice sustained malicious scraping, flipping this toggle forces suspected automated traffic to complete a challenge (like CAPTCHA), immediately shutting down automated scripts.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Supabase's Spend Cap (The Financial Safety Net)
&lt;/h3&gt;

&lt;p&gt;Under &lt;strong&gt;&lt;code&gt;Billing &amp;gt; Cost Control&lt;/code&gt;&lt;/strong&gt;:&lt;br&gt;
Keeping the &lt;strong&gt;Spend Cap "Enabled"&lt;/strong&gt; ensures that if metered limits are ever exceeded, the API gracefully throttles (HTTP 429) rather than racking up runaway cloud bills on your credit card.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. A Realistic Maintenance Routine for Indie Devs
&lt;/h2&gt;

&lt;p&gt;You don't need to spend hours auditing logs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Passive (Day-to-day)&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;If no alert emails arrive from Vercel or Supabase, do nothing. Focus 100% on product features.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weekly (Takes 2–3 minutes)&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;Glance at the usage charts for uncharacteristic request/egress spikes.&lt;/li&gt;
&lt;li&gt;Skim recent API logs to ensure 400-level errors aren't clustering abnormally.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;Reading news about "traces of unauthorized access" used to feel either like an abstract corporate problem or a terrifying unknown.&lt;/p&gt;

&lt;p&gt;By walking through official documentation and mapping out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;"Check Egress bytes vs Request counts first, don't panic"&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;"RLS protects the payload with empty arrays even under pressure"&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;"Set Spend Caps and alert emails to eliminate catastrophic risk"&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fog cleared up significantly, allowing me to focus on actual development.&lt;/p&gt;

&lt;p&gt;I hope this practical checklist helps you keep your apps safe and your mind at ease while shipping!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you keep tabs on anomalous traffic or potential scrapes in your own stack without dedicated SecOps? I’d love to hear your checklists and monitoring routines in the comments below!&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Official Documentation &amp;amp; References
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://supabase.com/docs/guides/platform/manage-your-usage/egress" rel="noopener noreferrer"&gt;Supabase: Manage Egress usage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://supabase.com/docs/guides/database/postgres/row-level-security" rel="noopener noreferrer"&gt;Supabase: Row Level Security&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://supabase.com/docs/guides/platform/cost-control" rel="noopener noreferrer"&gt;Supabase: Spend Cap&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://vercel.com/docs/vercel-firewall/attack-mode" rel="noopener noreferrer"&gt;Vercel: Attack Mode&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>supabase</category>
      <category>vercel</category>
      <category>security</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Wait, How Did Hackers Steal the Encryption Key Too?" 5 Supabase Security Traps I Fixed in Production</title>
      <dc:creator>UK</dc:creator>
      <pubDate>Sat, 17 Oct 2026 01:18:08 +0000</pubDate>
      <link>https://dev.to/utokazu/wait-how-did-hackers-steal-the-encryption-key-too-5-supabase-security-traps-i-fixed-in-production-1cph</link>
      <guid>https://dev.to/utokazu/wait-how-did-hackers-steal-the-encryption-key-too-5-supabase-security-traps-i-fixed-in-production-1cph</guid>
      <description>&lt;h2&gt;
  
  
  Introduction: The Headline That Made My Stomach Drop
&lt;/h2&gt;

&lt;p&gt;Almost every week, we see another headline about a major tech company or hot startup suffering an unauthorized access incident with API keys or credentials leaked.&lt;/p&gt;

&lt;p&gt;As a solo developer building and running &lt;a href="https://snappysnag.com" rel="noopener noreferrer"&gt;SnappySnag&lt;/a&gt;—a visual bug reporting &amp;amp; AI diagnostic tool for Flutter apps—reading these reports always triggers a cold sweat:&lt;br&gt;
&lt;em&gt;"If companies with dedicated security and SecOps teams get breached, is my database on Supabase actually safe?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;One sentence from a recent incident report hit me particularly hard:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"API keys, integration tokens, and credentials were encrypted at rest. However, the decryption key was also read in the same unauthorized access event..."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Wait.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Doesn't "encrypted at rest in the database" mean our data is secure?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;How on earth did attackers grab both the encrypted data AND the decryption key at the same time?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;And building on Supabase, what false assumptions are we making about our app's security?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That report prompted me to thoroughly audit SnappySnag’s database, functions, and Row-Level Security (RLS) from scratch. Along the way, I uncovered several dangerous traps hidden behind Supabase's developer-friendly convenience.&lt;/p&gt;

&lt;p&gt;Here are &lt;strong&gt;5 critical security checkpoints&lt;/strong&gt; every developer building on Supabase should check against their codebase today.&lt;/p&gt;


&lt;h2&gt;
  
  
  Check 1: Where Is Your Decryption Key Actually Sitting?
&lt;/h2&gt;
&lt;h3&gt;
  
  
  ⚠️ The Trap: "I encrypted the column in DB, so we're safe, right?"
&lt;/h3&gt;

&lt;p&gt;There are two fundamentally different ways we protect sensitive data:&lt;/p&gt;
&lt;h4&gt;
  
  
  1. Hashing (One-Way / Irreversible)
&lt;/h4&gt;

&lt;p&gt;Passwords never need to be converted back to plain text, so we hash them with algorithms like &lt;code&gt;bcrypt&lt;/code&gt; or &lt;code&gt;Argon2&lt;/code&gt;. There is literally no decryption key in the universe to steal. (Supabase Auth handles this automatically with salted bcrypt).&lt;/p&gt;
&lt;h4&gt;
  
  
  2. Encryption (Two-Way / Reversible)
&lt;/h4&gt;

&lt;p&gt;The real nightmare begins with &lt;strong&gt;integration secrets: GitHub personal tokens, Slack/Discord webhook URLs, or user-provided Gemini/OpenAI API keys.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your backend has to decrypt these values back into plain text before making outbound API requests, meaning you &lt;em&gt;cannot&lt;/em&gt; use one-way hashing.&lt;/p&gt;

&lt;p&gt;The fatal mistake is &lt;strong&gt;storing the "safe" (encrypted data) and the "safe's key" (decryption key) in the exact same room&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trap 1: Storing the decryption secret inside another DB table or metadata column.&lt;/strong&gt; (A single SQL injection or DB dump grabs both simultaneously).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Trap 2: Hardcoding the key into public/internal Git repositories or backup scripts.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Trap 3: Running DB and backend application server on the exact same compromised container/VM.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If an attacker dumps your database, having the encryption key in the same perimeter renders the encryption completely useless.&lt;/p&gt;
&lt;h3&gt;
  
  
  🛡️ The Fix: AES-256-GCM &amp;amp; Physical Separation of Keys
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Enforce Strict Physical Separation&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Encrypted Payloads&lt;/strong&gt;: Stored in Supabase (PostgreSQL).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decryption Master Key (&lt;code&gt;APP_ENCRYPTION_KEY&lt;/code&gt;)&lt;/strong&gt;: Stored exclusively as an encrypted secret in your application execution runtime (e.g., Vercel environment variables or edge workers)—&lt;strong&gt;never anywhere inside PostgreSQL&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;If a raw database snapshot ever leaks, the data remains cryptographically unreadable without the runtime server secret.
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Server-side (Node.js / Edge Runtime) AES-256-GCM encryption helper&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;encryptSecret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;plainText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;keyHex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&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;iv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Standard 12-byte IV for GCM&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cipher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createCipheriv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aes-256-gcm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;keyHex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;iv&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;encrypted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cipher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;plainText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;encrypted&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;cipher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;final&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&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;authTag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cipher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAuthTag&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Output format: enc:v1:&amp;lt;iv_hex&amp;gt;:&amp;lt;auth_tag_hex&amp;gt;:&amp;lt;ciphertext_hex&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`enc:v1:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;iv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;authTag&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;encrypted&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;ol&gt;
&lt;li&gt;
&lt;strong&gt;Never Return Raw Secrets in API Payloads&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;Even when a project owner opens their settings page, display secrets as masked placeholders (&lt;code&gt;••••••••&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;In shared queries (e.g., member or feedback queries), strictly omit sensitive columns from the &lt;code&gt;SELECT&lt;/code&gt; query so they never travel over browser network websockets/REST responses.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;


&lt;h2&gt;
  
  
  Check 2: Are Your &lt;code&gt;SECURITY DEFINER&lt;/code&gt; Functions Publicly Executable?
&lt;/h2&gt;

&lt;p&gt;Physically separating keys protects against database dumps. But what if attackers don't need a dump because &lt;strong&gt;your database exposes functions they can just trigger via public REST API?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This was by far the scariest discovery in our audit.&lt;/p&gt;
&lt;h3&gt;
  
  
  ⚠️ The Trap: Slapping &lt;code&gt;SECURITY DEFINER&lt;/code&gt; on a function and leaving it in &lt;code&gt;public&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;In PostgreSQL (and consequently Supabase), whenever you create a function, &lt;strong&gt;PostgreSQL grants &lt;code&gt;EXECUTE&lt;/code&gt; privilege to &lt;code&gt;PUBLIC&lt;/code&gt; (meaning all users, including unauthenticated &lt;code&gt;anon&lt;/code&gt; clients) by default.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you create a function with &lt;code&gt;SECURITY DEFINER&lt;/code&gt; (which runs with postgres superuser/creator privileges, bypassing RLS) in the default &lt;code&gt;public&lt;/code&gt; schema, &lt;strong&gt;anyone on the internet can call it directly via:&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;POST https://&amp;lt;project-ref&amp;gt;.supabase.co/rest/v1/rpc/&amp;lt;function_name&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;If you have a function designed for an internal cron job or payment webhook (e.g., &lt;code&gt;grant_addon_credits&lt;/code&gt;), an unauthenticated stranger with a simple &lt;code&gt;curl&lt;/code&gt; command could trigger administrative mutations on your database!&lt;/p&gt;
&lt;h3&gt;
  
  
  🛡️ The Fix: &lt;code&gt;REVOKE&lt;/code&gt; and the &lt;code&gt;private&lt;/code&gt; Schema Pattern
&lt;/h3&gt;
&lt;h4&gt;
  
  
  1. For Server-Only / Worker Functions: Strip Public Privileges
&lt;/h4&gt;

&lt;p&gt;Immediately revoke execution rights from &lt;code&gt;PUBLIC&lt;/code&gt;, &lt;code&gt;anon&lt;/code&gt;, and &lt;code&gt;authenticated&lt;/code&gt;, granting access solely to &lt;code&gt;service_role&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Lock down server-only functions to service_role&lt;/span&gt;
&lt;span class="k"&gt;REVOKE&lt;/span&gt; &lt;span class="k"&gt;EXECUTE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grant_addon_credits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;PUBLIC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;anon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;EXECUTE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grant_addon_credits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;service_role&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  2. For Functions Used Inside RLS Policies: Move to &lt;code&gt;private&lt;/code&gt; Schema
&lt;/h4&gt;

&lt;p&gt;We often write helper functions like &lt;code&gt;is_project_member(project_id, user_id)&lt;/code&gt; to avoid recursive loops in RLS policies.&lt;/p&gt;

&lt;p&gt;If you leave this in the &lt;code&gt;public&lt;/code&gt; schema, malicious users can brute-force test random UUIDs through the RPC API to enumerate who belongs to which project. But if you revoke permissions from &lt;code&gt;authenticated&lt;/code&gt;, your RLS policies break.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Solution: The &lt;code&gt;private&lt;/code&gt; Schema Pattern.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Supabase's PostgREST API only exposes tables and functions located in the &lt;code&gt;public&lt;/code&gt; schema. By moving helper functions to an internal &lt;code&gt;private&lt;/code&gt; schema, they become completely invisible to the external REST API while remaining lightning-fast within RLS evaluation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 1. Create a dedicated internal schema&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;private&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;USAGE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="n"&gt;private&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;service_role&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 2. Define the helper function inside 'private'&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;private&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_project_member&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;proj_id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_uuid&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="nb"&gt;boolean&lt;/span&gt;
&lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="k"&gt;sql&lt;/span&gt; &lt;span class="k"&gt;STABLE&lt;/span&gt; &lt;span class="k"&gt;SECURITY&lt;/span&gt; &lt;span class="k"&gt;DEFINER&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;search_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pg_temp&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;project_members&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;project_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;proj_id&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_uuid&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;EXECUTE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;private&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_project_member&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;service_role&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 3. Reference it cleanly in your public table's RLS policy&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="nv"&gt;"Project members can view issues"&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;issues&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;private&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_project_member&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Check 3: The Dangerous &lt;code&gt;SELECT USING (true)&lt;/code&gt; Trap That Linter Ignores
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ⚠️ The Trap: "Security Advisor shows 0 warnings, so my RLS is bulletproof!"
&lt;/h3&gt;

&lt;p&gt;Supabase provides an awesome built-in &lt;strong&gt;Security Advisor&lt;/strong&gt; that lints your database for insecure configurations.&lt;/p&gt;

&lt;p&gt;However, there is a massive catch.&lt;/p&gt;

&lt;p&gt;The Security Advisor will flag &lt;code&gt;INSERT / UPDATE / DELETE&lt;/code&gt; policies with &lt;code&gt;WITH CHECK (true)&lt;/code&gt; as dangerous (&lt;code&gt;rls_policy_always_true&lt;/code&gt;).&lt;br&gt;&lt;br&gt;
&lt;strong&gt;However, it intentionally ignores &lt;code&gt;SELECT ... USING (true)&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Why? Because many web apps legitimately have public read tables (e.g., public blog posts, pricing tiers). The linter assumes any open &lt;code&gt;SELECT&lt;/code&gt; is intentional.&lt;/p&gt;

&lt;p&gt;Because of this, temporary starter policies from early prototyping frequently slip into production unnoticed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An invite token lookup table left with &lt;code&gt;FOR SELECT USING (true)&lt;/code&gt; (leaking all pending invitation hashes).&lt;/li&gt;
&lt;li&gt;A user &lt;code&gt;profiles&lt;/code&gt; table with &lt;code&gt;FOR SELECT USING (true)&lt;/code&gt; (exposing usernames or metadata).&lt;/li&gt;
&lt;li&gt;Feedback or comment tables left publicly readable.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  🛡️ The Fix: Manually Audit Every &lt;code&gt;CREATE POLICY&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Do not rely solely on automated linter badges. Search your migration SQL files for &lt;code&gt;USING (true)&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;For private app tables&lt;/strong&gt;: Always restrict &lt;code&gt;SELECT&lt;/code&gt; to &lt;code&gt;auth.uid() = user_id&lt;/code&gt; or membership checks (&lt;code&gt;private.is_project_member(...)&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For public validation workflows (e.g., Invite Links)&lt;/strong&gt;: Avoid having the frontend query the table directly. Route the request through a backend endpoint (Next.js Route Handler / Edge Function) that validates the token and returns strictly the minimal project metadata needed.&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Check 4: Is &lt;code&gt;search_path&lt;/code&gt; Fixed on All Functions?
&lt;/h2&gt;
&lt;h3&gt;
  
  
  ⚠️ The Trap: Leaving out &lt;code&gt;SET search_path&lt;/code&gt; in function definitions
&lt;/h3&gt;

&lt;p&gt;When PostgreSQL runs a function, it resolves unqualified table and function names using a priority list called &lt;code&gt;search_path&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If an unprivileged user creates an object in a mutable schema (or &lt;code&gt;pg_temp&lt;/code&gt;), a &lt;code&gt;SECURITY DEFINER&lt;/code&gt; function might resolve to that malicious object instead, leading to privilege escalation attacks.&lt;/p&gt;
&lt;h3&gt;
  
  
  🛡️ The Fix: Always Explicitly Pin &lt;code&gt;search_path&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Always append &lt;code&gt;SET search_path = public, pg_temp&lt;/code&gt; to custom function definitions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;safe_operation&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;
&lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="n"&gt;plpgsql&lt;/span&gt; &lt;span class="k"&gt;SECURITY&lt;/span&gt; &lt;span class="k"&gt;DEFINER&lt;/span&gt;
&lt;span class="c1"&gt;-- Pin the schema search path explicitly&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;search_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pg_temp&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="c1"&gt;-- Safe internal logic&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Pro-tip&lt;/strong&gt;: An even stricter approach is &lt;code&gt;SET search_path = ''&lt;/code&gt; (empty string). This forces all queries inside the function to use fully qualified schema names (e.g., &lt;code&gt;public.my_table&lt;/code&gt;), completely eliminating any chance of search path hijacking.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Check 5: Project-Level Auth &amp;amp; Extensions Settings
&lt;/h2&gt;

&lt;p&gt;Two low-hanging fruit settings in your Supabase project dashboard you should check right now:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Enable Leaked Password Protection
&lt;/h3&gt;

&lt;p&gt;Under &lt;code&gt;Authentication&lt;/code&gt; -&amp;gt; &lt;code&gt;Attack Protection&lt;/code&gt; -&amp;gt; &lt;code&gt;Leaked Password Protection&lt;/code&gt;:&lt;br&gt;
Supabase integrates directly with HaveIBeenPwned. Enabling this automatically checks incoming passwords against billions of previously breached credentials and rejects them. It takes 10 seconds to toggle and prevents automated credential stuffing.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. Move Extensions Out of &lt;code&gt;public&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Older project templates or manual installs sometimes drop extensions like &lt;code&gt;pg_net&lt;/code&gt; or &lt;code&gt;http&lt;/code&gt; into the &lt;code&gt;public&lt;/code&gt; schema. This exposes extension functions directly to the public PostgREST API. Relocate extensions to the dedicated &lt;code&gt;extensions&lt;/code&gt; schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;pg_net&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="n"&gt;extensions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Bonus Check 6: The &lt;code&gt;USING&lt;/code&gt; vs &lt;code&gt;WITH CHECK&lt;/code&gt; Asymmetry &amp;amp; Storage Policy Leaks
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;(Special thanks to the Dev.to community for calling out this sneaky PostgreSQL behavior!)&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  ⚠️ Trap 6A: PostgreSQL's &lt;code&gt;USING&lt;/code&gt; / &lt;code&gt;WITH CHECK&lt;/code&gt; Asymmetry
&lt;/h3&gt;

&lt;p&gt;In PostgreSQL RLS, there are two clauses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;USING(...)&lt;/code&gt;: Determines which existing rows are visible / eligible to be read or modified.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;WITH CHECK(...)&lt;/code&gt;: Validates whether the &lt;strong&gt;newly written row values&lt;/strong&gt; are permitted.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you write an &lt;code&gt;UPDATE&lt;/code&gt; or &lt;code&gt;ALL&lt;/code&gt; policy with &lt;code&gt;USING&lt;/code&gt; but &lt;strong&gt;omit &lt;code&gt;WITH CHECK&lt;/code&gt;&lt;/strong&gt;, Postgres reuses your &lt;code&gt;USING&lt;/code&gt; predicate to validate the new row!&lt;/p&gt;

&lt;p&gt;If your &lt;code&gt;USING&lt;/code&gt; clause only checks row ownership or team membership:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- DANGEROUS: Omitting WITH CHECK&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="nv"&gt;"Users can update their own profile"&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;profiles&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;
&lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;-- No WITH CHECK!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A malicious user can submit an update modifying critical columns (e.g., boosting &lt;code&gt;credits: 999999&lt;/code&gt; or altering &lt;code&gt;role: 'admin'&lt;/code&gt;). As long as &lt;code&gt;id&lt;/code&gt; remains their own, the reused &lt;code&gt;USING&lt;/code&gt; check passes, and the privilege escalation write sails right through!&lt;/p&gt;

&lt;h4&gt;
  
  
  🛡️ The Audit &amp;amp; Fix:
&lt;/h4&gt;

&lt;p&gt;Audit your database right now with this query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;tablename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;policyname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;with_check&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_policies&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;schemaname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;with_check&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'UPDATE'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'ALL'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any row returned deserves an explicit &lt;code&gt;WITH CHECK&lt;/code&gt; or column-level permissions (&lt;code&gt;REVOKE UPDATE (sensitive_col) ON table FROM authenticated;&lt;/code&gt;) or a &lt;code&gt;BEFORE UPDATE&lt;/code&gt; trigger protecting critical fields.&lt;/p&gt;

&lt;h3&gt;
  
  
  ⚠️ Trap 6B: The Wide-Open &lt;code&gt;storage.objects&lt;/code&gt; Trap
&lt;/h3&gt;

&lt;p&gt;A common architectural oversight is locking down the public schema while leaving Supabase Storage wide open.&lt;/p&gt;

&lt;p&gt;If your screenshot or attachment bucket has a policy like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- DANGEROUS: Any authenticated user can read all assets&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="nv"&gt;"Allow authenticated read"&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
&lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bucket_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'feedback-assets'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A tight public table schema with an open storage policy is just the same data leak wearing a different hat. Any authenticated user can enumerate and download private screenshots belonging to other teams or tenants!&lt;/p&gt;

&lt;h4&gt;
  
  
  🛡️ The Fix: Tenant-Isolated Storage Policies
&lt;/h4&gt;

&lt;p&gt;Organize object paths by tenant/project (&lt;code&gt;[project_id]/[file_name]&lt;/code&gt;) and enforce membership checks directly in the Storage policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="nv"&gt;"Tenant isolated read for feedback-assets"&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
&lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;bucket_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'feedback-assets'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;foldername&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&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="o"&gt;~&lt;/span&gt; &lt;span class="s1"&gt;'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;private&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_project_owner&lt;/span&gt;&lt;span class="p"&gt;(((&lt;/span&gt;&lt;span class="k"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;foldername&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&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="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
      &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;private&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_project_member&lt;/span&gt;&lt;span class="p"&gt;(((&lt;/span&gt;&lt;span class="k"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;foldername&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&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="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&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;h2&gt;
  
  
  Conclusion: Security Is Architecture, Not a Checklist Tool
&lt;/h2&gt;

&lt;p&gt;Supabase is an incredible platform that supercharges development speed. But no matter how good your BaaS is, &lt;strong&gt;how you structure encryption keys, who can execute functions, and where you draw RLS boundaries is an architectural choice that rests entirely on us as engineers.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep encryption keys out of the database.&lt;/li&gt;
&lt;li&gt;Treat every database function as a potential public API endpoint until proven otherwise.&lt;/li&gt;
&lt;li&gt;Never trust a green linter check when it comes to open &lt;code&gt;SELECT&lt;/code&gt; queries.&lt;/li&gt;
&lt;li&gt;Audit &lt;code&gt;UPDATE&lt;/code&gt; policies for &lt;code&gt;WITH CHECK&lt;/code&gt; omissions and lock down &lt;code&gt;storage.objects&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any of these sound uncomfortably familiar in your current project, take 15 minutes during your next deploy to audit your SQL migrations. &lt;/p&gt;

&lt;p&gt;Have you caught any sneaky RLS or function privilege gotchas in your own Supabase builds? Let’s swap notes in the comments below!&lt;/p&gt;




&lt;h3&gt;
  
  
  Resources &amp;amp; Links
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Built with Supabase&lt;/strong&gt;: &lt;a href="https://snappysnag.com" rel="noopener noreferrer"&gt;SnappySnag - Visual Feedback &amp;amp; AI Diagnostics for Flutter&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supabase Official Security Guides&lt;/strong&gt;: &lt;a href="https://supabase.com/docs/guides/security" rel="noopener noreferrer"&gt;Supabase Security &amp;amp; Hardening&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>supabase</category>
      <category>postgres</category>
      <category>security</category>
      <category>webdev</category>
    </item>
    <item>
      <title>"Is That SDK Leaking Data?" Building Trust in a Solo-Dev Flutter SaaS Through Radical Privacy &amp; Transparency</title>
      <dc:creator>UK</dc:creator>
      <pubDate>Tue, 13 Oct 2026 03:06:53 +0000</pubDate>
      <link>https://dev.to/utokazu/is-that-sdk-leaking-data-building-trust-in-a-solo-dev-flutter-saas-through-radical-privacy--jb3</link>
      <guid>https://dev.to/utokazu/is-that-sdk-leaking-data-building-trust-in-a-solo-dev-flutter-saas-through-radical-privacy--jb3</guid>
      <description>&lt;h2&gt;
  
  
  Introduction: The "Trust Barrier" Facing Independent Developer Tools
&lt;/h2&gt;

&lt;p&gt;With news of credential leaks, tracking scandals, and unauthorized data harvesting surfacing almost daily, software engineers are rightfully more cautious than ever about &lt;strong&gt;introducing third-party SDKs into their client apps.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When evaluating a tool built by a solo founder or small indie team, natural hesitations immediately arise:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;"Is this SDK silently transmitting sensitive user data in the background?"&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"Will integrating this expose our end-users to privacy compliance risks?"&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"What happens if this project gets abandoned or security vulnerabilities go unpatched?"&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Having these concerns is completely natural. Without the backing of a large enterprise brand, &lt;strong&gt;the fear of security and privacy risks is often the single biggest reason developers hesitate to integrate a new tool.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I am currently developing and maintaining &lt;a href="https://snappysnag.com" rel="noopener noreferrer"&gt;SnappySnag&lt;/a&gt;—a visual feedback and AI-assisted bug diagnostic SaaS for Flutter applications. It allows testers and developers to drop pins directly onto live screens, annotate issues, and receive automated fix suggestions from an AI agent.&lt;/p&gt;

&lt;p&gt;Because this service handles screen captures and device diagnostics, I committed to a clear principle from day one:&lt;br&gt;
&lt;strong&gt;"As an independent developer, don't ask for blind trust—prove security and radical transparency through open code."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In this article, I want to share the practical engineering decisions we implemented to build that foundation of trust: &lt;strong&gt;Client-side Privacy by Design, guaranteed Zero Training on AI models, GDPR-compliant data lifecycles, and open-source transparency.&lt;/strong&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  1. Privacy by Design: Never Collect Sensitive Data in the First Place
&lt;/h2&gt;

&lt;p&gt;The simplest and most resilient security strategy is: &lt;strong&gt;never transmit or store sensitive customer data on your servers.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Rather than capturing everything and trying to sanitize it server-side, SnappySnag establishes its primary line of defense directly inside the client runtime (on the user's physical device).&lt;/p&gt;
&lt;h3&gt;
  
  
  1. Automatic On-Device PII Masking (Widget Tree Traversal)
&lt;/h3&gt;

&lt;p&gt;When a user captures a screen to report an issue, sensitive information—passwords, payment fields, email addresses, phone numbers, or user avatars—might be visible.&lt;/p&gt;

&lt;p&gt;Instead of running heavy server-side OCR post-upload, &lt;strong&gt;the Flutter SDK traverses the live widget tree locally on the device&lt;/strong&gt;, discovers rendering boundaries (&lt;code&gt;Rect&lt;/code&gt;), and applies pixelated blur masks directly onto the canvas before anything leaves memory:&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;// Excerpt from lib/snappy_snag.dart&lt;/span&gt;
&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Rect&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;findSensitiveFieldBounds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Rect&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;bounds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="c1"&gt;// Traverse starting from the topmost active Scaffold&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;searchElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Element&lt;/span&gt; &lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;widget&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;widget&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// 1. Automatically detect password &amp;amp; text inputs (TextField / EditableText)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;widget&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;TextField&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;widget&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;EditableText&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;renderBox&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findRenderObject&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;RenderBox&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;renderBox&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;renderBox&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hasSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;bounds&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;renderBox&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;localToGlobal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Offset&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;zero&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;renderBox&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. Detect common PII patterns (email, phone numbers, card patterns) in Text widgets&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;widget&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;_containsPiiPattern&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;widget&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Add widget coordinates to the masking bounds&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// 3. Automatically detect user profile avatars (CircleAvatar)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;widget&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;CircleAvatar&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Blur portrait photos locally to preserve user privacy&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;visitChildren&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;searchElement&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;bounds&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The image transmitted over the network is &lt;strong&gt;already baked with on-device mosaic blur&lt;/strong&gt;. Raw sensitive UI elements never touch our backend infrastructure.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Complete Opt-Out for Widget Tree Metadata
&lt;/h3&gt;

&lt;p&gt;While extracting widget hierarchy trees helps AI pinpoint the exact widget needing a fix, healthcare, fintech, or strictly regulated enterprise apps might prohibit sending UI hierarchy details.&lt;/p&gt;

&lt;p&gt;We provide a one-line configuration to completely disable widget tree extraction:&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="n"&gt;SnappySnag&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;apiKey:&lt;/span&gt; &lt;span class="s"&gt;'YOUR_API_KEY'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;enableWidgetTree:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Completely disables structural tree collection&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When disabled, AI diagnostics operate strictly from the visual annotation and screenshot context.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. End-User Anonymous Reporting Toggle
&lt;/h3&gt;

&lt;p&gt;Even if the host app attaches user IDs or reporter emails to aid triaging, the reporting modal includes a toggle allowing the user to uncheck identity sharing. This guarantees the report can be submitted as a completely anonymous event whenever preferred.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. AI Protection: Zero Model Training &amp;amp; Data Sovereignty
&lt;/h2&gt;

&lt;p&gt;A primary concern developers share today is: &lt;strong&gt;"Is our proprietary UI code or app screenshot being used to train third-party AI models?"&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Commercial Paid API Tiers (Zero Model Training)
&lt;/h3&gt;

&lt;p&gt;While SnappySnag leverages the Gemini API for root-cause diagnosis and code diff suggestions, we do not use consumer/free-tier endpoints. We integrate strictly through &lt;strong&gt;commercial paid API tiers (Pay-as-you-go)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Under Google Cloud's commercial terms of service:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero Model Training&lt;/strong&gt;: Customer prompts, screenshots, and metadata sent via commercial paid APIs are not used to train or improve Google's foundational models.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ephemeral Processing&lt;/strong&gt;: Diagnostic data is processed in memory during inference execution and discarded immediately after response generation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  BYOK (Bring Your Own Key) for Total Data Sovereignty
&lt;/h3&gt;

&lt;p&gt;For teams whose internal compliance forbids routing AI requests through third-party SaaS billing pipelines, we implemented a &lt;strong&gt;BYOK (Bring Your Own Key)&lt;/strong&gt; feature in the dashboard.&lt;/p&gt;

&lt;p&gt;In BYOK mode:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API requests route directly under your organization’s own Google Cloud project credentials.&lt;/li&gt;
&lt;li&gt;No SnappySnag AI quota is consumed.&lt;/li&gt;
&lt;li&gt;Full access logging and audit controls remain entirely in your organization's hands.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  3. Data Lifecycle Designed with GDPR &amp;amp; CCPA Principles in Mind
&lt;/h2&gt;

&lt;p&gt;A developer tool that makes data difficult to export or impossible to delete fails modern developer expectations around privacy rights. While we are an independent tool, we designed our data lifecycle around key principles from frameworks like &lt;strong&gt;GDPR (EU) and CCPA/CPRA (California)&lt;/strong&gt;—specifically focusing on data minimization and the user's right to deletion.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Automated 30-Day Expiration Pipeline
&lt;/h3&gt;

&lt;p&gt;For free-tier workspaces, feedback payloads and screenshots are &lt;strong&gt;automatically scrubbed by background workers 30 days after receipt&lt;/strong&gt;. Dormant, stale test data never sits indefinitely on cloud storage.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Frictionless Data Portability
&lt;/h3&gt;

&lt;p&gt;To eliminate vendor lock-in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One-Click Asset Export&lt;/strong&gt;: Download original captures and annotated screenshots directly from the dashboard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Direct GitHub Issue Sync&lt;/strong&gt;: Sync issue tickets, AI suggestions, and device specs directly to your team’s GitHub repository, keeping the permanent source of truth under your version control.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Irreversible Account Deletion &amp;amp; HMAC Audit Logging
&lt;/h3&gt;

&lt;p&gt;When an administrator triggers account deletion in the dashboard's "Danger Zone," all user credentials, projects, API keys, uploaded binaries, and activity logs are &lt;strong&gt;permanently and irreversibly purged across our database and storage clusters&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[User triggers account deletion]
       │
       ▼
[Next.js API Handler]
       ├─ (1) Permanently purge image binaries ──► [Supabase Storage]
       ├─ (2) Cascade hard-delete all data ──────► [PostgreSQL DB]
       └─ (3) Save salted HMAC-SHA256 only ──────► [Immutable Audit Table]
                                                   (Raw email is discarded forever)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By discarding raw email strings and retaining only a mathematically one-way cryptographic hash, we uphold both &lt;strong&gt;GDPR’s Right to Erasure (Article 17)&lt;/strong&gt; and our operational need to protect the service against automated reset abuse.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Open Source as the Ultimate Security Guarantee
&lt;/h2&gt;

&lt;p&gt;No matter how polished a privacy policy is, closed-source client SDKs leave developers wondering: &lt;em&gt;"What is this binary actually doing under the hood?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;To remove that ambiguity, we took three concrete transparency steps:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. 100% Open Source Client SDK
&lt;/h3&gt;

&lt;p&gt;The entire Flutter SDK is openly audited on GitHub:&lt;br&gt;&lt;br&gt;
👉 &lt;a href="https://github.com/snappysnag/snappy_snag_flutter" rel="noopener noreferrer"&gt;snappysnag/snappy_snag_flutter (GitHub)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every network call, storage interaction, and widget inspection routine is open for anyone to review line-by-line.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. English Code Comments &amp;amp; Automated CI Workflows
&lt;/h3&gt;

&lt;p&gt;To welcome global scrutiny and streamline security reviews, all codebase comments and documentation are standardized in English. Automated GitHub Actions CI runs on every commit, continuously validating code health via &lt;code&gt;dart analyze&lt;/code&gt; and test suites.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Dedicated Security Portal &amp;amp; Responsible Disclosure
&lt;/h3&gt;

&lt;p&gt;Rather than hiding vulnerability procedures deep inside terms of service, we maintain a dedicated security page at &lt;a href="https://snappysnag.com/security" rel="noopener noreferrer"&gt;snappysnag.com/security&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Our repository includes a standard &lt;code&gt;SECURITY.md&lt;/code&gt; file detailing our responsible disclosure policy alongside a direct reporting channel (&lt;code&gt;support@snappysnag.com&lt;/code&gt;).&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion: Security is Not a Cost, It's Mutual Respect
&lt;/h2&gt;

&lt;p&gt;When building as an indie developer, it is tempting to think: &lt;em&gt;"I'll just get the prototype working first and figure out security and legal terms later."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;However, to developers and companies evaluating your software, &lt;strong&gt;the weight of their users' data is the same whether you are a solo hacker or a tech giant.&lt;/strong&gt; In fact, because indie tools lack a decades-old corporate reputation, demonstrating meticulous care for privacy and opening your architecture is the most authentic way to build trust.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep data on the client whenever possible.&lt;/li&gt;
&lt;li&gt;Never let AI providers train on user assets.&lt;/li&gt;
&lt;li&gt;Make data easy to export and truly effortless to delete.&lt;/li&gt;
&lt;li&gt;And prove it all by keeping the client code open.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are building your own developer tools or open-source packages, consider making &lt;strong&gt;Privacy by Design and transparency your core strengths.&lt;/strong&gt; More than raw feature counts, they will be the reassuring factors that convince teams to welcome your code into their stack.&lt;/p&gt;




&lt;h3&gt;
  
  
  Links &amp;amp; Resources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live Product&lt;/strong&gt;: &lt;a href="https://snappysnag.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=security_transparency" rel="noopener noreferrer"&gt;SnappySnag&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security Overview&lt;/strong&gt;: &lt;a href="https://snappysnag.com/security" rel="noopener noreferrer"&gt;SnappySnag Security&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flutter SDK (pub.dev)&lt;/strong&gt;: &lt;a href="https://pub.dev/packages/snappy_snag" rel="noopener noreferrer"&gt;pub.dev/packages/snappy_snag&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Repository&lt;/strong&gt;: &lt;a href="https://github.com/snappysnag/snappy_snag_flutter" rel="noopener noreferrer"&gt;snappysnag/snappy_snag_flutter&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;How do you evaluate third-party SDK security when choosing tools for your apps? I’d love to hear your thoughts and checklist items in the comments below!&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>security</category>
      <category>privacy</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Eliminating First-3-Minute Churn in a Developer SaaS: Radical Simplification &amp; Building a Custom React Product Tour</title>
      <dc:creator>UK</dc:creator>
      <pubDate>Fri, 09 Oct 2026 09:28:55 +0000</pubDate>
      <link>https://dev.to/utokazu/eliminating-first-3-minute-churn-in-a-developer-saas-radical-simplification-building-a-custom-3bpf</link>
      <guid>https://dev.to/utokazu/eliminating-first-3-minute-churn-in-a-developer-saas-radical-simplification-building-a-custom-3bpf</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Hey everyone! I'm UK, a solo developer building &lt;a href="https://snappysnag.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=onboarding_productTour" rel="noopener noreferrer"&gt;SnappySnag&lt;/a&gt;, a visual feedback and AI-powered fix SDK for Flutter apps.&lt;/p&gt;

&lt;p&gt;For developer-focused SaaS (especially SDK-based developer tools), the most dangerous hurdle to survival is &lt;strong&gt;the first 3 minutes after signup.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Unlike typical consumer web apps where users can instantly click around and reach an "Aha! Moment," devtools demand serious friction right out of the gate:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sign up for an account&lt;/li&gt;
&lt;li&gt;Create a project&lt;/li&gt;
&lt;li&gt;Generate and copy an API key&lt;/li&gt;
&lt;li&gt;Open their local codebase and install the package&lt;/li&gt;
&lt;li&gt;Build and run the app on a physical device/emulator to send a test event&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's an &lt;strong&gt;Everest-sized mountain of friction&lt;/strong&gt;. If users drop off before running code on their machine, they experience exactly zero value.&lt;/p&gt;

&lt;p&gt;To survive this drop-off cliff, I recently revamped our web dashboard by &lt;strong&gt;ruthlessly eliminating visual clutter&lt;/strong&gt;, and built a &lt;strong&gt;lightweight, spotlight-following product tour from scratch using React and Tailwind CSS&lt;/strong&gt; to guide users the moment their first event arrives.&lt;/p&gt;

&lt;p&gt;Here is the full breakdown of what failed, what worked, and how I built it.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Starting Problem: "Too Much Information = No Clear Action"
&lt;/h2&gt;

&lt;p&gt;When I first designed the dashboard, I thought I was being helpful. I provided every feature upfront so users could see what the platform was capable of.&lt;/p&gt;

&lt;p&gt;Looking at it through the eyes of a first-time user, however, it was just a &lt;strong&gt;mountain of noise&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sidebar packed with menus&lt;/strong&gt;: Screen categories, Project settings, Team members, Billing plans—all visible when there was zero data yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Header cluttered with usage meters&lt;/strong&gt;: AI credits balance, "Upgrade to Pro" banners, etc., completely irrelevant to sending a first test event.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-step setup accordion&lt;/strong&gt;: Vertical scroll requiring users to enter project names, read long instructions, and copy snippets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Intimidating blocks of code&lt;/strong&gt;: Giant chunks of boilerplate immediately overwhelming the screen.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;By trying to explain everything, the single most critical action—"Copy this API key and paste it into your app"—was completely buried.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Phase 1: The Courage to Delete and Automate
&lt;/h2&gt;

&lt;p&gt;Before adding any new onboarding features, I started with &lt;strong&gt;radical subtraction and automation&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Automatic Project Creation (Zero Setup)
&lt;/h3&gt;

&lt;p&gt;Instead of forcing users through a "Create Project" modal, the backend now automatically creates a default project with an active API key the millisecond their account is confirmed. They land directly on the dashboard with everything ready.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Complete Sidebar Hiding on Project #1
&lt;/h3&gt;

&lt;p&gt;For brand-new users, the sidebar is completely hidden. Having a left-hand navigation bar naturally pulls human eyes away from the center. I stripped everything down to focus 100% on one single element: &lt;strong&gt;The API Key and the Copy Button&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Note for multi-project workflows:&lt;/em&gt; When a user creates their 2nd project (&lt;code&gt;projects.length &amp;gt; 1&lt;/code&gt;), the sidebar automatically reveals itself so they can switch between workspaces without friction.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  3. Collapsing Code Snippets by Default
&lt;/h3&gt;

&lt;p&gt;Originally, the setup snippet was expanded by default. Realizing it added visual fatigue, I collapsed it behind a clean "View Setup Code" button. The primary view remains spotless and focused.&lt;/p&gt;

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




&lt;h2&gt;
  
  
  3. Phase 2: Delivering the "Aha! Moment" on the Very First Event
&lt;/h2&gt;

&lt;p&gt;Once the dashboard was simplified, getting users to install the SDK and trigger a test feedback event became much smoother.&lt;/p&gt;

&lt;p&gt;But that immediately surfaced the next hurdle: &lt;strong&gt;"Now that an event has arrived, what should I do next?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;SnappySnag's core value shines when you:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Inspect the visual pins and privacy blurs placed by the tester.&lt;/li&gt;
&lt;li&gt;Hit the Gemini AI button to get an instant root-cause analysis and Flutter fix diff.&lt;/li&gt;
&lt;li&gt;Sync the issue and AI suggestions directly to GitHub in one click.&lt;/li&gt;
&lt;li&gt;Leave private developer notes at the bottom floating bar.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If users just stare at a static detail screen, they might miss these features entirely. That's when I decided to build a &lt;strong&gt;quick, interactive spotlight tour that triggers exclusively when the very first feedback arrives.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Why Central Modal Tours Fail (And What Worked)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Failure: Centered Backdrop Modal
&lt;/h3&gt;

&lt;p&gt;My first prototype was a typical centered modal. I applied a &lt;code&gt;backdrop-blur&lt;/code&gt; across the screen and showed a generic popover in the middle saying: &lt;em&gt;"Step 1 of 4: Check out the screenshot..."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It felt terrible:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Users had no idea which button was being referenced.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Blurring out the actual screenshot and AI buttons completely detached the explanation from the live UI. It just felt like an annoying popup to close.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Fix: Coordinate-Tracking Spotlight &amp;amp; Popovers
&lt;/h3&gt;

&lt;p&gt;Rather than pulling in heavy third-party libraries (like Driver.js or Intro.js), I built a custom solution using React, DOM rects, and Tailwind CSS. &lt;/p&gt;

&lt;p&gt;This kept our bundle lean, matched our dark-mode theme seamlessly, and gracefully handled responsive transitions on mobile viewports.&lt;/p&gt;

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




&lt;h2&gt;
  
  
  5. Under the Hood: React &amp;amp; Tailwind Implementation
&lt;/h2&gt;

&lt;p&gt;The tour relies on three key mechanisms:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Smooth Scrolling &amp;amp; Target Rect Calculation
&lt;/h3&gt;

&lt;p&gt;Whenever the step changes, we find the target element, smoothly center it in the viewport using &lt;code&gt;scrollIntoView&lt;/code&gt;, and save its bounding box to state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tourStep&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setTourTargetRect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Force open the detail view on mobile screens during the tour&lt;/span&gt;
  &lt;span class="nf"&gt;setIsMobileDetailOpen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getTargetElementId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;step&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tour-step-screenshot&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tour-step-ai-section&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tour-step-github-btn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tour-step-notes-bar&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;targetId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getTargetElementId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tourStep&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;el&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Scroll smoothly to keep the target in view&lt;/span&gt;
    &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scrollIntoView&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;behavior&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;smooth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;block&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;center&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;rect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBoundingClientRect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;setTourTargetRect&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tourStep&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. The Spotlight Ring (Pulsing Amber Glow)
&lt;/h3&gt;

&lt;p&gt;Instead of heavily dimming or blurring the UI, we place a subtle &lt;code&gt;bg-black/40&lt;/code&gt; overlay and a fixed glowing ring that wraps around the target element:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* Target Element Spotlight (Amber border with glowing pulse) */&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;tourTargetRect&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;
    &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"fixed border-2 border-amber-400 rounded-xl pointer-events-none transition-all duration-300 ring-4 ring-amber-500/30 shadow-[0_0_30px_rgba(245,158,11,0.4)] z-50 animate-pulse"&lt;/span&gt;
    &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tourTargetRect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tourTargetRect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tourTargetRect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tourTargetRect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Dynamic Popover Flipping &amp;amp; Arrow Placement
&lt;/h3&gt;

&lt;p&gt;To prevent tooltips from getting cut off on small screens or near viewport edges, the popover dynamically flips above or below the target:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Popover positioning logic (simplified snippet)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;screenWidth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerWidth&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;screenHeight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHeight&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;popoverWidth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;380&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;screenWidth&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tourStep&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Step 1: Place below if enough room, otherwise flip above&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;spaceBelow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;screenHeight&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tourTargetRect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;tourTargetRect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;spaceBelow&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;260&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;popoverStyle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tourTargetRect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;tourTargetRect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;arrowPlacement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;top&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;popoverStyle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tourTargetRect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;240&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;arrowPlacement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bottom&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;centerX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tourTargetRect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;tourTargetRect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;popoverStyle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screenWidth&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;popoverWidth&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;centerX&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;popoverWidth&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tourStep&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Step 4: Always position above the bottom floating input bar&lt;/span&gt;
  &lt;span class="nx"&gt;popoverStyle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bottom&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;56&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;screenHeight&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;tourTargetRect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;popoverStyle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screenWidth&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;popoverWidth&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screenWidth&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;popoverWidth&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="nx"&gt;arrowPlacement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bottom&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once finished or dismissed, we save a flag in &lt;code&gt;localStorage&lt;/code&gt; so it never interrupts the developer again.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Key Takeaways from Our Onboarding Redesign
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Subtracting UI takes more courage than adding, but delivers 10x the impact&lt;/strong&gt;
Hiding features you worked hard to build is difficult, but first-time users desperately crave a single clear next step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never hide the real UI behind generic tour modals&lt;/strong&gt;
Centering a popup isolates users from the interface. Highlighting elements directly with a lightweight spotlight turns the tour into a natural extension of the product.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Responsive behavior cannot be an afterthought in tours&lt;/strong&gt;
If your layout toggles between list and detail views on mobile viewports, your tour logic must explicitly control those views (e.g., forcing open the detail panel) to avoid broken states.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I'm currently tracking our activation conversion rates in GA4, and I'll share more quantitative findings as data builds up!&lt;/p&gt;




&lt;h3&gt;
  
  
  Links &amp;amp; Resources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live Product&lt;/strong&gt;: &lt;a href="https://snappysnag.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=onboarding_productTour" rel="noopener noreferrer"&gt;SnappySnag&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flutter Package&lt;/strong&gt;: &lt;a href="https://pub.dev/packages/snappy_snag" rel="noopener noreferrer"&gt;pub.dev/packages/snappy_snag&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/snappysnag/snappy_snag_flutter" rel="noopener noreferrer"&gt;snappysnag/snappy_snag_flutter&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Have you tackled onboarding drop-off in your own tools? I'd love to hear how you design for that critical first run in the comments below!&lt;/p&gt;

</description>
      <category>react</category>
      <category>nextjs</category>
      <category>webdev</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Capturing On-Device UI Feedback &amp; AI Code Suggestions: Why I Built SnappySnag for Flutter</title>
      <dc:creator>UK</dc:creator>
      <pubDate>Tue, 06 Oct 2026 04:02:15 +0000</pubDate>
      <link>https://dev.to/utokazu/capturing-on-device-ui-feedback-ai-code-suggestions-why-i-built-snappysnag-for-flutter-5c9a</link>
      <guid>https://dev.to/utokazu/capturing-on-device-ui-feedback-ai-code-suggestions-why-i-built-snappysnag-for-flutter-5c9a</guid>
      <description>&lt;h2&gt;
  
  
  1. What happens to the sparks of inspiration when you test on a real device?
&lt;/h2&gt;

&lt;p&gt;When building a Flutter app as an indie developer or within a small team, when do you get your best ideas or spot the subtlest UI quirks?&lt;/p&gt;

&lt;p&gt;Rarely is it when you're staring blankly at your desktop monitor and Android emulator. More often than not, it happens &lt;strong&gt;when you're sitting on the couch, casually tapping through the release build on your actual physical phone.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;"This button padding feels just a bit too cramped on an actual screen."&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"Adding a quick search bar right here would make navigation so much smoother."&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"This label might be confusing for a first-time user."&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are countless improvements you can only notice through the true physical dimensions and ergonomics of a handheld device.&lt;/p&gt;

&lt;p&gt;Yet, the moment you think, &lt;em&gt;"Let me note this down,"&lt;/em&gt; reality gets messy:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You take a screenshot.&lt;/li&gt;
&lt;li&gt;You switch away and open your notes app or Slack.&lt;/li&gt;
&lt;li&gt;You type out a wordy explanation: &lt;em&gt;"On the settings screen, scrolled all the way down, next to that button..."&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Or, you write a lazy shorthand note, only to open your IDE days later thinking, &lt;em&gt;"Wait, where on earth was I talking about?"&lt;/em&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The mental overhead of describing context with words is surprisingly high. Good ideas end up getting skipped, or hours get wasted reconstructing context later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"What if we could annotate directly on the live screen with a pen or note, and have AI instantly suggest the implementation code?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Driven by this simple question about developer experience (DX), I built and just published the initial release of &lt;strong&gt;SnappySnag&lt;/strong&gt;, a visual feedback &amp;amp; AI assistance package for Flutter.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. The Ideal "UI Improvement Loop" for Growing an App
&lt;/h2&gt;

&lt;p&gt;For any app's growth, one of the most critical factors is &lt;strong&gt;how fast you can iterate on real-world tactile UX.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No matter how solid your business logic or features are, small visual quirks (cluttered layouts, awkward spacing, clunky navigation) degrade user trust if left unattended.&lt;/p&gt;

&lt;p&gt;However, the traditional way of handling these day-to-day tweaks is riddled with friction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Context-Switching Cost&lt;/strong&gt;: Pausing physical testing to grab your laptop, open an issue tracker, and draft a ticket.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explanation Cost&lt;/strong&gt;: Spending minutes explaining &lt;em&gt;which&lt;/em&gt; widget on &lt;em&gt;which&lt;/em&gt; screen you are referring to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Triage Delay&lt;/strong&gt;: The lag between reading a note and finding the right file/code to start fixing it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To keep shipping fast, you need a workflow where &lt;strong&gt;capturing an idea takes zero effort, and the first step toward coding starts immediately.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here is the feedback loop we aimed for:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;One-action on-screen notation&lt;/strong&gt; (drop a pin, sketch, or leave a note directly on the device).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic context capture&lt;/strong&gt; (screen route, widget tree metadata, device specs, and OS details).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI drafting the fix&lt;/strong&gt; ("Here is how you can update the code on this screen to make that change happen").&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  3. How It Works: From Tester App to Developer Dashboard
&lt;/h2&gt;

&lt;p&gt;SnappySnag is designed not just as a typical "bug tracker," but as a lightweight companion for &lt;strong&gt;capturing spontaneous UI improvements, design tweaks, and feature ideas while dogfooding your app.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Common Use Cases
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;On-Device Feature Sparks&lt;/strong&gt;: Circling an area and jotting down &lt;em&gt;"Add quick action button here"&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fine Design Adjustments&lt;/strong&gt;: Tweaking padding, font sizes, colors, or alignment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visual Bugs &amp;amp; Edge Cases&lt;/strong&gt;: Pointing out text truncations or layout overflows across different screen sizes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Interaction Flow
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Trigger&lt;/strong&gt;: Invoke the SnappySnag overlay anytime via a floating button or a device shake.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Annotate&lt;/strong&gt;: A frozen capture of the current screen appears. Drop pins, sketch with the pen, or type quick notes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Submit &amp;amp; AI Analysis&lt;/strong&gt;: Once sent, the feedback and on-screen context are processed by an AI agent to draft an actionable code suggestion or fix.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The focus is to &lt;strong&gt;point directly at the UI&lt;/strong&gt; rather than writing lengthy bug descriptions.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Here is a conceptual overview of the workflow)&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/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5f0e4sfb8y17f47b6onm.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5f0e4sfb8y17f47b6onm.gif" alt="SnappySnag Workflow: From On-Device Pin to Dashboard AI Suggestion" width="719" height="419"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Left: Tester App (Flutter SDK)&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;While navigating the app, drop a pin on any widget with a quick remark (&lt;em&gt;"Overflow on menu row..."&lt;/em&gt; or &lt;em&gt;"Add spacing here"&lt;/em&gt;) and hit send.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Right: Developer Dashboard (Web)&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;The report syncs in real-time with device details (e.g., Pixel 8, Android 14).&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;AI Agent Prompt / code suggestion&lt;/strong&gt; is automatically generated on the spot (e.g., &lt;em&gt;"Wrap the overflowing child in an Expanded or Row in a SingleChildScrollView"&lt;/em&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By the time you open your dashboard, the feedback has already transformed into an actionable task with starter code.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Under the Hood: Engineering Considerations
&lt;/h2&gt;

&lt;p&gt;To make this seamless inside a Flutter app, we tackled several technical details:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Non-Intrusive Overlay Architecture
&lt;/h3&gt;

&lt;p&gt;The SDK must never disrupt the host app's layouts or state. Utilizing Flutter's &lt;code&gt;Overlay&lt;/code&gt; and &lt;code&gt;RepaintBoundary&lt;/code&gt;, SnappySnag layers an annotation canvas and capture mechanism smoothly without altering the app's widget hierarchy.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Contextual Metadata Extraction
&lt;/h3&gt;

&lt;p&gt;Passing raw images alone to an LLM often yields vague responses. Along with the screenshot, SnappySnag extracts runtime metadata—active Route names, screen dimensions, device metrics, and relevant widget context—to ground the AI in exact details.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Prompting for Actionable Diffs
&lt;/h3&gt;

&lt;p&gt;Short human notes like &lt;em&gt;"make this wider"&lt;/em&gt; or &lt;em&gt;"add a button below"&lt;/em&gt; need to be translated into sound Flutter code.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The model is instructed to identify the exact widget impacted.&lt;/li&gt;
&lt;li&gt;Rather than rewriting entire files, it is guided to output clean, targeted diffs, substantially reducing hallucinations.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  5. What Changed After Dogfooding It
&lt;/h2&gt;

&lt;p&gt;Integrating the package into my own ongoing Flutter projects brought an immediate shift: &lt;strong&gt;I stopped dropping ideas.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Previously, small UI rough edges that I promised myself to "fix later" were quietly forgotten. Now, they are captured in seconds right when the thought strikes, complete with an AI-generated head start on the code.&lt;/p&gt;

&lt;p&gt;For indie hackers and lean teams, reducing the friction of &lt;strong&gt;"passing notes to future me"&lt;/strong&gt; delivers an enormous boost to momentum.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. What's Next: Aiming for a Self-Contained Feedback Loop
&lt;/h2&gt;

&lt;p&gt;While capturing notes and getting AI code suggestions is already functional in this initial release, there is an even smoother workflow on the horizon:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Submitting a note from your device, and having a preview deployment URL spun up automatically where the proposed change is already live—ready to be merged with one tap.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Imagine tweaking UI elements on your couch, checking the updated preview link on your phone, and merging it without even opening a terminal.&lt;/p&gt;

&lt;p&gt;While not every change requires such automation, having this streamlined loop for UI tweaks and copy edits would make iterating feel almost effortless. We plan to explore this path as the core foundation matures.&lt;/p&gt;




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

&lt;p&gt;Polishing an app's experience should be enjoyable, fast, and frictionless.&lt;/p&gt;

&lt;p&gt;While SnappySnag has just taken its first step with this initial release, I hope it resonates with anyone who loves crafting apps directly on real devices.&lt;/p&gt;

&lt;p&gt;I’ll continue sharing updates and technical deep-dives into Flutter overlays and LLM prompt design. If you're building with Flutter, feel free to give it a spin!&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;pub.dev&lt;/strong&gt;: &lt;a href="https://pub.dev/packages/snappy_snag" rel="noopener noreferrer"&gt;pub.dev/packages/snappy_snag&lt;/a&gt;
Get started with &lt;code&gt;flutter pub add snappy_snag&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/snappysnag/snappy_snag_flutter" rel="noopener noreferrer"&gt;github.com/snappysnag/snappy_snag_flutter&lt;/a&gt;
Stars, feedback, and issues are warmly welcomed!&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Official Website&lt;/strong&gt;: &lt;a href="https://snappysnag.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=introduction_snappysnag" rel="noopener noreferrer"&gt;https://snappysnag.com&lt;/a&gt;
Learn more about the project and roadmap.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>showdev</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
