<?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: heocoi</title>
    <description>The latest articles on DEV Community by heocoi (@heocoi).</description>
    <link>https://dev.to/heocoi</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F156328%2Fa560ac0b-35b7-491f-8cce-1a62330d23db.png</url>
      <title>DEV Community: heocoi</title>
      <link>https://dev.to/heocoi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/heocoi"/>
    <language>en</language>
    <item>
      <title>Building MailSink: a temp email API with MCP server on Cloudflare Workers</title>
      <dc:creator>heocoi</dc:creator>
      <pubDate>Wed, 22 Apr 2026 00:35:35 +0000</pubDate>
      <link>https://dev.to/heocoi/building-mailsink-a-temp-email-api-with-mcp-server-on-cloudflare-workers-4g65</link>
      <guid>https://dev.to/heocoi/building-mailsink-a-temp-email-api-with-mcp-server-on-cloudflare-workers-4g65</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Testing email flows in CI has been painful for years. MailSlurp is $79/mo, Mailtrap's API isn't built for AI agents, and most alternatives want you to run your own SMTP server.&lt;/p&gt;

&lt;p&gt;I wanted something different: a REST API callable from tests, with an MCP server so AI agents could run email-verified signup flows natively.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;MailSink gives you programmatic temporary email inboxes via a simple REST API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Spin up a disposable inbox&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST api.mailsink.dev/v1/inboxes
&lt;span class="c"&gt;# → signup-k8m2@codenotify.net&lt;/span&gt;

&lt;span class="c"&gt;# Your app or agent signs up with that address...&lt;/span&gt;

&lt;span class="c"&gt;# Wait for the verification mail, extract the OTP code&lt;/span&gt;
curl api.mailsink.dev/v1/inboxes/inb_rae1z/wait-for-code
&lt;span class="c"&gt;# → { "code": "847291", "from": "stripe.com" }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole API surface for the common case.&lt;/p&gt;

&lt;h2&gt;
  
  
  The MCP angle
&lt;/h2&gt;

&lt;p&gt;An MCP server ships in the &lt;code&gt;@mailsink/mcp&lt;/code&gt; npm package. Claude Code, Cursor, Windsurf, any MCP client can call these tools natively during autonomous workflows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;create_inbox&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="c1"&gt;// → signup-k8m2@codenotify.net&lt;/span&gt;

&lt;span class="nf"&gt;get_verification_code&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;inbox_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="c1"&gt;// → { code: "847291" }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your agent can actually sign up for Stripe, GitHub, Clerk, Supabase. Not a browser automation wrapper. Native.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;

&lt;p&gt;Everything runs on Cloudflare's edge:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;workers/api&lt;/strong&gt; - Hono on Cloudflare Workers. Routes: inboxes, messages, domains, keys, auth (GitHub OAuth), billing (Stripe), internal. KV-based rate limiting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;workers/email&lt;/strong&gt; - Cloudflare Email Routing handler. Parses incoming mail with &lt;code&gt;postal-mime&lt;/code&gt;, writes metadata to D1, raw body to R2, triggers OTP extraction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;packages/shared&lt;/strong&gt; - OTP + magic-link extractors for Stripe, GitHub, Google, Clerk, Supabase, Auth0, Resend, AWS. Raw MIME always available if the extractor misses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;mcp&lt;/strong&gt; - &lt;code&gt;@mailsink/mcp&lt;/code&gt; npm package. 7 tools exposed to MCP clients.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why Cloudflare:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sub-20ms p50 latency via 320+ edge locations&lt;/li&gt;
&lt;li&gt;Email Routing handles SMTP receipt for free on shared domains&lt;/li&gt;
&lt;li&gt;D1 for metadata, R2 for bodies, zero infra to manage&lt;/li&gt;
&lt;li&gt;One deploy target, one config&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Email parsing is harder than it looks.&lt;/strong&gt; &lt;code&gt;postal-mime&lt;/code&gt; handles MIME well, but OTP patterns across senders are wildly inconsistent. I built per-sender extractors for the top 8 services and fall back to generic regex patterns for everything else.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;MCP turned a dev tool into an agent tool.&lt;/strong&gt; Same REST API, new audience. Zero additional code to maintain, major differentiator in the pitch.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cloudflare Email Routing + shared domains&lt;/strong&gt; lets you skip running Haraka or Postfix for shared-domain inboxes. You only need your own SMTP receiver when you support Bring Your Own Domain.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Pricing anchors matter.&lt;/strong&gt; MailSlurp at $79/mo signals "enterprise." I priced Pro at $15/mo because the real differentiator is agent-friendliness, not feature parity.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Pricing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Free: 3 inboxes, 100 msgs/mo&lt;/li&gt;
&lt;li&gt;Pro: $15/mo unlimited&lt;/li&gt;
&lt;li&gt;BYOD on Pro+ (bring your own domain, waitlist)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Live at &lt;a href="https://mailsink.dev" rel="noopener noreferrer"&gt;https://mailsink.dev&lt;/a&gt;. GitHub OAuth, you're in in 10 seconds.&lt;/p&gt;

&lt;p&gt;Feedback welcome, especially on API shape and pricing positioning.&lt;/p&gt;

&lt;p&gt;First open stable release. Still learning what breaks.&lt;/p&gt;

</description>
      <category>api</category>
      <category>cloudflare</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I built a $19/mo dunning tool because Churn Buster costs $249</title>
      <dc:creator>heocoi</dc:creator>
      <pubDate>Mon, 23 Mar 2026 16:55:34 +0000</pubDate>
      <link>https://dev.to/heocoi/i-built-a-19mo-dunning-tool-because-churn-buster-costs-249-56lb</link>
      <guid>https://dev.to/heocoi/i-built-a-19mo-dunning-tool-because-churn-buster-costs-249-56lb</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;If you run a SaaS on Stripe, around 9% of your MRR is silently leaking every month. Cards expire. Banks decline. Customers don't notice.&lt;/p&gt;

&lt;p&gt;Stripe retries the charge a few times, but it doesn't email your customers. It doesn't warn them their card is about to expire. And by the time you notice, they've already churned.&lt;/p&gt;

&lt;h2&gt;
  
  
  The existing solutions are expensive
&lt;/h2&gt;

