<?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: Hamed</title>
    <description>The latest articles on DEV Community by Hamed (@godofweb).</description>
    <link>https://dev.to/godofweb</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4112847%2F56f00235-2436-413f-9397-53a395ac3c27.png</url>
      <title>DEV Community: Hamed</title>
      <link>https://dev.to/godofweb</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/godofweb"/>
    <language>en</language>
    <item>
      <title>Browser first: the one rule behind every tool on KitDev Space</title>
      <dc:creator>Hamed</dc:creator>
      <pubDate>Sun, 06 Sep 2026 22:30:42 +0000</pubDate>
      <link>https://dev.to/godofweb/browser-first-the-one-rule-behind-every-tool-on-kitdev-space-19id</link>
      <guid>https://dev.to/godofweb/browser-first-the-one-rule-behind-every-tool-on-kitdev-space-19id</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6h0o052bn0tu8b9mb4y5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6h0o052bn0tu8b9mb4y5.png" alt="KitDev Space home page. Headline " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A while ago I caught myself pasting a JWT from a production API into the first "JWT decoder" Google gave me. I wanted to read the claims. I didn't stop to think about where the token went, and honestly, neither does anyone else. We do the same with &lt;code&gt;.env&lt;/code&gt; files in JSON converters and with photos in EXIF strippers. The site has a privacy policy that says the server keeps nothing, and we take its word for it.&lt;/p&gt;

&lt;p&gt;That moment is roughly why &lt;a href="https://kitdev.space" rel="noopener noreferrer"&gt;KitDev Space&lt;/a&gt; exists. I wanted a set of everyday developer tools where I didn't have to take anyone's word for it, including my own. The way I got there was less about the tools and more about one rule I wrote down early and then refused to bend. This post is about that rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule
&lt;/h2&gt;

&lt;p&gt;Run the work in the browser. Use a server only when the browser genuinely can't do the job.&lt;/p&gt;

&lt;p&gt;The first half is easy to agree with. The second half is where it gets interesting, because "can't" has to mean something concrete or every tool ends up on the server for convenience. So I wrote down the situations that actually qualify, and the list ended up shorter than I expected:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The work needs a native library. Encoding AVIF or rasterizing an SVG needs a real image codec. Opening a TLS handshake needs a socket.&lt;/li&gt;
&lt;li&gt;The work needs to talk to another host. DNS records, HTTP headers, RDAP, OpenGraph previews. The browser can't make those requests because of CORS, and it shouldn't be able to.&lt;/li&gt;
&lt;li&gt;The work needs a runtime API with no browser equivalent, like &lt;code&gt;Bun.dns&lt;/code&gt; or &lt;code&gt;Bun.Archive&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The input is too large for a browser tab to hold comfortably.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If a tool doesn't hit one of those, it stays in the browser. I've been tempted to make exceptions a few times, usually when the server version would have been twenty lines and the browser version two hundred. I haven't yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  The browser does more than I gave it credit for
&lt;/h2&gt;

&lt;p&gt;Going in, I assumed I'd be writing a lot of server code. I was wrong about that. Most of what a developer tool site does is already covered by platform APIs or by a small dependency that runs fine in a tab.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Work&lt;/th&gt;
&lt;th&gt;What handles it in the browser&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SHA-1, SHA-256, SHA-384, SHA-512&lt;/td&gt;
&lt;td&gt;&lt;code&gt;crypto.subtle.digest&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Random bytes, UUIDs&lt;/td&gt;
&lt;td&gt;&lt;code&gt;crypto.getRandomValues&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AES-256-GCM, PBKDF2, HMAC&lt;/td&gt;
&lt;td&gt;&lt;code&gt;crypto.subtle&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resize, rotate, crop&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;OffscreenCanvas&lt;/code&gt;, &lt;code&gt;createImageBitmap&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JPEG, PNG, WebP encode&lt;/td&gt;
&lt;td&gt;&lt;code&gt;OffscreenCanvas.convertToBlob&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reading EXIF and GPS metadata&lt;/td&gt;
&lt;td&gt;a &lt;code&gt;DataView&lt;/code&gt; over the file bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gzip, deflate, tar&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fflate&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;YAML, TOML, JSONC, JSON5&lt;/td&gt;
&lt;td&gt;&lt;code&gt;confbox&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQLite&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sql.js&lt;/code&gt; (WebAssembly)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So the JWT debugger verifies HS256 signatures with Web Crypto and never sees a server. The AES tool derives its key with PBKDF2 in your tab. The tar explorer opens an archive without uploading it. The SQLite studio runs an actual database in memory. None of that required anything clever, just a willingness to check what the platform already had before reaching for a route.&lt;/p&gt;

