<?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: Emiliano Saurin</title>
    <description>The latest articles on DEV Community by Emiliano Saurin (@njoylab).</description>
    <link>https://dev.to/njoylab</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%2F3436939%2F07167ad8-1f28-4946-8bb0-085474557fd2.jpeg</url>
      <title>DEV Community: Emiliano Saurin</title>
      <link>https://dev.to/njoylab</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/njoylab"/>
    <language>en</language>
    <item>
      <title>Your Emails Go to Spam Because of Three DNS Records You Never Set Up</title>
      <dc:creator>Emiliano Saurin</dc:creator>
      <pubDate>Mon, 20 Apr 2026 13:31:55 +0000</pubDate>
      <link>https://dev.to/njoylab/your-emails-go-to-spam-because-of-three-dns-records-you-never-set-up-3ej7</link>
      <guid>https://dev.to/njoylab/your-emails-go-to-spam-because-of-three-dns-records-you-never-set-up-3ej7</guid>
      <description>&lt;p&gt;As a fractional CTO working with startups in the B4i program, I've seen the same email delivery problem show up over and over. The app was fine. The email content was fine. The issue was usually three DNS records nobody had set up properly.&lt;/p&gt;

&lt;p&gt;If you've ever deployed something that sends email and then discovered nobody was receiving it, these are the first records to check.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Quick DNS Refresher (Skip If You Know This)
&lt;/h2&gt;

&lt;p&gt;DNS records are how the internet maps names to things. You probably know A records (domain → IP address) and CNAME records (domain → another domain). Here's the rest of the cast, briefly:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AAAA&lt;/strong&gt;: same as A, but IPv6. You need this if you want your site reachable over IPv6.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MX&lt;/strong&gt;: tells the world where your email goes. The number is priority (lower = preferred). MX values must be hostnames, not IP addresses. This trips people up.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;example&lt;/span&gt;.&lt;span class="n"&gt;com&lt;/span&gt;.    &lt;span class="n"&gt;MX&lt;/span&gt;    &lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="n"&gt;mail&lt;/span&gt;.&lt;span class="n"&gt;example&lt;/span&gt;.&lt;span class="n"&gt;com&lt;/span&gt;.
&lt;span class="n"&gt;example&lt;/span&gt;.&lt;span class="n"&gt;com&lt;/span&gt;.    &lt;span class="n"&gt;MX&lt;/span&gt;    &lt;span class="m"&gt;20&lt;/span&gt; &lt;span class="n"&gt;backup&lt;/span&gt;-&lt;span class="n"&gt;mail&lt;/span&gt;.&lt;span class="n"&gt;example&lt;/span&gt;.&lt;span class="n"&gt;com&lt;/span&gt;.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;TXT&lt;/strong&gt;: arbitrary text. In practice it's used for SPF, DKIM, DMARC, and domain verification ("add this TXT record to prove you own the domain").&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NS&lt;/strong&gt;: points to your nameservers. You touch this when switching DNS providers and basically never otherwise.&lt;/p&gt;

&lt;p&gt;You can check all of these at once with a DNS lookup tool instead of running multiple &lt;code&gt;dig&lt;/code&gt; commands.&lt;/p&gt;

&lt;h2&gt;
  
  
  Now: Why Your Email Goes to Spam
&lt;/h2&gt;

&lt;p&gt;Three records get you from "I send email" to "receiving servers can actually trust it." They're not the only thing that affects deliverability, but they're the baseline.&lt;/p&gt;

&lt;h3&gt;
  
  
  SPF: Who's Allowed to Send
&lt;/h3&gt;