&lt;p&gt;I looked at the options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Churn Buster&lt;/strong&gt;: $249/mo minimum&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Baremetrics Recover&lt;/strong&gt;: $158/mo
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stunning&lt;/strong&gt;: opaque MRR-based pricing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For an indie founder doing $5-25K MRR, spending $150-250/mo on dunning feels wrong. The ROI math works, but the cash flow doesn't.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://revenudge.com" rel="noopener noreferrer"&gt;Revenudge&lt;/a&gt; does three things:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Pre-dunning alerts&lt;/strong&gt; - Detects cards expiring in 30/14/7 days and emails your customers BEFORE the payment fails. This is the killer feature. Stripe doesn't do this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Recovery email sequences&lt;/strong&gt; - When a payment does fail, sends 3 branded emails (Day 1, 3, 7) with a one-click card update link. Your logo, your colors, your brand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Recovery dashboard&lt;/strong&gt; - Track MRR at risk, recovered revenue, recovery rate, email open/click rates. Know exactly how much money you're saving.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js&lt;/strong&gt; (App Router) on Vercel&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supabase&lt;/strong&gt; (Postgres + Auth + RLS)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stripe Connect&lt;/strong&gt; (OAuth, webhooks)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resend&lt;/strong&gt; (transactional emails)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One-click Stripe Connect setup. No API keys to paste. 60 seconds from signup to protected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Starter&lt;/strong&gt;: $19/mo (up to $5K MRR)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Growth&lt;/strong&gt;: $39/mo (up to $25K MRR) &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scale&lt;/strong&gt;: $79/mo (up to $100K MRR)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;14-day Growth trial, no credit card required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;If you're running subscriptions on Stripe and losing revenue to failed payments, give it a shot: &lt;a href="https://revenudge.com" rel="noopener noreferrer"&gt;revenudge.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Built by an indie dev, for indie devs. Happy to answer questions in the comments.&lt;/p&gt;

</description>
      <category>saas</category>
      <category>stripe</category>
      <category>showdev</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Your printer hates your designs. So I made them print-ready by default.</title>
      <dc:creator>heocoi</dc:creator>
      <pubDate>Sun, 15 Mar 2026 05:29:38 +0000</pubDate>
      <link>https://dev.to/heocoi/your-printer-hates-your-designs-so-i-made-them-print-ready-by-default-12fm</link>
      <guid>https://dev.to/heocoi/your-printer-hates-your-designs-so-i-made-them-print-ready-by-default-12fm</guid>
      <description>&lt;p&gt;I just shipped &lt;a href="https://inkpress.app" rel="noopener noreferrer"&gt;Inkpress&lt;/a&gt; - an AI-powered tool that generates print-ready business cards, flyers, posters, and restaurant menus from text prompts.&lt;/p&gt;

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

&lt;p&gt;Small business owners design something that looks great on screen, send it to a printer, and get back cards with white edges, blurry logos, or colors that look completely different. The gap between "looks good on my monitor" and "prints correctly" is surprisingly wide.&lt;/p&gt;

&lt;p&gt;Print design has a bunch of technical requirements that have nothing to do with design taste:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bleed&lt;/strong&gt;: 3mm of artwork extending past the cut line so you don't get white edges&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safe zone&lt;/strong&gt;: keeping text 3-5mm inside the trim so it doesn't get clipped&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resolution&lt;/strong&gt;: 300 DPI minimum (screens are 72-96 DPI)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Color mode&lt;/strong&gt;: CMYK instead of RGB, because ink and light mix differently&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most people don't know about any of this until their first print job comes back wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Inkpress does
&lt;/h2&gt;

&lt;p&gt;You describe what you want in plain text. Something like: "Business card for my photography studio, name Sarah Chen, dark minimal style." Inkpress generates 3 design variations, all print-ready from the start. You pick one, then refine it through conversation - change colors, swap fonts, adjust layout - until it's right. Export a PDF and send it to any print shop.&lt;/p&gt;

&lt;p&gt;Every export includes proper bleed, trim marks, safe zone guides, and 300 DPI resolution. The user never has to think about print specs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend&lt;/strong&gt;: React + Vite + Tailwind CSS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend&lt;/strong&gt;: Hono (lightweight, runs anywhere)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database&lt;/strong&gt;: better-sqlite3 (simple, no separate server)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI&lt;/strong&gt;: Claude API for both design generation and iterative refinement&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth&lt;/strong&gt;: Google OAuth via better-auth&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage&lt;/strong&gt;: Cloudflare R2 for generated assets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hosting&lt;/strong&gt;: Railway (backend) + Cloudflare (DNS/CDN)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The design generation works by having Claude output structured HTML/CSS for each design variation. The frontend renders the designs in real-time, and the export pipeline converts them to print-ready PDFs with correct dimensions and bleed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons learned so far
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Print specs are surprisingly standardized.&lt;/strong&gt; Business cards are 85x55mm (EU) or 88.9x50.8mm (US). Bleed is always 3mm. Once you encode these rules, the AI can apply them consistently every time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conversational iteration &amp;gt; form-based editing.&lt;/strong&gt; Early prototypes had sliders and dropdowns for every design parameter. Turns out "make the font bigger and move the logo to the left" works way better than a settings panel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generating 3 variations beats generating 1.&lt;/strong&gt; Users almost never love the first output. But when they see 3 options side by side, they can articulate what they want by pointing at what they like from each one.&lt;/p&gt;




&lt;p&gt;It's live at &lt;a href="https://inkpress.app" rel="noopener noreferrer"&gt;inkpress.app&lt;/a&gt;. Would love to hear feedback, especially from anyone who's worked with print design or AI-assisted creative tools.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I built an on-device duplicate photo finder for iPhone (and why I avoided the cloud)</title>
      <dc:creator>heocoi</dc:creator>
      <pubDate>Wed, 25 Feb 2026 16:40:23 +0000</pubDate>
      <link>https://dev.to/heocoi/i-built-an-on-device-duplicate-photo-finder-for-iphone-and-why-i-avoided-the-cloud-1ndn</link>
      <guid>https://dev.to/heocoi/i-built-an-on-device-duplicate-photo-finder-for-iphone-and-why-i-avoided-the-cloud-1ndn</guid>
      <description>&lt;p&gt;My camera roll had over 12,000 photos. A lot of them were near-identical shots I never cleaned up - bursts, multiple takes of the same thing, old screenshots.&lt;/p&gt;

&lt;p&gt;I tried a few duplicate cleaners. Most only caught exact pixel duplicates. That helped maybe 1% of the problem.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;Scans your photo library, groups photos that look similar (not just exact copies), and helps you keep one best shot from each group.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;scans the full library in under a minute&lt;/li&gt;
&lt;li&gt;groups near-duplicate photos side by side&lt;/li&gt;
&lt;li&gt;picks a candidate "best shot" in each group based on sharpness&lt;/li&gt;
&lt;li&gt;lets you delete the extras&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why on-device only
&lt;/h2&gt;