&lt;p&gt;Web Crypto does have gaps. There's no MD5, no CRC32, no xxHash. I could have pulled in a JavaScript MD5 library and kept everything client side, but the hash tool also offers xxhash64 and wyhash, and shipping all of that to the browser for algorithms most people rarely use felt wrong. So the hash generator keeps a server path for those, and the page says so in plain words when you pick one. I'd rather tell you which option leaves your machine than pretend nothing does.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc2bu1tppttnsjq5wl4ii.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc2bu1tppttnsjq5wl4ii.png" alt="Hash Generator with SHA-256 selected. A green notice reads " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  One module, two paths
&lt;/h2&gt;

&lt;p&gt;A tool with both a browser path and a server path is a maintenance trap waiting to happen. Two implementations, one of them quietly drifts, and now the same input gives different output depending on where it ran. I got bitten by exactly this on the data converter early on.&lt;/p&gt;

&lt;p&gt;The fix I settled on: each shared module exports the function that does the work plus a small guard that answers "can this run here?". The page checks the guard. The server route imports the same module. The code that actually hashes a file exists once.&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="c1"&gt;// shared/utils/crypto/hash.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SUBTLE_NAMES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;sha1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SHA-1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SHA-256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;sha384&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SHA-384&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;sha512&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SHA-512&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;canHashInBrowser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;algorithm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HashAlgorithm&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;algorithm&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;SUBTLE_NAMES&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// simplified page logic&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;canHashInBrowser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;algorithm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;hashInBrowser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;algorithm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;$fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/hash&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;algorithm&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;Hashing, data format conversion, code formatting and semver all use this shape now. When a new tool needs both paths, it gets the same treatment, and I don't have to think about it anymore.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the server is actually for
&lt;/h2&gt;

&lt;p&gt;The tools that do need a server run on &lt;a href="https://bun.sh" rel="noopener noreferrer"&gt;Bun&lt;/a&gt;. Each one is there because of one of the four reasons above, and I can tell you which:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The TLS Certificate Inspector opens a socket and reads the chain, the SANs and the cipher suite.&lt;/li&gt;
&lt;li&gt;DNS Lookup and the Email Health Inspector query A, MX, TXT, SPF, DKIM and DMARC records.&lt;/li&gt;
&lt;li&gt;The HTTP Inspector, OpenGraph Previewer and RDAP Lookup all fetch from a third-party host.&lt;/li&gt;
&lt;li&gt;Image Studio and the Favicon Set Generator use &lt;code&gt;Bun.Image&lt;/code&gt; for AVIF and ICO output.&lt;/li&gt;
&lt;li&gt;The TS/JSX Transpiler and the AST Playground run OXC, which is native code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because every server route is a liability, they all go through the same checklist before they ship. Validate the URL and allow only &lt;code&gt;http&lt;/code&gt; and &lt;code&gt;https&lt;/code&gt;. Block localhost, private ranges, link-local, cloud metadata hosts, and the IPv6 transition ranges that smuggle an IPv4 address inside (I learned about NAT64 and Teredo the hard way while writing that blocklist). Restrict the destination port, so the TLS inspector can't be turned into a port scanner. Check &lt;code&gt;content-length&lt;/code&gt;, then stream the body and stop at a cap instead of buffering it. Rate-limit by a client key that isn't derived from &lt;code&gt;x-forwarded-for&lt;/code&gt;, since anyone can set that header. Keep uploads in memory and never touch the disk. And store no input and no output, period.&lt;/p&gt;