&lt;p&gt;SPF is a TXT record that declares which servers are allowed to send mail for your envelope sender domain.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;v=spf1 include:_spf.google.com include:sendgrid.net -all
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Translation: Google can send, SendGrid can send, everyone else should fail SPF (&lt;code&gt;-all&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;The gotchas:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DNS lookup limit.&lt;/strong&gt; SPF allows max 10 DNS lookups. Each &lt;code&gt;include:&lt;/code&gt; is one. Nested includes count too. If you use Google Workspace + SendGrid + Mailchimp + HubSpot, you're probably already over. An SPF checker makes this easy to validate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;~all&lt;/code&gt; vs &lt;code&gt;-all&lt;/code&gt;.&lt;/strong&gt; Tilde means soft fail, unauthorized emails are usually accepted but flagged. Dash means SPF returns &lt;code&gt;fail&lt;/code&gt;. Many receivers will penalize or reject those messages, but the final decision is still theirs. Use &lt;code&gt;-all&lt;/code&gt; unless you have a specific reason not to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Forgetting a sender.&lt;/strong&gt; You set up SPF for your marketing tool but not your transactional email service. Welcome emails fail SPF and start landing in spam.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multiple SPF records.&lt;/strong&gt; You can only have one. Adding a second one doesn't add senders, it breaks everything.&lt;/p&gt;

&lt;h3&gt;
  
  
  DKIM: Proving Emails Weren't Tampered With
&lt;/h3&gt;

&lt;p&gt;DKIM is a cryptographic signature. Your email provider signs every outgoing email with a private key, and publishes the public key in DNS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;selector1._domainkey.yourdomain.com TXT "v=DKIM1; k=rsa; p=MIGfMA0GCS..."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Receiving servers check the signature. If someone modified the email in transit (or forged it), the signature won't match.&lt;/p&gt;

&lt;p&gt;Setup is straightforward: your email provider gives you a DNS record to add, you add it, done. But two things go wrong regularly:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wrong selector.&lt;/strong&gt; DKIM records are per-selector. Google uses &lt;code&gt;google&lt;/code&gt;, SendGrid uses &lt;code&gt;s1&lt;/code&gt; and &lt;code&gt;s2&lt;/code&gt;. If you're checking the wrong selector, it looks like DKIM is missing when it's fine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Short keys.&lt;/strong&gt; RSA keys should be 2048 bits minimum. Some old setups still use 512 or 1024. The shorter the key, the easier it is to forge signatures.&lt;/p&gt;

&lt;h3&gt;
  
  
  DMARC: What Happens When SPF/DKIM Don't Align
&lt;/h3&gt;

&lt;p&gt;DMARC ties SPF and DKIM to the visible &lt;code&gt;From:&lt;/code&gt; domain. It tells receiving servers what to do when neither SPF nor DKIM passes in alignment, and where to send reports about it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;_dmarc.yourdomain.com TXT "v=DMARC1; p=reject; rua=mailto:dmarc@yourdomain.com"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;p=&lt;/code&gt; tag is the policy: &lt;code&gt;none&lt;/code&gt; (just report), &lt;code&gt;quarantine&lt;/code&gt; (usually send to spam), &lt;code&gt;reject&lt;/code&gt; (ask receivers to reject it).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't start with &lt;code&gt;p=reject&lt;/code&gt;.&lt;/strong&gt; If your SPF or DKIM alignment is wrong, you can break your own mail. Roll out gradually:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start with &lt;code&gt;p=none&lt;/code&gt; and a &lt;code&gt;rua=&lt;/code&gt; email address. Collect reports for a few weeks.&lt;/li&gt;
&lt;li&gt;Read the reports. They show who's sending from your domain and whether they pass. You'll probably find senders you forgot about.&lt;/li&gt;
&lt;li&gt;Move to &lt;code&gt;p=quarantine&lt;/code&gt; with &lt;code&gt;pct=25&lt;/code&gt; (only quarantine 25% at first).&lt;/li&gt;
&lt;li&gt;Ramp up to &lt;code&gt;p=reject&lt;/code&gt; once you're confident nothing legitimate is failing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A DMARC checker will show your current policy quickly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fastest Way to Check a Domain
&lt;/h2&gt;

&lt;p&gt;If you're debugging a real domain, this is the order I use:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run a &lt;a href="https://lookup.echovalue.dev/dns-lookup/" rel="noopener noreferrer"&gt;DNS Lookup&lt;/a&gt; to confirm the domain resolves and the expected MX/TXT records exist.&lt;/li&gt;
&lt;li&gt;Run the &lt;a href="https://lookup.echovalue.dev/spf-check/" rel="noopener noreferrer"&gt;SPF checker&lt;/a&gt; to catch multiple records, missing senders, or the 10-lookup limit.&lt;/li&gt;
&lt;li&gt;Run the &lt;a href="https://lookup.echovalue.dev/dkim-check/" rel="noopener noreferrer"&gt;DKIM checker&lt;/a&gt; with the selector your provider gave you.&lt;/li&gt;
&lt;li&gt;Run the &lt;a href="https://lookup.echovalue.dev/dmarc-check/" rel="noopener noreferrer"&gt;DMARC checker&lt;/a&gt; to verify policy and reporting setup.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That usually gets you to the real problem faster than bouncing between provider dashboards and raw &lt;code&gt;dig&lt;/code&gt; output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging DNS Problems (While We're Here)
&lt;/h2&gt;

&lt;p&gt;Since you're already looking at DNS, a few common issues and what to check:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Site doesn't resolve.&lt;/strong&gt; NS records might be pointing to the wrong provider. Or the domain expired. Check the basics first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Changes not showing up.&lt;/strong&gt; DNS caching. Every record has a TTL. If yours is 3600 (1 hour), changes can take up to an hour to show up in resolvers you don't control. Tip: lower the TTL to 300 &lt;em&gt;before&lt;/em&gt; making changes, then raise it back after.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SSL certificate won't provision.&lt;/strong&gt; Let's Encrypt uses a TXT record for DNS challenges. If it hasn't propagated yet, provisioning fails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reverse DNS fails.&lt;/strong&gt; Some services (especially email servers) check PTR records. Given your IP, does it resolve back to your domain? This is set by your hosting provider, not your DNS provider.&lt;/p&gt;

&lt;h2&gt;
  
  
  tl;dr
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Record&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;What breaks without it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SPF&lt;/td&gt;
&lt;td&gt;Authorizes senders for the envelope domain&lt;/td&gt;
&lt;td&gt;Mail can fail authentication or lose trust&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DKIM&lt;/td&gt;
&lt;td&gt;Signs outgoing mail with a verifiable key&lt;/td&gt;
&lt;td&gt;Messages are easier to spoof or modify undetected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DMARC&lt;/td&gt;
&lt;td&gt;Enforces alignment and reporting for the &lt;code&gt;From:&lt;/code&gt; domain&lt;/td&gt;
&lt;td&gt;Spoofing is harder to detect and control&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Set up all three. If you want to validate them quickly, you can use &lt;a href="https://lookup.echovalue.dev" rel="noopener noreferrer"&gt;lookup.echovalue.dev&lt;/a&gt;. Start DMARC in report-only mode, fix what fails, then enforce.&lt;/p&gt;

</description>
      <category>dns</category>
      <category>email</category>
      <category>devops</category>
      <category>security</category>
    </item>
    <item>
      <title>I Built a JSON Fixer Because I Was Tired of Counting Characters</title>
      <dc:creator>Emiliano Saurin</dc:creator>
      <pubDate>Mon, 13 Apr 2026 21:05:00 +0000</pubDate>
      <link>https://dev.to/njoylab/i-built-a-json-fixer-because-i-was-tired-of-counting-characters-4ikb</link>
      <guid>https://dev.to/njoylab/i-built-a-json-fixer-because-i-was-tired-of-counting-characters-4ikb</guid>
      <description>&lt;p&gt;&lt;code&gt;JSON.parse()&lt;/code&gt; gives you an error at "position 42". Great. Position 42 of a 500-line config file. Let me just count characters manually, I guess.&lt;/p&gt;

&lt;p&gt;I hit this so often that at some point I stopped counting and built a &lt;a href="https://jsonlint.echovalue.dev/" rel="noopener noreferrer"&gt;JSON Linter&lt;/a&gt; that shows errors with actual line numbers. But more on that in a second. First, let's talk about why JSON keeps breaking in the same ways.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's Always the Same Five Things
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Trailing commas.&lt;/strong&gt; You delete the last field, forget the comma on the one above it. Or you copy from JavaScript, where trailing commas are fine. JSON disagrees.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Alice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"age"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Single quotes.&lt;/strong&gt; A Python habit. Or a "I'll just type this real quick" habit. JSON wants double quotes, always.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Comments.&lt;/strong&gt; JSON has no comments. None. This is honestly a spec failure. &lt;code&gt;tsconfig.json&lt;/code&gt; and VS Code settings use JSONC precisely because people need comments in config files. But standard JSON? Nope.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;this&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;breaks&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;everything&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.example.com"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Unquoted keys.&lt;/strong&gt; Valid JavaScript, invalid JSON. &lt;code&gt;{ name: "Alice" }&lt;/code&gt; fails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Missing brackets.&lt;/strong&gt; Usually from truncated logs or half-pasted API responses.&lt;/p&gt;

&lt;p&gt;These five cover maybe 90% of all JSON parse errors I've ever seen.&lt;/p&gt;

&lt;h2&gt;
  
  
  So I Built a Fixer
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://jsonlint.echovalue.dev/" rel="noopener noreferrer"&gt;JSON Linter and Fixer&lt;/a&gt; does two things:&lt;/p&gt;

&lt;p&gt;Paste broken JSON, hit "Fix" and it auto-repairs trailing commas, single quotes, comments, unquoted keys. It uses &lt;a href="https://github.com/josdejong/jsonrepair" rel="noopener noreferrer"&gt;jsonrepair&lt;/a&gt; under the hood. It won't magically reconstruct corrupted data, but if the JSON is &lt;em&gt;almost&lt;/em&gt; right (and it usually is), it'll clean it up.&lt;/p&gt;

&lt;p&gt;Hit "Lint" and you get properly formatted output with syntax highlighting. Useful even when the JSON isn't broken, just minified into an unreadable blob.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"users"&lt;/span&gt;&lt;span class="p"&gt;:[{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Alice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"age"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"addresses"&lt;/span&gt;&lt;span class="p"&gt;:[{&lt;/span&gt;&lt;span class="nl"&gt;"city"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"NYC"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"zip"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"10001"&lt;/span&gt;&lt;span class="p"&gt;}]},{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Bob"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"age"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"addresses"&lt;/span&gt;&lt;span class="p"&gt;:[{&lt;/span&gt;&lt;span class="nl"&gt;"city"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"LA"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"zip"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"90001"&lt;/span&gt;&lt;span class="p"&gt;}]}]}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nobody can read that. One click and it's formatted.&lt;/p&gt;

&lt;p&gt;Everything runs client-side. No backend, nothing uploaded anywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Other Half: jq Without Installing Anything
&lt;/h2&gt;

&lt;p&gt;Once the JSON is valid, you usually need to pull something out of it. "Give me all the user names." "Which items have status &lt;code&gt;failed&lt;/code&gt;?" "Group these by category."&lt;/p&gt;

&lt;p&gt;You could write a quick script. Or you could use &lt;code&gt;jq&lt;/code&gt;, which does this kind of thing in one line. Except now you need to install &lt;code&gt;jq&lt;/code&gt;, and you're on a machine where you can't, or you don't feel like it.&lt;/p&gt;

&lt;p&gt;So the same site has a &lt;a href="https://jsonlint.echovalue.dev/jq-playground/" rel="noopener noreferrer"&gt;jq playground&lt;/a&gt;. Paste JSON, write a filter, see the result. Browser-only, same as the linter.&lt;/p&gt;

&lt;p&gt;Some examples that come up all the time, given this data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Alice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"age"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"admin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"active"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bob"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"age"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"active"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Carol"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"age"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"admin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"active"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pull all names:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Filter active admins over 30:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;.[]&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;and&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;age&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reshape into a different format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;isAdmin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;admin&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;Group by role and count:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;group_by&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&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;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last one gives you &lt;code&gt;[{"role": "admin", "count": 2}, {"role": "user", "count": 1}]&lt;/code&gt;. The kind of thing you'd normally open Python for.&lt;/p&gt;

&lt;p&gt;Other one-liners I use constantly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;[.[]&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;unique&lt;/span&gt;           &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;unique&lt;/span&gt; &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="nx"&gt;an&lt;/span&gt; &lt;span class="nx"&gt;array&lt;/span&gt;
&lt;span class="nf"&gt;max_by&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                       &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;most&lt;/span&gt; &lt;span class="nx"&gt;expensive&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;
&lt;span class="p"&gt;[.&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;[].&lt;/span&gt;&lt;span class="nx"&gt;addresses&lt;/span&gt;&lt;span class="p"&gt;[].&lt;/span&gt;&lt;span class="nx"&gt;city&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;unique&lt;/span&gt; &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;flatten&lt;/span&gt; &lt;span class="nx"&gt;and&lt;/span&gt; &lt;span class="nx"&gt;dedupe&lt;/span&gt; &lt;span class="nx"&gt;nested&lt;/span&gt; &lt;span class="nx"&gt;arrays&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get the idea. &lt;a href="https://jsonlint.echovalue.dev/jq-playground/" rel="noopener noreferrer"&gt;Try it with your own data&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;I keep the &lt;a href="https://jsonlint.echovalue.dev/" rel="noopener noreferrer"&gt;linter&lt;/a&gt; bookmarked. API returns garbage, paste, fix, lint. Need to dig into the response, switch to the jq tab. It handles maybe 80% of my "what's in this JSON?" moments without opening a terminal.&lt;/p&gt;

&lt;p&gt;The whole thing is &lt;a href="https://github.com/njoylab/json-linter-tool" rel="noopener noreferrer"&gt;open source on GitHub&lt;/a&gt; if you want to look at the code or contribute.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Part of &lt;a href="https://www.echovalue.dev" rel="noopener noreferrer"&gt;echoValue&lt;/a&gt;. Free, no signups, no tracking.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>json</category>
      <category>jq</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>InstaTrack: Track Your Instagram Followers &amp; Following (Privacy-First, Open Source)</title>
      <dc:creator>Emiliano Saurin</dc:creator>
      <pubDate>Mon, 06 Oct 2025 13:29:12 +0000</pubDate>
      <link>https://dev.to/njoylab/instatrack-track-your-instagram-followers-following-privacy-first-open-source-5291</link>
      <guid>https://dev.to/njoylab/instatrack-track-your-instagram-followers-following-privacy-first-open-source-5291</guid>
      <description>&lt;p&gt;After getting frustrated with Instagram analytics apps that require account access or send data to their servers or are not free I built InstaTrack: a fully client-side tool to analyze followers/following.&lt;/p&gt;

&lt;p&gt;What it does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Upload Instagram JSON exports (followers/following)&lt;/li&gt;
&lt;li&gt;See who doesn't follow you back and vice versa&lt;/li&gt;
&lt;li&gt;Track trends over time with multiple snapshots&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All processing happens in your browser (localStorage), zero server involvement&lt;br&gt;
Try it: &lt;a href="https://instatrack.njoylab.com" rel="noopener noreferrer"&gt;https://instatrack.njoylab.com&lt;/a&gt;&lt;br&gt;
GitHub: &lt;a href="https://github.com/njoylab/instatrack" rel="noopener noreferrer"&gt;https://github.com/njoylab/instatrack&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I have few followers, so haven't tested performance with large datasets (10k+ followers). If anyone tries it with a big following, would love feedback on how it handles.&lt;/p&gt;

&lt;p&gt;Open to any feedback on UX or features!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>privacy</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>[iOS] Built an offline vault app - need honest UI/UX feedback</title>
      <dc:creator>Emiliano Saurin</dc:creator>
      <pubDate>Fri, 15 Aug 2025 10:07:23 +0000</pubDate>
      <link>https://dev.to/njoylab/ios-built-an-offline-vault-app-need-honest-uiux-feedback-1cph</link>
      <guid>https://dev.to/njoylab/ios-built-an-offline-vault-app-need-honest-uiux-feedback-1cph</guid>
      <description>&lt;p&gt;Hey folks,&lt;/p&gt;

&lt;p&gt;I recently shipped “In My Pocket”&lt;/p&gt;

&lt;p&gt;&lt;a href="https://apps.apple.com/us/app/in-my-pocket-offline-vault/id6444294006" rel="noopener noreferrer"&gt;https://apps.apple.com/us/app/in-my-pocket-offline-vault/id6444294006&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Main idea:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;100% offline storage, no cloud, no accounts, just local data&lt;/li&gt;
&lt;li&gt;Store key/value pairs like IBANs, loyalty cards, access codes, etc.&lt;/li&gt;
&lt;li&gt;Core features are free, a $3 one-time IAP unlocks a few extras&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Where I need help:&lt;br&gt;
I deliberately went with stock iOS components for speed and familiarity, but the result feels very plain. I’m looking for constructive UI/UX suggestions to make it cleaner and more pleasant without adding bloat.&lt;/p&gt;

&lt;p&gt;Next update: import/export between devices&lt;/p&gt;

&lt;p&gt;Would love if you could check it out and tell me what you’d change. Brutal honesty welcome.&lt;/p&gt;

</description>
      <category>ios</category>
      <category>sideprojects</category>
      <category>appstore</category>
      <category>privacy</category>
    </item>
  </channel>
</rss>