&lt;p&gt;The obvious path was to send photos to a server, run similarity matching there, and return results. Cheaper to build, easier to iterate.&lt;/p&gt;

&lt;p&gt;I didn't do that for two reasons.&lt;/p&gt;

&lt;p&gt;First, privacy. Uploading someone's personal photos to a server - even temporarily - is a trust problem I didn't want to create.&lt;/p&gt;

&lt;p&gt;Second, Apple's Vision framework is actually good enough to do this locally. The image embedding and similarity matching runs fast on modern iPhones. No server needed.&lt;/p&gt;

&lt;p&gt;Zero outbound network calls. Photos never leave the device.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technical notes
&lt;/h2&gt;

&lt;p&gt;The core of the app is image feature extraction via Vision's &lt;code&gt;VNGenerateImageFeaturePrintRequest&lt;/code&gt;. It generates a compact embedding for each photo, then I cluster similar embeddings together using cosine distance.&lt;/p&gt;

&lt;p&gt;"Best shot" selection looks at estimated sharpness (Laplacian variance on a downsampled version) and basic composition heuristics. It is not perfect but it is right most of the time.&lt;/p&gt;

&lt;p&gt;PhotoKit handles library access. The whole scan pipeline runs in a background task so the UI stays responsive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Business model
&lt;/h2&gt;

&lt;p&gt;Free to scan the full library. Free tier allows deleting up to 25 photos per month. One-time lifetime purchase available.&lt;/p&gt;

&lt;h2&gt;
  
  
  Current status
&lt;/h2&gt;

&lt;p&gt;Would like feedback on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;whether the grouping accuracy holds up on large real-world libraries&lt;/li&gt;
&lt;li&gt;cases where the "best pick" is clearly wrong&lt;/li&gt;
&lt;li&gt;any UX friction in the review flow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;App Store: &lt;a href="https://apps.apple.com/us/app/snapsift/id6759221222" rel="noopener noreferrer"&gt;https://apps.apple.com/us/app/snapsift/id6759221222&lt;/a&gt;&lt;br&gt;
Landing page: &lt;a href="https://snapsift.app" rel="noopener noreferrer"&gt;https://snapsift.app&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ios</category>
      <category>swift</category>
      <category>buildinpublic</category>
      <category>indiedev</category>
    </item>
    <item>
      <title>How to Check if Your VPN is Working on Mac: DNS Leak Testing Guide</title>
      <dc:creator>heocoi</dc:creator>
      <pubDate>Sun, 25 Jan 2026 08:24:31 +0000</pubDate>
      <link>https://dev.to/heocoi/how-to-check-if-your-vpn-is-working-on-mac-dns-leak-testing-guide-14m9</link>
      <guid>https://dev.to/heocoi/how-to-check-if-your-vpn-is-working-on-mac-dns-leak-testing-guide-14m9</guid>
      <description>&lt;p&gt;Your VPN icon shows "connected." Your ISP can still see everything you're doing.&lt;/p&gt;

&lt;p&gt;DNS leaks. IPv6 leaks. WebRTC leaks. Your VPN can be active while your traffic leaks around it. Here's how to know if your VPN is actually protecting you—and how to fix it when it's not.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem: VPN ≠ Privacy
&lt;/h2&gt;

&lt;p&gt;Most people assume their VPN works because the app says "connected." That's not enough.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What actually happens:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You connect to a VPN server in Switzerland&lt;/li&gt;
&lt;li&gt;Your Mac sends DNS requests to your ISP (leak)&lt;/li&gt;
&lt;li&gt;Your ISP sees every website you visit&lt;/li&gt;
&lt;li&gt;Your VPN did nothing&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is a &lt;strong&gt;DNS leak&lt;/strong&gt;—your DNS queries bypass the VPN tunnel. Your ISP, government, or network admin can see your browsing history even though you're "protected."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it happens:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;macOS defaults to system DNS servers&lt;/li&gt;
&lt;li&gt;VPN apps don't always override DNS properly&lt;/li&gt;
&lt;li&gt;IPv6 traffic can bypass VPN entirely&lt;/li&gt;
&lt;li&gt;VPN disconnect without killing internet (no kill switch)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; The VPN icon in your menu bar lies. You need to verify.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 3 Types of VPN Leaks
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. DNS Leak (Most Common)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What it is:&lt;/strong&gt; Your DNS requests go to your ISP instead of your VPN's DNS servers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; DNS queries reveal every domain you visit. If your ISP handles DNS, they see your browsing history regardless of VPN encryption.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to detect:&lt;/strong&gt; If a DNS leak test shows your ISP's DNS servers instead of your VPN provider's, you're leaking.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. IPv6 Leak
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What it is:&lt;/strong&gt; Your Mac uses IPv6 internet traffic that bypasses the VPN tunnel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; Most VPNs only tunnel IPv4. If you have IPv6 enabled, that traffic leaks your real location and identity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to detect:&lt;/strong&gt; If an IPv6 test shows your real ISP IPv6 address while connected to VPN, you're leaking.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. WebRTC Leak (Browser-Based)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What it is:&lt;/strong&gt; Browsers can expose your real IP address through WebRTC, even with VPN connected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; Websites can detect your true IP despite the VPN.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to detect:&lt;/strong&gt; If a WebRTC test shows your real IP alongside the VPN IP, you're leaking.&lt;/p&gt;




&lt;h2&gt;
  
  
  Manual Testing: The Free Way
&lt;/h2&gt;