&lt;p&gt;Analytics follows the same idea. An event carries the tool id and nothing else. I can see that someone opened the cron visualizer today. I have no idea what expression they typed, and I like it that way.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff7st5wgc37gtusfgltfv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff7st5wgc37gtusfgltfv.png" alt="SQLite Studio with a 7.5 MB database of 55,991 companies open in the tab. The query ran in 4 ms, and the file never left the browser." width="800" height="456"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Making the rule visible
&lt;/h2&gt;

&lt;p&gt;A rule that only I know about isn't a privacy feature. It's a promise. So the sidebar shows where each tool runs.&lt;/p&gt;

&lt;p&gt;Every tool has a small badge next to its name. 🔒 Client means nothing on that page calls the server. ⚡ Bun means every action does. A tool with both paths shows no badge, and the page itself explains which action leaves the browser. The badge isn't something I type by hand, either. It comes from two flags in the tool registry, the same registry that generates the page title and the search index, so it can't drift out of sync with the code.&lt;/p&gt;

&lt;p&gt;And the code is public. KitDev Space is open source under the MIT license, and the repository is linked from the site footer. If a badge says Client, you don't have to trust the badge. You can open the tool's module and see for yourself that nothing in it calls a server. That was the last piece of the "don't take my word for it" idea, and honestly the part I should have done first.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  What's in there
&lt;/h2&gt;

&lt;p&gt;New tools land most weeks, so I won't give you a count. Here are the ones I open most often, by category.&lt;/p&gt;

&lt;p&gt;Data: a JSON formatter that accepts JSON5 and JSONC, JSON to TypeScript, converters between YAML, TOML, XML and CSV (including CSV to SQL inserts), a table viewer for large CSVs, and the SQLite studio.&lt;/p&gt;

&lt;p&gt;Dev: a regex tester that explains each token, a cron visualizer that shows the next runs, cURL to fetch/Axios/Python/Go, HTML and SVG to JSX or Vue, a glob tester, and the tar explorer.&lt;/p&gt;

&lt;p&gt;Network: TLS inspector, DNS lookup, email health check, HTTP header inspector, CIDR calculator, cookie inspector, RDAP lookup.&lt;/p&gt;

&lt;p&gt;Crypto: hash generator, HMAC, JWT debugger, TOTP generator, AES encrypt and decrypt, UUID and passphrase generators.&lt;/p&gt;

&lt;p&gt;Color: an OKLCH converter, a WCAG contrast checker, a Tailwind shade generator, a CSS gradient studio, and palette extraction from an image.&lt;/p&gt;

&lt;p&gt;Image: crop, resize, rotate and convert in one pass, an EXIF inspector and remover, SVG to PNG or WebP at 1x/2x/4x, and a favicon set generator that hands you a ZIP.&lt;/p&gt;

&lt;p&gt;There's no account, no paywall, and the whole thing is open source. The site follows your system color scheme, so you won't have to hunt for a toggle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Under the hood
&lt;/h2&gt;

&lt;p&gt;Nuxt 4 and Nuxt UI 4 on the front end, VueUse for composables, CodeMirror for the editors, Bun for the server routes, Vitest and Playwright for tests, deployed on Vercel. Nothing exotic.&lt;/p&gt;

&lt;h2&gt;
  
  
  I'd like your help
&lt;/h2&gt;

&lt;p&gt;If you try &lt;a href="https://kitdev.space" rel="noopener noreferrer"&gt;kitdev.space&lt;/a&gt;, I have two requests. Tell me which tool you wanted and couldn't find. And if you ever catch a tool sending something to the server that should have stayed in your browser, tell me that first. It would mean the rule has a hole, and I'd want to know.&lt;/p&gt;

&lt;p&gt;The repository is one click away from the footer. Issues and pull requests are welcome, whether it's a new tool, a fix, or just a note that a badge is wrong. A tool hub like this gets better with more hands on it, which is the main reason I opened the source.&lt;/p&gt;

&lt;p&gt;If you build tools of your own, take the rule with you. Write down the handful of cases where the browser truly can't do the work. Everything else belongs on the user's machine.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>privacy</category>
      <category>bunjs</category>
    </item>
  </channel>
</rss>