&lt;p&gt;You can test for leaks manually using online tools. Here's the process:&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Check Your Baseline IP
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Before connecting to VPN:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Visit &lt;a href="https://ipleak.net" rel="noopener noreferrer"&gt;ipleak.net&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Note your real IP address and location&lt;/li&gt;
&lt;li&gt;Note your ISP's DNS servers&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is what you're trying to hide.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Connect to VPN
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Connect to your VPN:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Choose a server in a different country&lt;/li&gt;
&lt;li&gt;Wait for connection to establish&lt;/li&gt;
&lt;li&gt;Verify VPN app shows "connected"&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 3: Test for DNS Leaks
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Check DNS servers:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Visit &lt;a href="https://www.dnsleaktest.com" rel="noopener noreferrer"&gt;dnsleaktest.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Click "Extended Test"&lt;/li&gt;
&lt;li&gt;Wait for results&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;What to look for:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Good:&lt;/strong&gt; DNS servers belong to your VPN provider or are in the VPN server's country&lt;/li&gt;
&lt;li&gt;❌ &lt;strong&gt;Bad:&lt;/strong&gt; DNS servers belong to your ISP or show your real location&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 4: Test for IPv6 Leaks
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Check IPv6 address:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Visit &lt;a href="https://test-ipv6.com" rel="noopener noreferrer"&gt;test-ipv6.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Check if IPv6 address is detected&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;What to look for:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Good:&lt;/strong&gt; "No IPv6 address detected" or VPN provider's IPv6 address&lt;/li&gt;
&lt;li&gt;❌ &lt;strong&gt;Bad:&lt;/strong&gt; Your real ISP IPv6 address shows up&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 5: Test for WebRTC Leaks
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Check browser leaks:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Visit &lt;a href="https://browserleaks.com/webrtc" rel="noopener noreferrer"&gt;browserleaks.com/webrtc&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Look at "Local IP Address" section&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;What to look for:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Good:&lt;/strong&gt; Only VPN IP addresses appear&lt;/li&gt;
&lt;li&gt;❌ &lt;strong&gt;Bad:&lt;/strong&gt; Your local network IP (192.168.x.x or 10.x.x.x) appears&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Quick fix for WebRTC leaks:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use browser extensions like "Disable WebRTC" (Chrome/Firefox)&lt;/li&gt;
&lt;li&gt;Or disable WebRTC in browser settings&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Fixing Common Leaks
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Fix DNS Leaks
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Option 1: Use VPN's DNS servers&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check if your VPN app has "Use VPN DNS" option&lt;/li&gt;
&lt;li&gt;Enable it in VPN settings&lt;/li&gt;
&lt;li&gt;Reconnect and retest&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Option 2: Manually set DNS in macOS&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;strong&gt;System Settings → Network&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Select your network connection&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Details&lt;/strong&gt; → &lt;strong&gt;DNS&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Remove ISP DNS servers&lt;/li&gt;
&lt;li&gt;Add VPN provider's DNS servers&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Option 3: Enable VPN kill switch&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Most VPN apps have this feature&lt;/li&gt;
&lt;li&gt;Blocks all internet if VPN disconnects&lt;/li&gt;
&lt;li&gt;Prevents accidental leaks&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Fix IPv6 Leaks
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Option 1: Disable IPv6 (Recommended)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most VPNs don't support IPv6. Safest to disable it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;strong&gt;System Settings → Network&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Select your connection → &lt;strong&gt;Details&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;TCP/IP&lt;/strong&gt; tab&lt;/li&gt;
&lt;li&gt;Set "Configure IPv6" to &lt;strong&gt;Link-local only&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;OK&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Option 2: Use VPN with IPv6 support&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mullvad, IVPN, and some others support IPv6&lt;/li&gt;
&lt;li&gt;Verify with their documentation&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Fix WebRTC Leaks
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Browser settings:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Safari:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Safari doesn't leak WebRTC by default (Apple disabled it)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Chrome:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Install "WebRTC Leak Prevent" extension&lt;/li&gt;
&lt;li&gt;Or use Brave browser (blocks by default)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Firefox:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Type &lt;code&gt;about:config&lt;/code&gt; in address bar&lt;/li&gt;
&lt;li&gt;Search for &lt;code&gt;media.peerconnection.enabled&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set to &lt;code&gt;false&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Automatic Monitoring vs. Manual Testing
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Problem with manual testing:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You have to remember to check&lt;/li&gt;
&lt;li&gt;VPN can disconnect silently while you work&lt;/li&gt;
&lt;li&gt;Leaks can start mid-session&lt;/li&gt;
&lt;li&gt;Testing every site visit is impractical&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example scenario:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You connect to VPN before work&lt;/li&gt;
&lt;li&gt;VPN silently drops connection during a Zoom call&lt;/li&gt;
&lt;li&gt;You browse for 2 hours without noticing&lt;/li&gt;
&lt;li&gt;ISP logs everything&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;macOS doesn't alert you when VPN disconnects. You might not notice for hours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; One-time tests are snapshots. Continuous monitoring catches problems immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automated VPN Monitoring Tools
&lt;/h2&gt;

&lt;p&gt;Several tools provide persistent monitoring:&lt;/p&gt;

&lt;h3&gt;
  
  
  Built-in: macOS System Preferences
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What it shows:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Active VPN connection indicator (lock icon)&lt;/li&gt;
&lt;li&gt;VPN on/off status&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Limitations:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No leak detection&lt;/li&gt;
&lt;li&gt;No alerts on disconnect&lt;/li&gt;
&lt;li&gt;No DNS/IPv6 verification&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Basic awareness that VPN is connected (not that it's working correctly).&lt;/p&gt;

&lt;h3&gt;
  
  
  Third-Party: VPN Provider Apps
&lt;/h3&gt;

&lt;p&gt;Most VPN apps include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Connection status&lt;/li&gt;
&lt;li&gt;Server selection&lt;/li&gt;
&lt;li&gt;Kill switch option&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Limitations:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Don't test for leaks&lt;/li&gt;
&lt;li&gt;Assume DNS routing is correct&lt;/li&gt;
&lt;li&gt;No verification of actual traffic flow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Connecting to VPN, but not verifying privacy.&lt;/p&gt;

&lt;h3&gt;
  
  
  VPN Peek
&lt;/h3&gt;

&lt;p&gt;A dedicated menu bar leak monitor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Real-time DNS leak detection&lt;/li&gt;
&lt;li&gt;IPv6 leak checking&lt;/li&gt;
&lt;li&gt;Alerts when VPN disconnects&lt;/li&gt;
&lt;li&gt;Auto-refresh leak tests at intervals&lt;/li&gt;
&lt;li&gt;Shows current IP and location&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Requires separate app&lt;/li&gt;
&lt;li&gt;Not built into VPN client&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Users who want continuous verification without manual testing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Choosing the Right VPN for Mac
&lt;/h2&gt;

&lt;p&gt;Not all VPNs handle macOS properly. Look for:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Native macOS App
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; Native apps integrate better with macOS network stack.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Avoid:&lt;/strong&gt; Browser extensions (leak prone), manual OpenVPN configs (complex)&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Kill Switch Feature
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What it does:&lt;/strong&gt; Blocks all internet if VPN disconnects&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why essential:&lt;/strong&gt; Prevents accidental leaks between disconnect and reconnect&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test it:&lt;/strong&gt; Disconnect VPN manually, try browsing. Should be blocked.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. DNS Leak Protection
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What to look for:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Use VPN DNS" or "DNS leak protection" toggle&lt;/li&gt;
&lt;li&gt;Custom DNS servers option&lt;/li&gt;
&lt;li&gt;Automatic DNS override&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Test it:&lt;/strong&gt; Run DNS leak test with feature on/off&lt;/p&gt;

&lt;h3&gt;
  
  
  4. IPv6 Handling
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Options:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;IPv6 support:&lt;/strong&gt; VPN tunnels IPv6 traffic (rare)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IPv6 blocking:&lt;/strong&gt; VPN disables IPv6 to prevent leaks (common)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No handling:&lt;/strong&gt; You must disable IPv6 manually (bad)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Recommended VPNs with good macOS support:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mullvad (open source, IPv6 support, kill switch)&lt;/li&gt;
&lt;li&gt;IVPN (privacy-focused, leak protection)&lt;/li&gt;
&lt;li&gt;ProtonVPN (secure core, NetShield)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;(This is not sponsored. These just handle macOS correctly.)&lt;/p&gt;




&lt;h2&gt;
  
  
  When to Test Your VPN
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Daily Routine
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Morning:&lt;/strong&gt; Verify VPN connected and no leaks before starting work&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why:&lt;/strong&gt; VPN might have disconnected overnight, DNS settings can reset&lt;/p&gt;

&lt;h3&gt;
  
  
  After Network Changes
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Test after:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Switching WiFi networks (home → coffee shop)&lt;/li&gt;
&lt;li&gt;macOS updates&lt;/li&gt;
&lt;li&gt;VPN app updates&lt;/li&gt;
&lt;li&gt;Changing VPN servers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why:&lt;/strong&gt; Network changes can break VPN routing&lt;/p&gt;

&lt;h3&gt;
  
  
  Random Spot Checks
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Test during:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Before accessing sensitive accounts&lt;/li&gt;
&lt;li&gt;When using public WiFi&lt;/li&gt;
&lt;li&gt;When browsing from restricted locations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why:&lt;/strong&gt; VPN can fail silently. Regular verification catches issues.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common macOS VPN Issues
&lt;/h2&gt;

&lt;h3&gt;
  
  
  "VPN Connected" but Internet Doesn't Work
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Cause:&lt;/strong&gt; Split tunneling or routing conflict&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Disconnect VPN&lt;/li&gt;
&lt;li&gt;Flush DNS: &lt;code&gt;sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Reconnect VPN&lt;/li&gt;
&lt;li&gt;Test connection&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  DNS Requests Still Go to ISP
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Cause:&lt;/strong&gt; macOS not respecting VPN DNS servers&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;System Settings → Network → Advanced → DNS&lt;/li&gt;
&lt;li&gt;Remove all DNS servers&lt;/li&gt;
&lt;li&gt;Add only VPN provider's DNS&lt;/li&gt;
&lt;li&gt;Drag VPN DNS to top of list&lt;/li&gt;
&lt;li&gt;Apply changes&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  VPN Keeps Disconnecting
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Cause:&lt;/strong&gt; Firewall or antivirus interference&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check System Settings → Network → Firewall&lt;/li&gt;
&lt;li&gt;Add VPN app to allowed list&lt;/li&gt;
&lt;li&gt;Disable "Block all incoming connections"&lt;/li&gt;
&lt;li&gt;Restart Mac&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  My Recommendation
&lt;/h2&gt;

&lt;p&gt;For most Mac users, here's the practical approach:&lt;/p&gt;

&lt;h3&gt;
  
  
  Initial Setup (One-time)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Test your VPN manually&lt;/strong&gt; using ipleak.net and dnsleaktest.com&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix any leaks&lt;/strong&gt; before relying on VPN&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enable kill switch&lt;/strong&gt; in VPN app settings&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disable IPv6&lt;/strong&gt; unless your VPN supports it&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Ongoing Monitoring
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Option A: Manual (Free)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Test weekly with online leak tests&lt;/li&gt;
&lt;li&gt;Check before sensitive browsing&lt;/li&gt;
&lt;li&gt;Requires discipline&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Option B: Automated&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use menu bar monitor like VPN Peek&lt;/li&gt;
&lt;li&gt;Get alerts on disconnect or leaks&lt;/li&gt;
&lt;li&gt;Continuous peace of mind&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You don't need paranoia-level checking. But you should verify your VPN works more than once.&lt;/p&gt;




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

&lt;p&gt;The VPN icon in your menu bar only tells you the app is connected. It doesn't tell you if your privacy is protected.&lt;/p&gt;

&lt;p&gt;DNS leaks, IPv6 leaks, and silent disconnections happen frequently. macOS won't warn you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test manually:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use ipleak.net, dnsleaktest.com, test-ipv6.com&lt;/li&gt;
&lt;li&gt;Check after setup and network changes&lt;/li&gt;
&lt;li&gt;Free but requires manual checking&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Monitor automatically:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;VPN Peek for continuous leak detection&lt;/li&gt;
&lt;li&gt;Alerts when VPN fails&lt;/li&gt;
&lt;li&gt;Persistent visibility in menu bar&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pick based on your threat model. If you use VPN casually for geo-unblocking, manual tests are fine. If you rely on VPN for privacy or work remotely with sensitive data, automated monitoring catches failures before they matter.&lt;/p&gt;

&lt;p&gt;Either way, "connected" doesn't mean "protected." Verify.&lt;/p&gt;




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

&lt;p&gt;Continuous VPN monitoring and leak detection from your Mac menu bar. Get instant alerts when your VPN disconnects or DNS/IPv6 leaks are detected—no manual testing required.&lt;br&gt;
Menu Bar DNS Leak Test IPv6 Detection&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://apps.apple.com/us/app/vpn-peek/id6757384027?mt=12" rel="noopener noreferrer"&gt;Download via App Store&lt;/a&gt;&lt;/p&gt;

</description>
      <category>howto</category>
      <category>learning</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Monitor Battery Health on Mac: Built-in vs Third-Party Apps</title>
      <dc:creator>heocoi</dc:creator>
      <pubDate>Sat, 17 Jan 2026 02:28:25 +0000</pubDate>
      <link>https://dev.to/heocoi/how-to-monitor-battery-health-on-mac-built-in-vs-third-party-apps-32ic</link>
      <guid>https://dev.to/heocoi/how-to-monitor-battery-health-on-mac-built-in-vs-third-party-apps-32ic</guid>
      <description>&lt;p&gt;Your MacBook battery is a consumable. After enough charge cycles, it degrades. Knowing when that degradation happens—before your laptop dies mid-presentation—is worth a few minutes of setup.&lt;/p&gt;

&lt;p&gt;Here's how to monitor battery health on macOS, from built-in tools to dedicated apps.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Built-in Method: System Information
&lt;/h2&gt;

&lt;p&gt;Apple provides basic battery stats in System Information. Here's how to access them:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Hold &lt;strong&gt;Option&lt;/strong&gt; and click the Apple menu&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;System Information&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Hardware → Power&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You'll see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cycle Count&lt;/strong&gt; — How many full charge cycles the battery has completed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Condition&lt;/strong&gt; — Apple's assessment (Normal, Replace Soon, Service Battery)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full Charge Capacity&lt;/strong&gt; — Current maximum capacity in mAh&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Health Information&lt;/strong&gt; — Maximum capacity percentage (macOS 11+)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a quick check, this works. Apple considers batteries "healthy" until they drop below 80% of original capacity or reach their rated cycle count (typically 1000 cycles for modern MacBooks).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation:&lt;/strong&gt; You have to dig through menus every time. No persistent monitoring, no alerts, no history.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Built-in Tools Fall Short
&lt;/h2&gt;

&lt;p&gt;System Information tells you the current state. It doesn't tell you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How fast your battery is degrading&lt;/li&gt;
&lt;li&gt;Whether your charging habits are causing damage&lt;/li&gt;
&lt;li&gt;Real-time power consumption by apps&lt;/li&gt;
&lt;li&gt;Temperature during charging (heat kills batteries)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you charge to 100% every night and leave it plugged in, you're accelerating wear. If you frequently drain to 0%, same problem. The built-in tools won't warn you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; Apple's tools show status. They don't help you improve habits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Third-Party Apps: The Options
&lt;/h2&gt;

&lt;p&gt;Several apps fill this gap. Here's an honest comparison.&lt;/p&gt;

&lt;h3&gt;
  
  
  coconutBattery (Free / $10 Pro)
&lt;/h3&gt;

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

&lt;p&gt;The veteran. coconutBattery has been around since 2005 and remains reliable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Shows detailed battery stats&lt;/li&gt;
&lt;li&gt;Tracks history over time&lt;/li&gt;
&lt;li&gt;Can read iPhone/iPad battery health via USB&lt;/li&gt;
&lt;li&gt;Pro version adds menu bar display&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Interface feels dated&lt;/li&gt;
&lt;li&gt;Pro version required for menu bar&lt;/li&gt;
&lt;li&gt;No charging management features&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Users who want detailed stats and iPhone monitoring.&lt;/p&gt;




&lt;h3&gt;
  
  
  AlDente (Free / $22 Pro)
&lt;/h3&gt;

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

&lt;p&gt;Focused on extending battery lifespan through charge limiting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Limits maximum charge level (e.g., cap at 80%)&lt;/li&gt;
&lt;li&gt;Heat protection features&lt;/li&gt;
&lt;li&gt;Good for users who stay plugged in&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Doesn't monitor health—manages charging&lt;/li&gt;
&lt;li&gt;Overkill if you use your laptop unplugged often&lt;/li&gt;
&lt;li&gt;Pro version required for advanced features&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Desktop-mode users who stay plugged in most of the time.&lt;/p&gt;




&lt;h3&gt;
  
  
  Battery Vitals
&lt;/h3&gt;

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

&lt;p&gt;A lightweight menu bar utility focused on at-a-glance monitoring.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Clean, minimal interface&lt;/li&gt;
&lt;li&gt;Menu bar icon shows current health&lt;/li&gt;
&lt;li&gt;One-click access to key stats&lt;/li&gt;
&lt;li&gt;No configuration required&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fewer features than coconutBattery&lt;/li&gt;
&lt;li&gt;No charge limiting like AlDente&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Users who want simple, persistent monitoring without complexity.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Metrics Actually Matter
&lt;/h2&gt;

&lt;p&gt;Apps show dozens of numbers. Focus on these three:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Cycle Count
&lt;/h3&gt;

&lt;p&gt;One cycle = using 100% of battery capacity (not necessarily in one session). Using 50% today and 50% tomorrow equals one cycle.&lt;/p&gt;

&lt;p&gt;Modern MacBooks are rated for 1000 cycles before dropping to 80% capacity. If you're at 800 cycles and battery health is still 95%, you're doing well. If you're at 300 cycles and health is 85%, something's wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Maximum Capacity
&lt;/h3&gt;

&lt;p&gt;This percentage shows how much charge your battery holds compared to when it was new. 100% is factory condition. Below 80% is "degraded" by Apple's standards.&lt;/p&gt;

&lt;p&gt;The rate of decline matters more than the current number. Losing 5% in two years is normal. Losing 5% in three months suggests a problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Temperature During Charging
&lt;/h3&gt;

&lt;p&gt;Batteries degrade faster when charged hot. If your MacBook runs hot (resource-intensive apps, poor ventilation), battery wear accelerates.&lt;/p&gt;

&lt;p&gt;Some apps show charging temperature. If you consistently see temps above 35°C (95°F) during charging, consider improving airflow or limiting intensive tasks while plugged in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; Track trends, not just snapshots. A single reading tells you little.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Recommendation
&lt;/h2&gt;

&lt;p&gt;For most users, a lightweight menu bar app provides the right balance.&lt;/p&gt;

&lt;p&gt;Here's my approach:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Glance at the menu bar&lt;/strong&gt; — See current health without opening anything&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check detailed stats monthly&lt;/strong&gt; — Look for unusual degradation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;React to alerts&lt;/strong&gt; — If health drops suddenly, investigate&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You don't need to obsess over battery stats. You need to catch problems before they strand you without power.&lt;/p&gt;

&lt;p&gt;System Information works for occasional checks. But if you want persistent awareness without thinking about it, a menu bar utility is worth it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Habits That Actually Help
&lt;/h2&gt;

&lt;p&gt;Beyond monitoring, these habits extend battery life:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Avoid extreme charge levels&lt;/strong&gt; — The 20-80% range is gentlest on lithium-ion batteries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't leave it plugged in at 100% indefinitely&lt;/strong&gt; — Modern macOS has Optimized Battery Charging to help, but it's not perfect&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep it cool&lt;/strong&gt; — Don't charge while running intensive tasks if avoidable&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update macOS&lt;/strong&gt; — Apple occasionally improves battery management in software updates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Monitoring tells you when these habits aren't working. That's its value.&lt;/p&gt;

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

&lt;p&gt;Apple's built-in tools provide basic battery stats. They require manual checking and don't track history or provide alerts.&lt;/p&gt;

&lt;p&gt;Third-party apps fill the gap:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;coconutBattery&lt;/strong&gt; for detailed analysis and iOS device monitoring&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AlDente&lt;/strong&gt; for charge limiting if you're always plugged in&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Battery Vitals&lt;/strong&gt; for simple, persistent menu bar monitoring&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pick based on your usage pattern. If you're frequently mobile and want to catch degradation early, a lightweight monitor like Battery Vitals makes sense. If you're docked most of the time, AlDente's charge limiting might extend your battery's lifespan.&lt;/p&gt;

&lt;p&gt;Either way, knowing your battery health beats being surprised when it fails.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://apps.apple.com/us/app/battery-vitals/id6757302299?mt=12" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0oe3tfc7dkilchlhckir.png" alt="Battery Vitals" width="800" height="585"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I built a Mac app to stop exceeding my mobile data cap</title>
      <dc:creator>heocoi</dc:creator>
      <pubDate>Wed, 14 Jan 2026 14:27:28 +0000</pubDate>
      <link>https://dev.to/heocoi/i-built-a-mac-app-to-stop-exceeding-my-mobile-data-cap-2d8c</link>
      <guid>https://dev.to/heocoi/i-built-a-mac-app-to-stop-exceeding-my-mobile-data-cap-2d8c</guid>
      <description>&lt;p&gt;Ever tethered from your iPhone for "just a quick Zoom call" and ended up burning through half your monthly data?&lt;/p&gt;

&lt;p&gt;Yeah, me too. Multiple times.&lt;/p&gt;

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

&lt;p&gt;macOS doesn't distinguish between your home WiFi and your iPhone hotspot. Activity Monitor shows total bandwidth, but not "how much data did I use specifically on this limited connection?"&lt;/p&gt;

&lt;p&gt;Existing solutions either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TripMode&lt;/strong&gt; ($15/year) - Overkill. I don't need app blocking, just awareness&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bandwidth+&lt;/strong&gt; (Free) - Doesn't know the difference between hotspot and home WiFi&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;A simple menu bar app that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Detects hotspot connections using &lt;code&gt;NWPathMonitor.isExpensive&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Tracks data per session and per month&lt;/li&gt;
&lt;li&gt;Supports multiple profiles (personal phone, work phone, travel MiFi)&lt;/li&gt;
&lt;li&gt;Alerts before you hit your cap&lt;/li&gt;
&lt;/ul&gt;

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

&lt;h2&gt;
  
  
  Technical bits
&lt;/h2&gt;

&lt;p&gt;Built with SwiftUI + AppKit (NSMenu for native feel). Uses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;NWPathMonitor&lt;/code&gt; for connection type detection&lt;/li&gt;
&lt;li&gt;CoreWLAN for SSID identification (to auto-switch profiles)&lt;/li&gt;
&lt;li&gt;Pure system frameworks, no external dependencies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;macOS 14+ only because I wanted to use the &lt;code&gt;@Observable&lt;/code&gt; macro.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Sweet spot matters&lt;/strong&gt; - Found the gap between "too basic" and "overkill"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Location permission UX&lt;/strong&gt; - Needed for SSID access on Sonoma+, had to explain clearly why&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Menu bar apps&lt;/strong&gt; - NSMenu &amp;gt; NSPopover for authentic macOS feel&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;This is part of my "Peek" series - simple utilities that surface hidden macOS info:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;VPN Peek (VPN status + leak detection)&lt;/li&gt;
&lt;li&gt;Signal Peek (WiFi signal strength)&lt;/li&gt;
&lt;li&gt;Battery Vitals (battery health)&lt;/li&gt;
&lt;li&gt;Storage Peek (disk info)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Available on the Mac App Store for $4.99 one-time purchase. &lt;br&gt;
&lt;strong&gt;No BS subscriptions.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://apps.apple.com/us/app/hotspot-peek/id6757591640?mt=12" rel="noopener noreferrer"&gt;https://apps.apple.com/us/app/hotspot-peek/id6757591640?mt=12&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;What's your experience with data caps? Any features you'd want to see?&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>swift</category>
      <category>productivity</category>
      <category>development</category>
    </item>
    <item>
      <title>Why I Build macOS Menu Bar Apps</title>
      <dc:creator>heocoi</dc:creator>
      <pubDate>Mon, 12 Jan 2026 14:27:49 +0000</pubDate>
      <link>https://dev.to/heocoi/why-i-build-macos-menu-bar-apps-2eg</link>
      <guid>https://dev.to/heocoi/why-i-build-macos-menu-bar-apps-2eg</guid>
      <description>&lt;p&gt;Building software has always been about solving problems for me. Not big, world-changing problems necessarily, but the small frustrations that add up throughout the day. The kind of friction that makes you think, "There has to be a better way."&lt;/p&gt;

&lt;p&gt;That's how I got into building macOS menu bar apps.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Appeal of Menu Bar Apps
&lt;/h2&gt;

&lt;p&gt;Menu bar apps occupy this unique space in the macOS ecosystem. They're always accessible, yet unobtrusive. They live in the background, ready when you need them, invisible when you don't. This constraint—having to be useful while taking up minimal screen real estate—forces you to focus on what truly matters.&lt;/p&gt;

&lt;p&gt;I love that challenge.&lt;/p&gt;

&lt;p&gt;Each app I build is designed around a single purpose. Battery Vitals monitors your battery health. Storage Peek shows you what's eating your disk space. SignalPeek keeps tabs on your network. One tool, one job, done well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Minimal Matters
&lt;/h2&gt;

&lt;p&gt;We live in an age of feature creep. Apps that start simple inevitably accumulate buttons, settings, and capabilities until they become bloated and confusing. I've fallen into that trap before, adding features because I could, not because I should.&lt;/p&gt;

&lt;p&gt;With menu bar apps, there's a natural pressure to stay minimal. You simply don't have the real estate to add unnecessary complexity. This constraint is liberating. It forces me to make hard choices about what stays and what goes.&lt;/p&gt;

&lt;p&gt;The result? Apps that are easier to build, easier to maintain, and—most importantly—easier to use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building for Myself (And Others Like Me)
&lt;/h2&gt;

&lt;p&gt;Every app I've built started as a tool I needed for myself. I got tired of diving into System Preferences to check my battery health. I wanted a quick way to visualize my storage without waiting for Finder to analyze my drives. I needed network stats at a glance.&lt;/p&gt;

&lt;p&gt;Building for my own needs keeps me honest. I can't hide behind market research or user personas. If I wouldn't use it daily, why would anyone else?&lt;/p&gt;

&lt;p&gt;But here's the beautiful part: when you build something genuinely useful for yourself, you often discover there are thousands of other people with the same problem. That's when an indie project becomes something bigger.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Joy of Shipping
&lt;/h2&gt;

&lt;p&gt;There's something deeply satisfying about shipping a complete product. Not a feature, not a component, but a whole thing that someone can download and use immediately. Menu bar apps, by their nature, are small enough to ship regularly.&lt;/p&gt;

&lt;p&gt;This rapid feedback loop "build, ship, iterate" keeps me motivated. I'm not working on something for months before users see it. I can release an MVP in weeks, get real feedback, and improve from there.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;I'm continuing to explore this space. There are so many small problems worth solving, so many opportunities to build something minimal and useful. Each app teaches me something new about design, development, and what people actually need versus what they think they want.&lt;/p&gt;

&lt;p&gt;If you're thinking about building your own macOS apps, my advice is simple: start small, solve your own problems, and don't be afraid to ship something minimal. The world doesn't need another bloated feature-rich app. It needs more tools that do one thing exceptionally well.&lt;/p&gt;

&lt;p&gt;That's what I'm building. One menu bar app at a time.&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>learning</category>
    </item>
    <item>
      <title>What I Learned Building a Native macOS Menu Bar App</title>
      <dc:creator>heocoi</dc:creator>
      <pubDate>Thu, 08 Jan 2026 13:39:56 +0000</pubDate>
      <link>https://dev.to/heocoi/what-i-learned-building-a-native-macos-menu-bar-app-4im6</link>
      <guid>https://dev.to/heocoi/what-i-learned-building-a-native-macos-menu-bar-app-4im6</guid>
      <description>&lt;p&gt;I just shipped VPN Peek - a menu bar utility for macOS — and the journey taught me more about AppKit than I expected.&lt;/p&gt;

&lt;p&gt;Here's what I wish someone told me before I started.&lt;/p&gt;

&lt;h2&gt;
  
  
  NSPopover vs NSMenu: Choose Wisely
&lt;/h2&gt;

&lt;p&gt;Most tutorials show NSPopover for menu bar apps. It's easy — just attach a SwiftUI view and you're done.&lt;/p&gt;

&lt;p&gt;But it never felt right. The popover has a slight delay, doesn't dismiss naturally, and looks like a "floating app" rather than a system utility.&lt;/p&gt;

&lt;p&gt;I switched to &lt;strong&gt;NSMenu with custom views&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;statusItem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;NSStatusBar&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;statusItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;withLength&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;NSStatusItem&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;variableLength&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;menu&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;NSMenu&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;customItem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;NSMenuItem&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;customItem&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;view&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;NSHostingView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;rootView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;YourSwiftUIView&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="n"&gt;menu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customItem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;statusItem&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;menu&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;menu&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference? It feels like a real Mac app now. Instant open, standard dismiss behavior, native animations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; If your app lives in the menu bar, make it &lt;em&gt;feel&lt;/em&gt; like it belongs there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hybrid Trap
&lt;/h2&gt;

&lt;p&gt;"Just use SwiftUI" is tempting advice. But menu bar apps hit SwiftUI's edge cases fast:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Window management is weird&lt;/li&gt;
&lt;li&gt;Menu styling is limited
&lt;/li&gt;
&lt;li&gt;Some AppKit APIs have no SwiftUI equivalent&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I ended up with roughly 70% SwiftUI, 30% AppKit. SwiftUI for views and state, AppKit for system integration.&lt;/p&gt;

&lt;p&gt;Don't fight it. Embrace the hybrid.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sandbox Will Break Your Heart
&lt;/h2&gt;

&lt;p&gt;My original feature list was ambitious. Then I met macOS sandbox.&lt;/p&gt;

&lt;p&gt;Things I couldn't do in a Mac App Store app:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Access other apps' network connections&lt;/li&gt;
&lt;li&gt;Read system logs&lt;/li&gt;
&lt;li&gt;Monitor processes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Half my "cool ideas" died here. But honestly? It forced me to focus. The app does fewer things, but does them well.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; Check sandbox limitations &lt;em&gt;before&lt;/em&gt; you design features, not after.&lt;/p&gt;

&lt;h2&gt;
  
  
  Small Details That Matter
&lt;/h2&gt;

&lt;p&gt;Some things I obsessed over that users actually noticed:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Adaptive menu bar icon.&lt;/strong&gt; Light icon on dark menu bar, dark icon on light. Sounds obvious, but many apps get this wrong.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="n"&gt;statusItem&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;NSImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;named&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"MenuIcon"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;statusItem&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isTemplate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="c1"&gt;// This one line handles dark/light mode&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Proper number formatting.&lt;/strong&gt; "1234 ms" vs "1,234 ms" vs "1.2s". Tiny detail, but it signals polish.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No Dock icon.&lt;/strong&gt; Menu bar apps shouldn't clutter the Dock.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Info.plist --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;LSUIElement&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;true/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These aren't features. They're expectations. Miss them and your app feels off.&lt;/p&gt;

&lt;h2&gt;
  
  
  App Store Review: Expect the Unexpected
&lt;/h2&gt;

&lt;p&gt;My first submission was rejected. Reason? The app "didn't have enough functionality."&lt;/p&gt;

&lt;p&gt;I added a few quality-of-life features, resubmitted, approved.&lt;/p&gt;

&lt;p&gt;No logic change. Just perception.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; Reviewers spend seconds on your app. Make sure it looks complete at first glance.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Start with NSMenu from day one.&lt;/strong&gt; I wasted time on NSPopover before accepting it wasn't right.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build the preferences window early.&lt;/strong&gt; I kept hardcoding values during development. Technical debt adds up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Target macOS 14+ and don't look back.&lt;/strong&gt; Modern Swift features like &lt;code&gt;@Observable&lt;/code&gt; are worth dropping older OS support.&lt;/p&gt;

&lt;h2&gt;
  
  
  Worth It?
&lt;/h2&gt;

&lt;p&gt;Building for macOS is humbling. The platform has opinions, and fighting them is painful.&lt;/p&gt;

&lt;p&gt;But when everything clicks — when your app feels like it &lt;em&gt;belongs&lt;/em&gt; on macOS — that's satisfying in a way web development never gave me.&lt;/p&gt;

&lt;p&gt;VPN Peek is on the &lt;a href="https://apps.apple.com/us/app/vpn-peek/id6757384027" rel="noopener noreferrer"&gt;Mac App Store&lt;/a&gt; if you want to see the result. &lt;/p&gt;

&lt;p&gt;And I'm launching on Product Hunt this Saturday (Jan 10) if you want to support the journey.&lt;/p&gt;

&lt;p&gt;Happy to answer questions about menu bar development in the comments 👇&lt;/p&gt;

</description>
      <category>swift</category>
      <category>beginners</category>
      <category>tutorial</category>
      <category>learning</category>
    </item>
  </channel>
</rss>
