Category: Tools & Setup

Tools & Setup is where orthogonal.info curates practical, battle-tested guides on developer productivity tools, CLI utilities, self-hosted software, and environment configuration. Whether you are bootstrapping a new development machine, evaluating self-hosted alternatives to SaaS products, or fine-tuning your terminal workflow, this category delivers step-by-step walkthroughs grounded in real-world experience. Every article is written with one goal: help you build a faster, more reliable, and more enjoyable development environment.

With over 25 in-depth posts and growing, Tools & Setup is one of the most active categories on the site — reflecting just how much time engineers spend (and save) by getting their tooling right from day one.

Key Topics Covered

Command-line productivity — Shell customization (Zsh, Fish, Starship), terminal multiplexers (tmux, Zellij), and CLI utilities like ripgrep, fd, fzf, and bat that supercharge daily workflows.
Self-hosted alternatives — Deploying and configuring tools like Gitea, Nextcloud, Vaultwarden, and Uptime Kuma so you own your data without sacrificing usability.
IDE and editor setup — Configuration guides for VS Code, Neovim, and JetBrains IDEs, including extension recommendations, keybindings, and remote development workflows.
Development environment automation — Using Ansible, Homebrew, Nix, dotfiles repositories, and container-based dev environments (Dev Containers, Devbox) to make setups reproducible.
Git workflows and tooling — Advanced Git techniques, hooks, aliases, and GUI clients that streamline version control for solo developers and teams alike.
API testing and debugging — Hands-on guides for curl, HTTPie, Postman, and browser DevTools to debug REST and GraphQL APIs efficiently.
Package and runtime management — Managing multiple language runtimes with asdf, mise, nvm, and pyenv, plus dependency management best practices.

Who This Content Is For
This category is designed for software engineers, DevOps practitioners, system administrators, and hobbyist developers who want to work smarter, not harder. Whether you are a junior developer setting up your first Linux workstation or a senior engineer optimizing a multi-machine workflow, you will find actionable advice that respects your time. The guides assume basic command-line comfort but explain advanced concepts clearly.

What You Will Learn
By exploring the articles in Tools & Setup, you will learn how to automate repetitive environment tasks so a fresh machine is productive in minutes, not days. You will discover modern CLI replacements for legacy Unix tools, understand how to evaluate self-hosted software against its SaaS equivalent, and gain confidence configuring complex development stacks. Each guide includes copy-paste commands, configuration snippets, and links to upstream documentation so you can adapt the advice to your own infrastructure.

Start browsing below to find your next productivity upgrade.

  • How to Remove GPS Location from Photos Before Sharing Online

    How to Remove GPS Location from Photos Before Sharing Online

    Every time you take a photo with your phone, the exact GPS coordinates are embedded in the image file. When you share that photo online — on forums, marketplaces, or messaging apps — anyone who downloads it can see exactly where you were standing. Here’s how to remove it in 3 seconds.

    Quick Fix: Strip All Metadata

    📌 TL;DR: Every time you take a photo with your phone, the exact GPS coordinates are embedded in the image file. When you share that photo online — on forums, marketplaces, or messaging apps — anyone who downloads it can see exactly where you were standing. Here’s how to remove it in 3 seconds.
    🎯 Quick Answer: Every smartphone photo embeds GPS coordinates in EXIF metadata that reveal your exact location. Remove GPS data in 3 seconds using a browser-based EXIF stripper—no uploads, no installs, no accounts required.
    1. Open PixelStrip
    2. Drop your photo on the page
    3. See the metadata report (GPS coordinates highlighted in red)
    4. Click “Strip All Metadata & Download”

    The downloaded photo looks identical but contains zero hidden data. No GPS, no camera model, no timestamps.

    What Metadata Is Actually in Your Photos?

    EXIF metadata was designed for photographers to track camera settings. But smartphones added fields that reveal far more than you’d expect:

    • GPS Latitude/Longitude — accurate to within 3 meters on modern phones
    • Device Model — “iPhone 16 Pro” or “Samsung Galaxy S25” — narrows down who took the photo
    • Date & Time — the exact second the photo was captured
    • Camera Serial Number — a unique identifier that links photos from the same device
    • Thumbnail — a smaller version that may contain content you cropped out

    Which Platforms Strip Metadata Automatically?

    Do strip metadata: Facebook, Instagram, Twitter/X, WhatsApp (when sent as photo, not file)

    Don’t strip metadata: Email, Telegram (file mode), Discord, Forums, Craigslist, eBay, Dropbox, Google Drive shared links

    If you’re sharing via any channel that doesn’t strip metadata, do it yourself first.

    👉 Strip your photos with PixelStrip — no upload, no account, 100% in your browser.

    Command-Line GPS Removal With ExifTool

    ExifTool is the Swiss Army knife of metadata manipulation. It reads and writes EXIF, IPTC, XMP, and dozens of other metadata formats across virtually every image type. For GPS removal specifically, it offers surgical precision — you can strip location data while preserving camera settings, copyright info, and other tags you want to keep.

    # View all GPS-related tags in a photo
    exiftool -gps:all photo.jpg
    
    # View GPS as a Google Maps link
    exiftool -n -p "https://maps.google.com/?q=$GPSLatitude,$GPSLongitude" photo.jpg
    
    # Remove ONLY GPS tags (keep everything else)
    exiftool -gps:all= photo.jpg
    
    # Remove GPS from all JPEGs in a folder
    exiftool -gps:all= -overwrite_original *.jpg
    
    # Remove GPS + create backup of originals
    exiftool -gps:all= -o stripped/ *.jpg
    
    # Nuclear option: remove ALL metadata
    exiftool -all= photo.jpg
    
    # Verify GPS is gone
    exiftool -gps:all photo.jpg
    # Should output: (no output = no GPS tags)

    The key flag is -gps:all= (with the equals sign and no value). This tells ExifTool to set all GPS-related tags to empty, effectively deleting them. The -overwrite_original flag skips creating backup files (ExifTool normally saves photo.jpg_original as a safety net). For batch processing, the wildcard *.jpg handles every JPEG in the current directory.

    For automated workflows, here’s a shell script that watches a folder and strips GPS from any new images:

    #!/bin/bash
    # auto_strip_gps.sh - Watch a folder and strip GPS from new images
    WATCH_DIR="${1:-.}"
    PROCESSED_LOG="$WATCH_DIR/.stripped_files"
    touch "$PROCESSED_LOG"
    
    echo "Watching $WATCH_DIR for new images..."
    
    while true; do
      find "$WATCH_DIR" -maxdepth 1 -type f \( -name "*.jpg" -o -name "*.jpeg" -o -name "*.png" \) | while read -r file; do
        if ! grep -qF "$file" "$PROCESSED_LOG" 2>/dev/null; then
          echo "Stripping GPS from: $file"
          exiftool -gps:all= -overwrite_original "$file" 2>/dev/null
          echo "$file" >> "$PROCESSED_LOG"
        fi
      done
      sleep 5
    done

    And here’s a Python wrapper that gives you more control — logging, error handling, and the ability to selectively preserve certain tags:

    import subprocess
    import os
    import json
    
    def get_gps_data(filepath):
        result = subprocess.run(
            ["exiftool", "-json", "-gps:all", filepath],
            capture_output=True, text=True
        )
        if result.returncode != 0:
            return None
        data = json.loads(result.stdout)
        return data[0] if data else None
    
    def strip_gps(filepath, keep_backup=False):
        cmd = ["exiftool", "-gps:all="]
        if not keep_backup:
            cmd.append("-overwrite_original")
        cmd.append(filepath)
        result = subprocess.run(cmd, capture_output=True, text=True)
        return result.returncode == 0
    
    def batch_strip_gps(folder, extensions=None):
        if extensions is None:
            extensions = {".jpg", ".jpeg", ".tiff", ".png"}
        results = {"stripped": [], "skipped": [], "errors": []}
        for fname in os.listdir(folder):
            ext = os.path.splitext(fname)[1].lower()
            if ext not in extensions:
                continue
            fpath = os.path.join(folder, fname)
            gps = get_gps_data(fpath)
            if gps and any(k.startswith("GPS") for k in gps):
                if strip_gps(fpath):
                    results["stripped"].append(fname)
                else:
                    results["errors"].append(fname)
            else:
                results["skipped"].append(fname)
        return results
    
    # Usage:
    # results = batch_strip_gps("/path/to/photos")
    # print(f"Stripped: {len(results['stripped'])}")
    # print(f"Skipped (no GPS): {len(results['skipped'])}")

    The Python wrapper adds intelligence that the raw command lacks. It checks whether GPS data actually exists before attempting removal (avoiding unnecessary file writes), tracks which files were processed, and separates results into stripped/skipped/error categories. This is useful when processing large photo libraries where most images may already be clean.

    Building a Browser-Based GPS Stripper

    Command-line tools are powerful but require installation. For a zero-install solution, you can build a browser-based GPS stripper that processes photos entirely client-side. The challenge: you need to parse the JPEG binary format, find the GPS IFD (Image File Directory) entries, and nullify them — all without corrupting the rest of the file.

    // Parse JPEG to find and nullify GPS data
    // GPS lives in the EXIF APP1 segment (marker 0xFFE1)
    
    async function removeGPSFromJPEG(file) {
      const buffer = await file.arrayBuffer();
      const bytes = new Uint8Array(buffer);
    
      // Verify JPEG signature
      if (bytes[0] !== 0xFF || bytes[1] !== 0xD8) {
        throw new Error("Not a valid JPEG file");
      }
    
      // Find APP1 (EXIF) segment
      let offset = 2;
      while (offset < bytes.length - 1) {
        if (bytes[offset] !== 0xFF) break;
        const marker = bytes[offset + 1];
    
        // APP1 = 0xE1
        if (marker === 0xE1) {
          const segLen = (bytes[offset+2] << 8) | bytes[offset+3];
          // Check for EXIF header: "Exif\0\0"
          const header = String.fromCharCode(
            bytes[offset+4], bytes[offset+5],
            bytes[offset+6], bytes[offset+7]
          );
          if (header === "Exif") {
            nullifyGPSInSegment(bytes, offset + 10, segLen - 8);
          }
        }
    
        // SOS marker = start of image data, stop parsing
        if (marker === 0xDA) break;
    
        // Skip to next segment
        const len = (bytes[offset+2] << 8) | bytes[offset+3];
        offset += 2 + len;
      }
    
      return new Blob([bytes], { type: "image/jpeg" });
    }
    
    function nullifyGPSInSegment(bytes, tiffStart, length) {
      // Read byte order: "II" (Intel/little-endian)
      // or "MM" (Motorola/big-endian)
      const isLittleEndian = (bytes[tiffStart] === 0x49);
    
      function readUint16(pos) {
        return isLittleEndian
          ? bytes[pos] | (bytes[pos+1] << 8)
          : (bytes[pos] << 8) | bytes[pos+1];
      }
    
      function readUint32(pos) {
        return isLittleEndian
          ? bytes[pos] | (bytes[pos+1]<<8) | (bytes[pos+2]<<16) | (bytes[pos+3]<<24)
          : (bytes[pos]<<24) | (bytes[pos+1]<<16) | (bytes[pos+2]<<8) | bytes[pos+3];
      }
    
      // Parse IFD0 to find GPS IFD pointer (tag 0x8825)
      const ifd0Offset = tiffStart + readUint32(tiffStart + 4);
      const entryCount = readUint16(ifd0Offset);
    
      for (let i = 0; i < entryCount; i++) {
        const entryPos = ifd0Offset + 2 + (i * 12);
        const tagId = readUint16(entryPos);
    
        // Tag 0x8825 = GPS IFD pointer
        if (tagId === 0x8825) {
          const gpsOffset = tiffStart + readUint32(entryPos + 8);
          const gpsEntries = readUint16(gpsOffset);
          // Zero out all GPS IFD entries
          for (let j = 0; j < gpsEntries * 12 + 2; j++) {
            if (gpsOffset + j < bytes.length) {
              bytes[gpsOffset + j] = 0;
            }
          }
          // Zero the GPS IFD pointer itself
          for (let j = 0; j < 12; j++) {
            bytes[entryPos + j] = 0;
          }
          break;
        }
      }
    }
    
    // Usage with File API:
    // const input = document.getElementById("fileInput");
    // input.addEventListener("change", async (e) => {
    //   const file = e.target.files[0];
    //   const cleanBlob = await removeGPSFromJPEG(file);
    //   const url = URL.createObjectURL(cleanBlob);
    //   const a = document.createElement("a");
    //   a.href = url;
    //   a.download = "nogps_" + file.name;
    //   a.click();
    // });

    This approach is more surgical than the Canvas re-encoding method. Instead of re-creating the entire image (which causes quality loss from re-compression), it modifies the binary directly — zeroing out only the GPS IFD entries while leaving everything else intact. The image pixels, camera settings, and file structure remain untouched. The tradeoff is complexity: you need to handle both byte orderings (Intel and Motorola), navigate the TIFF IFD chain, and correctly parse variable-length tag values.

    The nullifyGPSInSegment function locates the GPS IFD by scanning IFD0 for tag 0x8825 (the GPS Info IFD Pointer). Once found, it zeros out every byte in the GPS IFD block, including the pointer itself. This is a safe operation because EXIF parsers skip zero-valued entries, and the rest of the file structure remains valid. PixelStrip uses a more refined version of this technique that also handles XMP GPS data and IPTC location fields, which can duplicate GPS information in different metadata formats.

    My Photo Privacy Workflow

    I set up an automated pipeline: every photo I export goes through ExifTool before it hits any cloud service. The workflow is simple but effective — a folder action on my Mac watches my “Export” directory and runs exiftool -gps:all= -overwrite_original on any new JPEG or PNG that appears. By the time I drag a photo into an email, a chat app, or a forum post, the GPS data is already gone.

    The tradeoff between convenience and privacy is real. GPS-tagged photos are genuinely useful — they power the “Places” album in your photo library, enable location-based search, and add context to travel memories. I don’t strip GPS from photos that stay in my private library. The rule is: GPS stays on photos I keep, GPS gets removed from photos I share. That boundary is the entire workflow in one sentence.

    Here’s what I learned about platform-by-platform metadata handling after testing each one with a GPS-tagged test image:

    • Facebook/Instagram — Strips all EXIF on upload. Your photos are safe, but Facebook stores the GPS data server-side for their own use before removing it from the public file.
    • Twitter/X — Strips EXIF. Clean since 2014 after multiple privacy incidents.
    • WhatsApp — Strips EXIF when sent as a photo. But if you send as a “document” (file attachment), all metadata is preserved. This catches people off guard.
    • Telegram — Same as WhatsApp: photo mode strips, file mode preserves.
    • Discord — Does NOT strip EXIF. Every image uploaded to Discord retains full metadata including GPS. This is a major blind spot for the gaming community.
    • Email — No stripping whatsoever. Attachments are sent as-is.
    • Google Drive / Dropbox shared links — No stripping. The recipient downloads the original file with all metadata intact.
    • Forums (Reddit, Discourse, phpBB) — Depends on configuration. Reddit strips EXIF, but most self-hosted forums do not.
    • eBay / Craigslist / Marketplace — Inconsistent. Some strip, some don’t. Always strip before uploading to any marketplace.

    The takeaway: if you’re not sure whether a platform strips metadata, assume it doesn’t. It takes 3 seconds to run a photo through PixelStrip or ExifTool. That’s a trivial cost compared to the risk of leaking your home coordinates to a stranger on the internet. I treat GPS stripping the same way I treat locking my front door — it’s not paranoia, it’s just basic hygiene.

    Related Reading

    Get Weekly Security & DevOps Insights

    Join 500+ engineers getting actionable tutorials on Kubernetes security, homelab builds, and trading automation. No spam, unsubscribe anytime.

    Subscribe Free →

    Delivered every Tuesday. Read by engineers at Google, AWS, and startups.

    Frequently Asked Questions

    What is How to Remove GPS Location from Photos Before Sharing Online about?

    Every time you take a photo with your phone, the exact GPS coordinates are embedded in the image file. When you share that photo online — on forums, marketplaces, or messaging apps — anyone who downlo

    Who should read this article about How to Remove GPS Location from Photos Before Sharing Online?

    Anyone interested in learning about How to Remove GPS Location from Photos Before Sharing Online and related topics will find this article useful.

    What are the key takeaways from How to Remove GPS Location from Photos Before Sharing Online?

    Here’s how to remove it in 3 seconds. Quick Fix: Strip All Metadata Open PixelStrip Drop your photo on the page See the metadata report (GPS coordinates highlighted in red) Click “Strip All Metadata &

    References

  • Compress Images Without Losing Quality (Free Tool)

    Compress Images Without Losing Quality (Free Tool)

    You need to send a photo by email but it’s 8MB. You need to upload a product image but the CMS has a 2MB limit. You need to speed up your website but your hero image is 4MB. The solution is always the same: compress the image. But most tools upload your photo to a random server first.

    Here’s how to compress images without uploading them anywhere, using only your browser.

    The 30-Second Method

    📌 TL;DR: You need to send a photo by email but it’s 8MB. You need to upload a product image but the CMS has a 2MB limit. You need to speed up your website but your hero image is 4MB.
    🎯 Quick Answer: Compress images up to 80% smaller without visible quality loss using browser-based compression that processes photos entirely on your device. Unlike cloud tools like TinyPNG, your images never leave your browser.
    1. Open QuickShrink
    2. Drag your image onto the page (or click to select)
    3. Adjust the quality slider — 80% gives great results for most photos
    4. Click Download

    That’s it. Your image never leaves your device. The compression happens entirely in your browser using the Canvas API — the same technology that powers web-based photo editors.

    What Quality Setting Should I Use?

    80% — Best for most photos. Reduces file size by 40-60% with no visible difference. This is what most professional websites use.

    60% — Good for thumbnails, social media, and email attachments. 70-80% smaller. You might notice slight softening if you zoom in, but it looks perfect at normal viewing size.

    40% — Maximum compression. 85-90% smaller. Visible artifacts on close inspection, but fine for previews and low-bandwidth situations.

    100% — No compression. Use this only if you need to convert PNG to JPEG without quality loss.

    Why Not Use TinyPNG or Squoosh?

    TinyPNG uploads your image to their servers. For personal photos or client work, that’s a privacy concern. Also limited to 20 free compressions per month.

    Squoosh (by Google) is excellent and also runs client-side. But it’s heavier — loads a WASM codec and has a complex UI. If you just want to shrink a photo fast, it’s overkill.

    QuickShrink is a single HTML file, loads in under 200ms, works offline, and does one thing: make your image smaller. No account, no limits, no tracking.

    👉 Try QuickShrink — free, forever, private by design.

    How Image Compression Works Under the Hood

    When you drag a photo into QuickShrink and move the quality slider, here’s what actually happens at the code level. The browser’s Canvas API does the heavy lifting, but understanding the mechanics helps you choose the right settings.

    The compression pipeline has three stages:

    1. Decode — the browser reads your JPEG/PNG file and decompresses it into raw pixel data (an array of RGBA values)
    2. Render — those pixels are drawn onto an invisible HTML5 Canvas element
    3. Re-encode — the Canvas exports the image as a new JPEG at your chosen quality level

    The quality parameter controls how aggressively the JPEG encoder discards visual information. JPEG compression works by dividing the image into 8×8 pixel blocks, applying a Discrete Cosine Transform (DCT) to each block, and then quantizing the frequency coefficients. Lower quality means more aggressive quantization — more data thrown away, smaller file, more artifacts.

    The lossy vs. lossless distinction matters here. JPEG is always lossy — every re-encode loses some data. PNG is lossless — it compresses without discarding anything, which is why PNG files are larger. When QuickShrink converts a PNG to JPEG, you get dramatic size reduction because you’re switching from lossless to lossy compression.

    Here’s the JPEG quality vs. file size curve that explains the diminishing returns:

    • 100% → 95% — saves 30-40% file size with zero visible change
    • 95% → 80% — saves another 30-40% with no visible change at normal viewing
    • 80% → 60% — saves another 20-30% with slight softening visible at 100% zoom
    • 60% → 40% — saves only 10-15% more but introduces visible blockiness

    This is why 80% is the sweet spot. You capture the steepest part of the compression curve — maximum size reduction before visible quality loss.

    Here’s a complete browser-based image compressor in about 30 lines of JavaScript:

    // Complete client-side image compressor
    document.getElementById('file-input').addEventListener('change', async (e) => {
      const file = e.target.files[0];
      if (!file) return;
    
      // Decode the image
      const img = new Image();
      const objectUrl = URL.createObjectURL(file);
      await new Promise(resolve => { img.onload = resolve; img.src = objectUrl; });
      URL.revokeObjectURL(objectUrl);
    
      // Render to canvas
      const canvas = document.createElement('canvas');
      canvas.width = img.naturalWidth;
      canvas.height = img.naturalHeight;
      canvas.getContext('2d').drawImage(img, 0, 0);
    
      // Re-encode as JPEG at 80% quality
      const quality = 0.8;
      const blob = await new Promise(resolve =>
        canvas.toBlob(resolve, 'image/jpeg', quality)
      );
    
      // Show results
      const savings = ((1 - blob.size / file.size) * 100).toFixed(1);
      console.log(`Original: ${(file.size/1024).toFixed(0)}KB`);
      console.log(`Compressed: ${(blob.size/1024).toFixed(0)}KB`);
      console.log(`Saved: ${savings}%`);
    
      // Trigger download
      const a = document.createElement('a');
      a.href = URL.createObjectURL(blob);
      a.download = 'compressed-' + file.name;
      a.click();
    });

    That’s the entire core of QuickShrink. Everything else — the drag-and-drop UI, the quality slider, the preview — is just interface around these 30 lines.

    Batch Compression Script

    Browser tools work great for one-off compressions, but if you need to process an entire folder of images, command-line scripts are faster. Here are two approaches I use regularly.

    Python script using Pillow — good for cross-platform use and when you need precise control:

    #!/usr/bin/env python3
    # Batch compress JPEG images using Pillow
    import os, sys
    from pathlib import Path
    from PIL import Image
    
    def compress_images(input_dir, output_dir, quality=80):
        Path(output_dir).mkdir(parents=True, exist_ok=True)
        total_saved = 0
    
        for filename in os.listdir(input_dir):
            if not filename.lower().endswith(('.jpg', '.jpeg', '.png')):
                continue
    
            input_path = os.path.join(input_dir, filename)
            output_path = os.path.join(output_dir, filename)
            original_size = os.path.getsize(input_path)
    
            img = Image.open(input_path)
            if img.mode == 'RGBA':
                img = img.convert('RGB')
            img.save(output_path, 'JPEG', quality=quality, optimize=True)
    
            new_size = os.path.getsize(output_path)
            saved = original_size - new_size
            total_saved += saved
            print(f"{filename}: {original_size//1024}KB -> {new_size//1024}KB")
    
        print(f"Total saved: {total_saved // 1024}KB")
    
    if __name__ == '__main__':
        compress_images(sys.argv[1], sys.argv[2],
                        int(sys.argv[3]) if len(sys.argv) > 3 else 80)

    Shell one-liner using ImageMagick — fastest option if you already have it installed:

    # Compress all JPEGs in current directory to 80% quality
    mkdir -p compressed
    for f in *.jpg *.jpeg *.png; do
      [ -f "$f" ] || continue
      convert "$f" -quality 80 -strip "compressed/$f"
      echo "$f: $(du -k "$f" | cut -f1)KB -> $(du -k "compressed/$f" | cut -f1)KB"
    done

    The -strip flag removes all metadata (EXIF, ICC profiles, etc.), which often saves an additional 20-50KB per image. If you want to keep color profiles, drop that flag.

    Comparing output sizes at different quality levels for a typical 4MB smartphone photo:

    • Quality 95 — 2.4MB (40% reduction)
    • Quality 80 — 820KB (80% reduction) ← the sweet spot
    • Quality 60 — 480KB (88% reduction)
    • Quality 40 — 320KB (92% reduction)
    • Quality 20 — 180KB (95% reduction, visible artifacts)

    The jump from 95 to 80 gives you the most bang for your buck — nearly halving the file size from the 95% baseline with no perceptible quality loss.

    Why I Built QuickShrink Instead of Using Existing Tools

    Every free image compressor I could find had the same problem: they upload your files to their server. TinyPNG, Compressor.io, iLoveIMG — they all send your images over the network, process them on their infrastructure, and send them back. That’s a privacy problem.

    Think about what you’re compressing. Product photos for your business. Screenshots of your desktop with open tabs. Family photos you want to email. Medical images for an insurance claim. None of that should pass through a third-party server just because you need to reduce file size.

    I started building QuickShrink as a weekend project after reading the privacy policies of five popular image compressors. Most said they delete uploaded files “within a few hours” — but that still means your photos sit on someone else’s server, unencrypted, for hours. Some didn’t specify a deletion timeline at all.

    The technical challenge was matching server-side compression quality using only browser APIs. Server-side tools like MozJPEG and libjpeg-turbo have decades of optimization behind them. The browser’s built-in JPEG encoder is good but not as sophisticated. After extensive testing, here’s what I found:

    • At 80% quality, the browser’s output is within 5-8% of MozJPEG for file size — close enough for practical use
    • At 60% quality, MozJPEG pulls ahead significantly — its perceptual optimization produces noticeably better results at high compression
    • Visual quality at 80% is indistinguishable between browser and server in blind tests I ran with 20 sample images

    For the 80% quality range that most people need, browser compression is good enough. And the privacy tradeoff makes it the clear winner. Your files never leave your device. There’s no server to get breached, no upload to be intercepted, no retention policy to worry about.

    QuickShrink loads in under 200ms, works offline after first visit, and is a single HTML file with zero dependencies. That’s the kind of tool the web was supposed to give us — fast, private, and free.

    Related Reading

    Get Weekly Security & DevOps Insights

    Join 500+ engineers getting actionable tutorials on Kubernetes security, homelab builds, and trading automation. No spam, unsubscribe anytime.

    Subscribe Free →

    Delivered every Tuesday. Read by engineers at Google, AWS, and startups.

    Frequently Asked Questions

    How can I compress images without losing quality?

    Modern image compression tools use smart algorithms that reduce file size by removing redundant data while preserving visual quality. Techniques like lossless PNG compression and optimized JPEG encoding can reduce file sizes by 50-80% with no visible difference to the human eye.

    What is the difference between lossy and lossless image compression?

    Lossless compression reduces file size without discarding any image data, so the decompressed image is pixel-identical to the original. Lossy compression achieves smaller files by permanently removing some visual information, though at high quality settings the difference is imperceptible.

    Does compressing images affect SEO?

    Yes, image compression significantly improves page load speed, which is a direct Google ranking factor. Faster-loading pages also reduce bounce rates and improve Core Web Vitals scores, giving compressed-image pages a measurable advantage in search results.

    What image format should I use for web: JPEG, PNG, or WebP?

    Use JPEG for photographs and complex images with many colors. Use PNG for graphics, logos, and images requiring transparency. WebP offers the best of both with 25-35% smaller files than JPEG at equivalent quality, and is now supported by all major browsers.

    References

  • NoiseLog: A Sound Meter App for Noisy Neighbors

    NoiseLog: A Sound Meter App for Noisy Neighbors

    When I complained about noise to my building manager, they asked for evidence. “It’s loud” wasn’t enough. They wanted dates, times, and decibel readings. So I built an app that gives you all three — and generates a report you can actually hand to someone.

    The Noise Complaint Trap

    📌 TL;DR: When I complained about noise to my building manager, they asked for evidence. “It’s loud” wasn’t enough. They wanted dates, times, and decibel readings.
    🎯 Quick Answer: NoiseLog is a sound meter app that records decibel readings with timestamps and automatically generates formatted noise complaint reports with date, time, duration, and dB levels—ready to submit to landlords or local authorities.

    Here’s how noise complaints typically go: you’re frustrated, you call the landlord or the city, they say “we’ll look into it,” nothing happens. Why? Because verbal complaints carry almost zero weight. Without documentation — specific dates, times, duration, and measured intensity — you’re just another person saying “it’s too loud.”

    Professional sound level meters cost $200+. An acoustic engineering assessment starts at $500. Most people just suffer in silence (pun intended) or escalate to a confrontation. Neither is a great outcome.

    Your Phone Microphone Is Good Enough

    Modern smartphone microphones are surprisingly capable. They won’t match a calibrated Type I sound meter, but for documenting noise levels in the 40–100 dB range — which covers everything from a loud TV to construction equipment — they’re more than adequate. Courts and housing authorities don’t require laboratory-grade measurements; they require consistent, timestamped records.

    NoiseLog uses your phone’s microphone to capture ambient sound, processes it through a standard A-weighted decibel calculation, and displays the result in real time.

    Three Screens, One Workflow

    Sound Meter. A live dB reading with a 60-second rolling chart. Color bands show you whether the noise level is safe (green), moderate (yellow), loud (orange), or harmful (red, 85+ dB). The day limit indicator shows if you’ve been exposed to noise above safe thresholds.

    Incidents. One tap logs the current noise level with a timestamp. “Tuesday, 11:47 PM, 78 dB.” Over a few days or weeks, you build a pattern. “This happens every weeknight between 11 PM and 2 AM, averaging 72 dB.” That’s not a complaint — that’s evidence.

    Report. Generate a formatted summary of all logged incidents. Dates, times, decibel readings, in a clean layout you can screenshot, print, or share. Hand it to your landlord, attach it to a noise ordinance complaint, or bring it to a mediator. Structured data is harder to ignore than “my neighbor is loud.”

    Beyond Neighbors

    Noise complaints are the obvious use case, but people have found others:

    • Workplace safety — OSHA requires hearing protection above 85 dB. NoiseLog helps document whether your factory floor or machine shop meets standards
    • Event planning — check if your venue stays within local noise ordinance limits during rehearsals
    • Parenting — curiosity check: how loud is that toy your kid loves? (Often shockingly loud)
    • Musicians — monitor rehearsal room levels to protect hearing
    • Real estate — measure ambient noise levels in a potential apartment before signing a lease

    Free vs. Pro

    The free version is fully functional — real-time metering, incident logging, reports. The only friction is a short video ad when you start a measurement session. The Pro subscription ($1.99/month) removes ads, unlocks unlimited incident storage, and enables detailed CSV export for anyone who needs to submit formal documentation.

    How Decibel Measurement Works in the Browser

    NoiseLog’s core measurement engine runs entirely in the browser using the Web Audio API. No server, no uploads, no third-party SDKs touching your microphone data. Here’s the signal chain that turns raw microphone input into a decibel reading:

    // Web Audio API: microphone to dB meter
    async function initMeter() {
      const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
      const audioCtx = new AudioContext();
      const source = audioCtx.createMediaStreamSource(stream);
      const analyser = audioCtx.createAnalyser();
      analyser.fftSize = 2048;
      source.connect(analyser);
    
      const buffer = new Float32Array(analyser.fftSize);
    
      function calculateDB() {
        analyser.getFloatTimeDomainData(buffer);
    
        // RMS (Root Mean Square) calculation
        let sumSquares = 0;
        for (let i = 0; i < buffer.length; i++) {
          sumSquares += buffer[i] * buffer[i];
        }
        const rms = Math.sqrt(sumSquares / buffer.length);
    
        // Convert RMS to decibels
        const db = 20 * Math.log10(Math.max(rms, 1e-10));
    
        // A-weighting approximation for human-perceived loudness
        // Boosts mid-frequencies (1-6 kHz) where human hearing is most sensitive
        const aWeighted = applyAWeighting(db, analyser, buffer);
    
        return {
          raw: db.toFixed(1),
          aWeighted: aWeighted.toFixed(1),
          rms: rms
        };
      }
    
      // A-weighting filter approximation
      function applyAWeighting(dbValue, analyser, timeDomain) {
        const freqData = new Float32Array(analyser.frequencyBinCount);
        analyser.getFloatFrequencyData(freqData);
        const nyquist = audioCtx.sampleRate / 2;
        let weightedSum = 0;
        let totalEnergy = 0;
    
        for (let i = 0; i < freqData.length; i++) {
          const freq = (i / freqData.length) * nyquist;
          const energy = Math.pow(10, freqData[i] / 10);
          // Simplified A-weighting curve
          const weight = aWeightCoeff(freq);
          weightedSum += energy * weight;
          totalEnergy += energy;
        }
    
        const correction = totalEnergy > 0
          ? 10 * Math.log10(weightedSum / totalEnergy)
          : 0;
        return dbValue + correction;
      }
    
      function aWeightCoeff(f) {
        // IEC 61672 A-weighting approximation
        const f2 = f * f;
        const num = 1.4866e8 * f2 * f2;
        const den = (f2 + 424.36) * Math.sqrt((f2 + 11599.29) *
          (f2 + 544496.41)) * (f2 + 148693636);
        const ra = num / den;
        return ra * ra * 1.9997;
      }
    
      // Update loop at 30fps
      setInterval(() => {
        const reading = calculateDB();
        updateDisplay(reading);
        logToHistory(reading);
      }, 33);
    }

    The key steps: getUserMedia grabs the microphone stream, the AnalyserNode provides raw audio samples, and then it’s just math. RMS gives you the average energy in the signal, and the log10 conversion maps it to the decibel scale humans are used to. The A-weighting filter is critical — without it, you’d measure low-frequency rumble (like HVAC systems) at the same level as a conversation, which isn’t how humans perceive loudness.

    Calibration and Accuracy

    Let’s be honest: your phone microphone is not a professional SPL meter. It has a limited dynamic range, nonlinear frequency response, and automatic gain control that most browsers can’t fully disable. But here’s the thing — for the purpose of documenting noise complaints, it doesn’t need to be perfect. It needs to be consistent and timestamped.

    I calibrated NoiseLog against a UNI-T UT353 mini sound level meter (about $30 on most electronics sites) in my apartment. The process was straightforward: play pink noise at a known level, read both meters simultaneously, and calculate the offset. Here’s what I found across different volume levels:

    • 40–50 dB (quiet room): Phone read 2–4 dB lower than the SPL meter. Microphone sensitivity drops off at very low levels.
    • 50–70 dB (conversation to loud TV): Within ±2 dB. This is the sweet spot where phone microphones are most accurate.
    • 70–85 dB (loud music, vacuum cleaner): Within ±3 dB. Still reliable enough for evidence.
    • 85+ dB (harmful levels): Phone microphone begins to clip. Readings plateau around 90–95 dB regardless of actual level. If you’re measuring construction noise, the phone underreports.

    For the 50–85 dB range — which covers virtually every noise complaint scenario (loud neighbors, barking dogs, late-night parties) — a typical smartphone accuracy of ±3 dB is more than adequate. Housing authorities and mediators aren’t looking for laboratory precision. They’re looking for a pattern: repeated measurements at specific times showing levels above the local ordinance threshold.

    NoiseLog includes a calibration offset setting so you can adjust readings if you do have access to a reference meter. The code is simple:

    // Calibration offset stored in user settings
    const CALIBRATION_KEY = 'noiselog_calibration';
    
    function getCalibratedDB(rawDB) {
      const offset = parseFloat(
        localStorage.getItem(CALIBRATION_KEY) || '0'
      );
      return rawDB + offset;
    }
    
    // User measures a known source, enters the reference value
    function calibrate(measuredDB, referenceDB) {
      const offset = referenceDB - measuredDB;
      localStorage.setItem(CALIBRATION_KEY, offset.toString());
      console.log(
        'Calibration offset set to ' + offset.toFixed(1) + ' dB'
      );
    }

    One calibration session with a known reference point improves accuracy significantly. Even without calibration, the relative measurements are consistent — if NoiseLog says tonight is 8 dB louder than last night, that’s a real difference regardless of the absolute number.

    From Side Project to Noise Complaint Evidence

    My upstairs neighbor’s subwoofer was the reason this app exists. Every night between 11 PM and 1 AM, bass-heavy music would vibrate through the ceiling. I asked politely. I left a note. I called the building manager, who asked: “Can you document the dates and times? Do you have any readings?”

    I didn’t. So I built NoiseLog over a weekend, and for the next three weeks, I logged every incident. Tap the button when it starts, tap again when it stops. The app records the timestamp, peak dB, average dB, and duration automatically.

    After 21 days, I generated a report showing 18 incidents, all between 10:45 PM and 1:30 AM, averaging 68 dB with peaks hitting 76 dB. For context, most local noise ordinances set the nighttime limit at 50–55 dB for residential areas. My data showed consistent, documented violations — not a one-time complaint.

    The building manager took it seriously because structured data is harder to dismiss than “my neighbor is loud.” They issued a formal warning, and the late-night subwoofer sessions stopped within a week. No confrontation, no escalation, just data.

    What I learned about noise ordinances along the way: they vary wildly by municipality, but almost all of them distinguish between daytime and nighttime limits, and most set the residential nighttime threshold between 45 and 55 dB measured at the property line. Some jurisdictions use C-weighting (which captures bass better than A-weighting), and some require measurements from inside the affected unit. Check your local code before relying on any specific number.

    The most important lesson: time-series logging matters more than peak readings. A single 80 dB spike could be a dropped pan — it means nothing. But 18 data points showing sustained 65+ dB levels between 11 PM and 1 AM over three weeks? That’s a pattern, and patterns are what enforcement agencies act on. That’s why NoiseLog emphasizes incident logging over instantaneous measurements — the real value is in the history, not the number on screen right now.

    More From the App Factory

    NoiseLog is part of our collection of free browser and mobile tools built to solve real problems. If you like the idea of gamified productivity, check out FocusForge — a Pomodoro timer with XP, levels, and daily streaks. We also wrote about why the Pomodoro Technique actually works when your timer has streaks.

    Get It

    👉 NoiseLog on Google Play (Android)

    If you’re dealing with noise issues, start logging today. A week of data is worth more than a year of verbal complaints.

    Get Weekly Security & DevOps Insights

    Join 500+ engineers getting actionable tutorials on Kubernetes security, homelab builds, and trading automation. No spam, unsubscribe anytime.

    Subscribe Free →

    Delivered every Tuesday. Read by engineers at Google, AWS, and startups.

    Frequently Asked Questions

    What is NoiseLog and how does it measure sound?

    NoiseLog is a sound meter app that uses your device’s microphone to measure ambient noise levels in decibels. It provides real-time readings and logs noise events over time, creating a documented record useful for noise complaints or monitoring environmental sound levels.

    Can I use a sound meter app as evidence for noise complaints?

    A sound meter app provides supporting documentation for noise complaints by logging decibel levels with timestamps. While phone-based measurements may not be legally calibrated, a consistent log of excessive noise readings strengthens your case when reporting to landlords or local authorities.

    How loud is too loud for residential noise?

    Most local ordinances set residential noise limits between 55-65 decibels during the day and 45-55 decibels at night. Sustained noise above 70 decibels can cause hearing damage over time, and sounds above 85 decibels are considered hazardous with prolonged exposure.

    How accurate are phone-based sound meter apps?

    Modern smartphones can measure noise levels within 2-5 decibels of professional equipment for typical indoor ranges (40-90 dB). Accuracy varies by device and microphone quality, but for documenting noise patterns and relative changes, phone-based meters are sufficiently reliable.

    References

  • FocusForge: How Gamification Tricked Me Into Deep Focus

    FocusForge: How Gamification Tricked Me Into Deep Focus

    I’ve downloaded at least ten Pomodoro timer apps over the years. I used each one for about three days before forgetting it existed. Then I built FocusForge, added XP and levels, and accidentally created a focus habit I can’t stop.

    The Pomodoro Problem

    📌 TL;DR: I’ve downloaded at least ten Pomodoro timer apps over the years. I used each one for about three days before forgetting it existed. Then I built FocusForge, added XP and levels, and accidentally created a focus habit I can’t stop.
    🎯 Quick Answer: FocusForge is a gamified Pomodoro timer that awards XP and levels for completed focus sessions. The RPG-style progression system creates lasting deep focus habits by turning productivity into a game with visible streaks and milestones.

    The Pomodoro Technique is elegant: work for 25 minutes, take a 5-minute break, repeat. After four cycles, take a longer break. It’s been around since the late 1980s and it works — when you actually do it.

    The problem isn’t the technique. It’s the timer. A countdown clock gives you no reason to come back tomorrow. There’s no cost to skipping a day, no reward for consistency, no progression. It’s like a gym with no mirror — you can’t see if you’re making progress, so you stop going.

    XP, Levels, and the Streak That Won’t Let You Quit

    FocusForge adds three things to the standard Pomodoro timer:

    1. Experience Points. Every completed focus session earns XP. A Quick 25 gives you 25 XP. A Deep 45 gives you 50. A Marathon 60 gives you 75. It’s a simple formula, but watching a number go up is an irrationally powerful motivator.

    2. Levels. You start as a Rookie. At 100 XP, you become an Apprentice. Then Journeyman, Expert, Master, Legend, and finally Immortal. Each level has its own color and badge. It’s completely meaningless — and completely addictive. I’m a Master. I refuse to let it drop.

    3. Daily Streaks. Complete at least one focus session per day and your streak increments. Miss a day and it resets to zero. This is the mechanic that Duolingo used to build a $12 billion company. It works because loss aversion is stronger than the desire for gain — you don’t want to break a 30-day streak more than you want to skip a 25-minute session.

    What It Looks Like

    The main screen is a large countdown timer with three mode buttons: Quick 25, Deep 45, Marathon 60. Below the timer, a motivational quote rotates — Stoic philosophy from Marcus Aurelius and Seneca, productivity wisdom from Cal Newport and James Clear. It sounds cheesy. It works at 6 AM when you don’t want to start.

    The Stats tab shows your level, XP progress bar, total sessions completed, and streak calendar. The Settings tab lets you upgrade to Pro ($1.99 one-time) to remove ads and unlock custom durations.

    Why Not Just Use the Phone’s Built-in Timer?

    You could. But a phone timer doesn’t track your history, doesn’t reward consistency, and doesn’t create a feedback loop. FocusForge turns “I should focus for 25 minutes” into “I need 15 more XP to reach Legend.” The outcome is the same — deep work — but the motivation mechanism is completely different.

    The Code Behind the Focus Loop

    FocusForge’s timer isn’t a simple setInterval countdown. It’s a state machine with four states — idle, running, break, and back to running — and each transition triggers side effects like XP calculation and sound notifications. Here’s the core logic I use in the web prototype:

    // Timer State Machine
    const STATES = { IDLE: 'idle', RUNNING: 'running', BREAK: 'break' };
    let state = STATES.IDLE;
    let streak = 0;
    let totalXP = 0;
    let level = 1;
    
    // XP calculation with streak multiplier and difficulty bonus
    function calculateXP(duration) {
      const baseXP = duration; // 1 XP per minute
      const streakMultiplier = 1 + Math.min(streak, 30) * 0.05; // +5% per streak day, cap 30
      const difficultyBonus = duration >= 60 ? 1.5 : duration >= 45 ? 1.2 : 1.0;
      return Math.round(baseXP * streakMultiplier * difficultyBonus);
    }
    
    // Level thresholds using exponential curve
    function xpForLevel(lvl) {
      return Math.floor(10 * Math.pow(1.5, lvl));
    }
    
    // Check and apply level-ups
    function applyXP(earned) {
      totalXP += earned;
      while (totalXP >= xpForLevel(level)) {
        totalXP -= xpForLevel(level);
        level++;
        playSound(880, 0.3); // celebration tone
      }
    }
    
    // Sound notification using Web Audio API
    function playSound(freq, duration) {
      const ctx = new (window.AudioContext || window.webkitAudioContext)();
      const osc = ctx.createOscillator();
      const gain = ctx.createGain();
      osc.connect(gain);
      gain.connect(ctx.destination);
      osc.frequency.value = freq;
      gain.gain.setValueAtTime(0.3, ctx.currentTime);
      gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + duration);
      osc.start(ctx.currentTime);
      osc.stop(ctx.currentTime + duration);
    }
    
    // State transitions
    function transition(action) {
      if (state === STATES.IDLE && action === 'start') {
        state = STATES.RUNNING;
        startCountdown();
      } else if (state === STATES.RUNNING && action === 'complete') {
        const xp = calculateXP(selectedDuration);
        applyXP(xp);
        playSound(660, 0.5); // completion chime
        state = STATES.BREAK;
        startBreak();
      } else if (state === STATES.BREAK && action === 'resume') {
        state = STATES.RUNNING;
        startCountdown();
      }
    }

    The key insight is that the state machine enforces the Pomodoro rhythm automatically. You can’t skip the break, and you can’t get XP without completing a full cycle. The streakMultiplier caps at 30 days because I found that beyond that point, the habit is self-sustaining — you don’t need the extra incentive.

    Designing the Reward System

    My first version used linear XP — every session gave you the same flat reward. It worked for about two weeks and then engagement fell off a cliff. The problem is predictability: when you know exactly what you’ll earn, the dopamine loop breaks. There’s no surprise, no variable reward.

    The fix was an exponential level curve. Early levels come fast — you hit Level 3 in your first day. But each subsequent level requires roughly 50% more XP than the last. This creates the “near miss” psychology that keeps slot machines profitable and Duolingo sticky: you’re always tantalizingly close to the next milestone.

    I tested three different XP curves with a small group of friends who agreed to use the app daily for a month:

    • Linear (flat 25 XP per session): Average engagement dropped to 40% by week 3. People said it felt “pointless” after hitting Level 5.
    • Logarithmic (diminishing returns): Even worse — people felt punished for playing more. Engagement cratered by week 2.
    • Exponential with streak bonus: 78% of testers were still using the app daily at day 30. The streak multiplier made each consecutive day feel more valuable.

    The other trick is showing progress toward the next level constantly. The XP bar is always visible on the main screen, and it fills in real time as you complete sessions. After a session, a popup shows exactly how much XP you earned and how close you are to leveling up. If you’re within one session of the next level, a subtle glow animation appears on the progress bar. It’s shameless psychological manipulation — and I’m the one being manipulated, happily.

    All game state persists in localStorage with a simple schema:

    // localStorage schema for game state persistence
    const STORAGE_KEY = 'focusforge_state';
    
    function saveState() {
      const state = {
        totalXP: totalXP,
        level: level,
        streak: streak,
        lastSessionDate: new Date().toISOString().split('T')[0],
        sessionsCompleted: sessionsCompleted,
        history: sessionHistory.slice(-90) // keep 90 days
      };
      localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
    }
    
    function loadState() {
      const raw = localStorage.getItem(STORAGE_KEY);
      if (!raw) return defaultState();
      const saved = JSON.parse(raw);
      // Check streak continuity
      const today = new Date().toISOString().split('T')[0];
      const yesterday = new Date(Date.now() - 86400000)
        .toISOString().split('T')[0];
      if (saved.lastSessionDate !== today &&
          saved.lastSessionDate !== yesterday) {
        saved.streak = 0; // streak broken
      }
      return saved;
    }

    I deliberately limited the history to 90 days to avoid localStorage bloat, and the streak check runs on load — if your last session was more than one calendar day ago, your streak resets. Simple, brutal, effective.

    What Changed After 90 Days

    Before FocusForge, my deep work average was about 2 hours per day. After 90 days of gamified Pomodoros, it jumped to 4.5 hours. That’s not a guess — I have the session logs to prove it.

    Here’s what the data actually showed across 90 days of daily use:

    • Days 1–14: Average 3.2 sessions/day (mostly Quick 25). I was still getting used to the rhythm. Total focus time: ~80 min/day.
    • Days 15–30: Shifted to Deep 45 as my primary mode. Average 4.1 sessions/day. The streak counter was at 22 and I refused to break it. Focus time: ~185 min/day.
    • Days 31–60: Mixed Deep 45 and Marathon 60. Average 4.8 sessions/day. Hit Level 12 (Legend). Focus time: ~250 min/day.
    • Days 61–90: Settled into a routine of two Marathon 60s in the morning and two Deep 45s after lunch. Average 4.3 sessions/day. Focus time: ~270 min/day.

    The features that actually drove the improvement weren’t the ones I expected. The XP system was fun but not transformative. The streak counter was the killer feature — it created a daily commitment device that worked through loss aversion. Once you hit a 30-day streak, the psychological cost of breaking it exceeds the effort of doing one more 25-minute session.

    The features that were just fun to build but didn’t move the needle: custom themes, the quote rotation system, and ironically, the level badges. Nobody cares about a badge after the first week. What they care about is a number going up and a streak not breaking.

    If I were rebuilding FocusForge from scratch, I’d double down on the streak mechanic and add a “freeze” token — one per week, usable to preserve your streak on a sick day. That single feature was the most-requested in user feedback, and I think it would push 90-day retention even higher.

    Get It

    👉 FocusForge on Google Play (Android)

    Free with occasional ads. $1.99 to remove them permanently. No subscription, no account, no data collection beyond what AdMob does in the free version.

    Related Developer Tools

    Get Weekly Security & DevOps Insights

    Join 500+ engineers getting actionable tutorials on Kubernetes security, homelab builds, and trading automation. No spam, unsubscribe anytime.

    Subscribe Free →

    Delivered every Tuesday. Read by engineers at Google, AWS, and startups.

    Frequently Asked Questions

    What is FocusForge and how does gamification help with focus?

    FocusForge applies game design principles like points, streaks, and achievements to deep work sessions. By turning focused work into a game with visible progress, it uses the same dopamine-driven reward loops that make video games engaging to help you maintain concentration.

    How does gamification improve productivity?

    Gamification breaks large tasks into smaller, rewarding milestones that trigger dopamine releases when completed. This creates a positive feedback loop where the act of focusing itself becomes rewarding, making it easier to start deep work sessions and sustain them longer.

    What is the Pomodoro technique and how does FocusForge use it?

    The Pomodoro technique involves working in focused 25-minute intervals followed by 5-minute breaks. FocusForge enhances this by adding experience points for completed sessions, streak bonuses for consecutive focus blocks, and level-ups that make the technique more engaging and sustainable.

    Can gamified productivity apps actually replace willpower?

    Gamification does not replace willpower but reduces your dependence on it. By creating external reward structures and making progress visible, these apps lower the activation energy needed to start working and provide motivation that persists even when intrinsic motivation is low.

    References

  • TypeFast: Snippet Manager Without Electron Bloat

    TypeFast: Snippet Manager Without Electron Bloat

    I needed a place to store code snippets, email templates, and frequently pasted text blocks. Everything I found was either a full IDE extension, a note-taking app in disguise, or yet another Electron app eating 200MB of RAM. So I built TypeFast — a snippet manager that runs in a browser tab.

    The Snippet Graveyard Problem

    📌 TL;DR: I needed a place to store code snippets, email templates, and frequently pasted text blocks. Everything I found was either a full IDE extension, a note-taking app in disguise, or yet another Electron app eating 200MB of RAM. So I built TypeFast — a snippet manager that runs in a browser tab.
    🎯 Quick Answer: TypeFast is a lightweight, browser-based snippet manager for storing and instantly pasting code snippets and frequently used text. Unlike Electron-based alternatives, it uses zero system resources when idle and works entirely in your browser.

    Every developer has one. A folder called snippets or useful-stuff sitting somewhere in their home directory. A Notion page titled “Code Templates” that hasn’t been updated since 2023. Three GitHub Gists they can’t find because they never gave them proper names. Slack messages to themselves that got buried under 400 notifications.

    The common thread: the tool was never designed for quick retrieval. Notion is a document editor. Gists are for sharing, not searching. Slack is for messaging. Using them as snippet managers is like using a spreadsheet as a to-do list — it technically works, but the friction kills you.

    What TypeFast Actually Does

    TypeFast has exactly four features:

    1. Add a snippet — give it a title, a category, paste the content
    2. Find a snippet — type in the search bar, or filter by category tab
    3. Copy a snippet — one click, it’s on your clipboard, a “✅ Copied!” confirmation appears
    4. Edit or delete — because snippets evolve

    That’s it. No folders, no tags cloud, no sharing, no collaboration, no AI suggestions. Just a fast, searchable list with a copy button.

    The Technical Non-Architecture

    TypeFast is a single HTML file. No React, no Vue, no build step. The entire application — HTML, CSS, and JavaScript — weighs about 10KB. It stores data in localStorage, which means:

    • No server, no database, no API calls
    • Data persists across browser sessions
    • No account, no sync, no privacy concerns
    • Works offline (it’s also a PWA)

    The trade-off is obvious: your snippets live only in that browser, on that device. If you clear your browser data, they’re gone. For most people, this is fine — snippets aren’t precious documents. But if you want durability, export them (coming in a future update) or just keep the tab pinned.

    How the App Architecture Works

    Most web apps start with npx create-react-app and immediately inherit thousands of dependencies, a build pipeline, and a node_modules folder heavier than the app itself. TypeFast takes the opposite approach: vanilla JavaScript with zero dependencies, organized around an event-driven pattern that would look familiar to anyone who wrote web apps before the framework era.

    The DOM manipulation strategy is intentionally boring. Instead of a virtual DOM or reactive bindings, TypeFast uses document.createElement() for building snippet cards and direct property assignment for updates. When the snippet list changes, the app clears the container and rebuilds it. For a list of a few hundred items, this is imperceptibly fast — the browser’s layout engine handles it in under a frame.

    State management is a plain JavaScript object that gets serialized to localStorage on every mutation. Here’s the actual core data model:

    // Core data model — everything TypeFast needs
    const AppState = {
      snippets: [],
      categories: ['General'],
      activeCategory: 'All',
      searchQuery: ''
    };
    
    // Persist to localStorage on every mutation
    function saveState() {
      localStorage.setItem('typefast_data', JSON.stringify({
        snippets: AppState.snippets,
        categories: AppState.categories
      }));
    }
    
    // Hydrate on startup
    function loadState() {
      const saved = localStorage.getItem('typefast_data');
      if (saved) {
        const data = JSON.parse(saved);
        AppState.snippets = data.snippets || [];
        AppState.categories = data.categories || ['General'];
      }
    }

    That’s the entire state layer. No Redux store with actions and reducers. No Vuex modules. No React context providers wrapping five levels deep. A single object, two functions, and localStorage as the persistence layer. When something changes, call saveState(). When the page loads, call loadState(). The simplicity is the feature — there are zero state synchronization bugs because there’s only one source of truth.

    Code Walkthrough: The Snippet Engine

    The search and filter system is the heart of TypeFast. Every keystroke in the search bar triggers a filter pass across all snippets. Here’s the actual implementation:

    function filterSnippets() {
      const query = AppState.searchQuery.toLowerCase();
      const category = AppState.activeCategory;
    
      return AppState.snippets.filter(snippet => {
        const matchesCategory = category === 'All' || snippet.category === category;
        const matchesSearch = !query ||
          snippet.title.toLowerCase().includes(query) ||
          snippet.content.toLowerCase().includes(query) ||
          snippet.category.toLowerCase().includes(query);
        return matchesCategory && matchesSearch;
      });
    }

    It searches across title, content, and category simultaneously. No fancy indexing, no search library — just Array.filter() and String.includes(). For collections under a few thousand snippets, this brute-force approach is faster than the overhead of maintaining a search index.

    The more interesting piece is the template variable system. TypeFast lets you embed dynamic placeholders in your snippets that get expanded at copy time. Type {{date}} in a snippet and it becomes today’s date when you copy it:

    // Template variables — type {{date}} and get today's date
    const TEMPLATE_VARS = {
      '{{date}}': () => new Date().toISOString().split('T')[0],
      '{{time}}': () => new Date().toLocaleTimeString(),
      '{{timestamp}}': () => Date.now().toString(),
      '{{uuid}}': () => crypto.randomUUID(),
      '{{clipboard}}': async () => {
        try {
          return await navigator.clipboard.readText();
        } catch {
          return '{{clipboard}}'; // fallback if permission denied
        }
      }
    };
    
    async function expandTemplateVars(text) {
      let result = text;
      for (const [pattern, resolver] of Object.entries(TEMPLATE_VARS)) {
        if (result.includes(pattern)) {
          const value = await resolver();
          result = result.replaceAll(pattern, value);
        }
      }
      return result;
    }

    The {{clipboard}} variable is particularly useful: it reads the current clipboard content and injects it into the snippet. So you can create a template like git commit -m "{{clipboard}}", copy some text, then copy the snippet, and the clipboard content gets wrapped in the git command. The copy-to-clipboard function processes all template variables before writing to the clipboard, so the user always gets the fully expanded version.

    The one-click copy includes visual feedback so you know something happened:

    async function copySnippet(snippetId) {
      const snippet = AppState.snippets.find(s => s.id === snippetId);
      if (!snippet) return;
    
      const expanded = await expandTemplateVars(snippet.content);
    
      await navigator.clipboard.writeText(expanded);
    
      // Visual feedback
      const btn = document.querySelector(`[data-copy="${snippetId}"]`);
      const original = btn.textContent;
      btn.textContent = '✅ Copied!';
      btn.classList.add('copied');
      setTimeout(() => {
        btn.textContent = original;
        btn.classList.remove('copied');
      }, 1500);
    }

    The 1500ms timeout for the feedback animation is deliberate — long enough to register visually, short enough that it resets before you need to copy another snippet. The copied CSS class triggers a brief green highlight animation on the button. Small detail, but it’s the difference between “did that work?” and “done, next.”

    Performance: TypeFast vs Electron Alternatives

    The entire reason TypeFast exists is that snippet managers shouldn’t need Electron. Here’s how it compares against typical alternatives:

    Metric TypeFast Electron Snippet Manager VS Code Extension
    RAM Usage ~15MB (browser tab) 180–250MB ~50MB (VS Code overhead)
    Disk Space 12KB 150–300MB 2–5MB + VS Code
    Startup Time <100ms 2–4 seconds 1–2 seconds (cold)
    Works Offline ✔ (PWA)
    Search Speed Instant (<1ms for 1000 snippets) ~50ms ~100ms
    Dependencies Zero Node.js, Chromium VS Code

    I tested this by loading 1,000 snippets into TypeFast and measuring search latency with performance.now(). The filter function runs in under 1ms because it’s just Array.filter() on a JavaScript array that’s already in memory. No database queries, no IPC calls, no virtual DOM diffing. The bottleneck isn’t the search — it’s the DOM rebuild, which still finishes in under 16ms (one frame at 60fps).

    RAM usage is the most dramatic difference. An Electron app bundles an entire Chromium instance, which starts at about 80MB before your app code even loads. TypeFast shares the Chromium instance you already have open — your browser. The marginal cost of one more tab is roughly 15MB, and that includes the full snippet dataset.

    The Build and Deploy Pipeline (There Isn’t One)

    TypeFast has no build step. No webpack config, no Vite setup, no Babel transpilation. The source code is the production artifact. This is a deliberate choice, not a limitation — when your entire app is a single HTML file, build tooling adds complexity without adding value.

    The app is served by nginx running on my homelab TrueNAS box. Here’s the full server config:

    server {
        listen 443 ssl http2;
        server_name typefast.orthogonal.info;
    
        root /usr/share/nginx/typefast;
        index index.html;
    
        # Cache everything aggressively — it's one file
        location / {
            add_header Cache-Control "public, max-age=86400";
            try_files $uri $uri/ /index.html;
        }
    
        # Security headers
        add_header X-Content-Type-Options nosniff;
        add_header X-Frame-Options DENY;
        add_header Content-Security-Policy "default-src 'self' 'unsafe-inline'";
    }

    The entire deploy pipeline is one line:

    scp index.html [email protected]:/mnt/data/nginx/typefast/

    No CI/CD, no Docker build, no artifact registry. When your app is a single file, you don’t need infrastructure. The security headers in the nginx config are honestly overkill for a static HTML file, but old habits die hard when you spend your day job doing security engineering. The Content-Security-Policy header restricts the page to only loading resources from itself, which means even if someone injected a script tag via a snippet, it wouldn’t be able to phone home. Defense in depth, even for a 12KB app.

    Use Cases I Didn’t Expect

    • A support team member saves 15 canned responses, copies the right one in under 2 seconds
    • A writer keeps character descriptions and plot points for quick reference
    • A sysadmin stores SSH commands, config blocks, and one-liners
    • A recruiter saves personalized outreach templates by role type

    Try It

    👉 typefast.orthogonal.info

    It comes pre-loaded with two example snippets. Delete them, add your own, and see if it sticks. If you’re still using a text file for snippets in a week, I’ll be surprised.

    Get Weekly Security & DevOps Insights

    Join 500+ engineers getting actionable tutorials on Kubernetes security, homelab builds, and trading automation. No spam, unsubscribe anytime.

    Subscribe Free →

    Delivered every Tuesday. Read by engineers at Google, AWS, and startups.

    Frequently Asked Questions

    What is TypeFast: Snippet Manager Without Electron Bloat about?

    I needed a place to store code snippets, email templates, and frequently pasted text blocks. Everything I found was either a full IDE extension, a note-taking app in disguise, or yet another Electron

    Related Developer Tools

    Who should read this article about TypeFast: Snippet Manager Without Electron Bloat?

    Anyone interested in learning about TypeFast: Snippet Manager Without Electron Bloat and related topics will find this article useful.

    What are the key takeaways from TypeFast: Snippet Manager Without Electron Bloat?

    So I built TypeFast — a snippet manager that runs in a browser tab. The Snippet Graveyard Problem Every developer has one. A folder called snippets or useful-stuff sitting somewhere in their home dire

  • PixelStrip: Stop Photos Broadcasting Your Location

    PixelStrip: Stop Photos Broadcasting Your Location

    Every photo taken on a smartphone embeds invisible metadata — including GPS coordinates accurate to within a few meters. PixelStrip strips it all out before you share. Zero upload, zero tracking, zero excuses.

    A Quick Experiment

    📌 TL;DR: Every photo taken on a smartphone embeds invisible metadata — including GPS coordinates accurate to within a few meters. PixelStrip strips it all out before you share. Zero upload, zero tracking, zero excuses.
    🎯 Quick Answer: PixelStrip removes all EXIF metadata from photos—including GPS coordinates, camera info, and timestamps—entirely in your browser. Your photos never touch a server, making it the safest way to strip location data before sharing online.

    Pick any photo from your camera roll. Right-click it on your computer, open Properties (Windows) or Get Info (Mac), and look for the GPS fields. If location services were on when you took the photo — and they almost certainly were — you’ll see latitude and longitude coordinates that pinpoint exactly where you were standing.

    Now imagine you posted that photo on a forum, sold something with it on Craigslist, or sent it in a group chat that got forwarded around. Anyone who saves the image can extract those coordinates and drop them into Google Maps. They’ll see your home, your office, your kid’s school.

    This isn’t theoretical. It’s happened to journalists, activists, and abuse victims. And it’s happening to you right now, every time you share an unstripped photo.

    What’s Hiding in Your Photos

    The EXIF (Exchangeable Image File Format) standard was designed in the 1990s for digital cameras. It stores useful technical data — aperture, shutter speed, focal length. But smartphones added fields that were never meant to be shared publicly:

    • GPS coordinates — latitude, longitude, altitude, and sometimes direction
    • Device fingerprint — phone make, model, OS version, unique camera serial number
    • Timestamps — date and time of capture, modification history
    • Thumbnail images — a smaller version of the original, sometimes containing content you cropped out
    • Software chain — every app that touched the image

    Social media platforms like Facebook, Instagram, and Twitter strip EXIF data on upload — but email, messaging apps, forums, file-sharing services, and most CMS platforms do not.

    How PixelStrip Works

    PixelStrip parses the JPEG binary structure directly in your browser using JavaScript. It identifies EXIF markers (APP1 segments), IFD entries, and GPS sub-IFDs, then displays what it found with clear warning labels.

    When you click “Strip All Metadata,” the image is re-rendered through an HTML5 Canvas — which by design does not preserve EXIF data — and exported as a clean JPEG. The visual content is identical; the metadata is gone.

    No server involved. No upload. The file never leaves your browser tab.

    The Technical Architecture

    PixelStrip doesn’t just run the image through a Canvas and re-export it (though that would strip EXIF data too). Instead, it parses the JPEG binary structure directly, which lets it show you exactly what metadata is present before removing anything. Here’s how the pipeline works at the code level.

    A JPEG file is a sequence of binary segments, each starting with a two-byte marker. The image data lives in the SOS (Start of Scan) segment. Metadata lives in APP segments — specifically APP1 (0xFFE1), which contains the EXIF data. PixelStrip reads the file as an ArrayBuffer and walks through these segments to find and parse the EXIF block.

    Inside the EXIF APP1 segment, data is organized as IFD (Image File Directory) entries — essentially key-value pairs. IFD0 contains basic image info (camera make, software). The GPS sub-IFD (linked from IFD0) contains the location tags: GPSLatitude, GPSLongitude, GPSAltitude, GPSTimeStamp, and others. Each IFD entry has a tag ID, a data type, a count, and either the value itself or an offset to where the value is stored.

    Here’s a simplified example of reading EXIF data from a File object in the browser:

    async function readExif(file) {
      const buffer = await file.arrayBuffer();
      const view = new DataView(buffer);
    
      // Verify JPEG magic bytes: 0xFFD8
      if (view.getUint16(0) !== 0xFFD8) {
        throw new Error('Not a JPEG file');
      }
    
      // Walk segments looking for APP1 (EXIF)
      let offset = 2;
      while (offset < view.byteLength) {
        const marker = view.getUint16(offset);
        if (marker === 0xFFE1) {
          // Found APP1 segment - parse EXIF
          const length = view.getUint16(offset + 2);
          const exifData = buffer.slice(offset + 4, offset + 2 + length);
          return parseExifIFD(new DataView(exifData));
        }
        // Skip to next segment
        const segLength = view.getUint16(offset + 2);
        offset += 2 + segLength;
      }
      return null; // No EXIF found
    }

    The parseExifIFD function then walks the IFD entries, reading tag IDs and extracting values. GPS coordinates are stored as rational numbers (numerator/denominator pairs) in degrees, minutes, and seconds format. Converting to decimal coordinates requires: degrees + minutes/60 + seconds/3600.

    To strip the metadata, PixelStrip identifies the APP1 segment boundaries and reconstructs the JPEG without it — copying the SOI marker, skipping APP1, and then copying all remaining segments (DQT, SOF, DHT, SOS, and the compressed image data) intact. The visual image data is never decoded or re-encoded, so there’s zero quality loss.

    Why Client-Side Processing Matters

    The irony of most metadata removal tools is hard to overstate: they ask you to upload your photo to their server to remove the data that reveals your location. You’re sending your GPS coordinates to a third party in order to prevent third parties from seeing your GPS coordinates.

    PixelStrip’s architecture eliminates this entirely. The processing pipeline runs 100% in your browser:

    • File API — reads the image from your local disk into JavaScript memory
    • ArrayBuffer — provides raw binary access to parse JPEG segments
    • Parse — walks the binary structure to find and display EXIF data
    • Strip — reconstructs the JPEG without the APP1 (EXIF) segment
    • Blob — wraps the clean binary data into a downloadable file
    • Download — triggers a browser download with no network request

    Open your browser’s Network tab while using PixelStrip. You’ll see zero outgoing requests after the initial page load. The JavaScript, CSS, and HTML are served once, cached by your browser, and everything after that happens locally. It even works in airplane mode.

    Compare this to server-side alternatives like several popular EXIF removal websites. Their typical flow is: upload your image (sending your GPS data over the network), wait for server processing, download the stripped version. Even with HTTPS, your photo sits on their server during processing. Their privacy policy might say they delete it “promptly” — but you have no way to verify that. And if their server is compromised, your photos (with location data) are exposed.

    Client-side processing isn’t just a privacy feature — it’s also faster. There’s no upload latency, no server queue, no download wait. Stripping metadata from a 5MB photo takes under 100ms locally. The same operation through a server-based tool takes 3-5 seconds minimum, most of that being network transfer time.

    Building PixelStrip: Lessons Learned

    I started this project after reading about a journalist whose source was identified through photo EXIF data. The source had sent photos to document conditions at a facility, and the GPS coordinates in those photos led directly back to them. That story stuck with me — not because the technology was exotic, but because the fix was so simple. Strip the metadata before sharing. The problem was that no tool made it easy to do privately.

    Building a JPEG parser from scratch taught me that the JPEG binary format is messy in practice, even though the spec is straightforward in theory. Every camera manufacturer embeds EXIF slightly differently. Some use big-endian byte order, others use little-endian — and the byte order is specified per-segment, so you have to check it for each EXIF block. I spent an entire weekend debugging a parsing failure that turned out to be a Samsung phone embedding a non-standard MakerNote tag with its own internal IFD structure.

    Some camera brands embed thumbnail images inside the EXIF data — a smaller version of the original photo. This is particularly dangerous because even if you crop a photo to remove something sensitive, the uncropped thumbnail might still be in the EXIF data. PixelStrip removes these thumbnails along with everything else in the APP1 segment.

    Browser memory limits were another challenge. JavaScript’s ArrayBuffer works great for typical photos (2-10MB), but panoramic shots and RAW-converted JPEGs can exceed 30MB. On mobile browsers with limited memory, loading a 30MB file into an ArrayBuffer while also keeping the original for display can cause the tab to crash. I added a file size check that warns users about very large files and processes them in chunks when possible.

    Testing was the most time-consuming part. I collected sample photos from every major smartphone brand — Apple, Samsung, Google, OnePlus, Xiaomi, Huawei — because each manufacturer embeds EXIF differently. Apple’s format is clean and consistent. Samsung adds proprietary MakerNote fields. Google Pixel phones embed detailed lens calibration data. Each one required testing to make sure PixelStrip could identify and strip all metadata tags without corrupting the image data.

    The most satisfying moment was running a stripped photo through ExifTool and seeing No EXIF data found. That’s the standard I hold PixelStrip to: not “most metadata removed” but “all metadata removed, verified by an independent tool.” If ExifTool can’t find it, neither can anyone else.

    Who This Is For

    • Online sellers — don’t leak your home address through product photos
    • Freelancers & agencies — strip client metadata before handing off deliverables
    • Privacy-conscious individuals — clean photos before posting anywhere
    • Journalists & researchers — protect source locations and device identities
    • Parents — remove geotags from family photos shared in group chats

    Try It

    👉 pixelstrip.orthogonal.info

    Drop a photo. See what’s hiding. Strip it. Download. Takes about three seconds.

    Related Privacy Tools

    Get Weekly Security & DevOps Insights

    Join 500+ engineers getting actionable tutorials on Kubernetes security, homelab builds, and trading automation. No spam, unsubscribe anytime.

    Subscribe Free →

    Delivered every Tuesday. Read by engineers at Google, AWS, and startups.

    Frequently Asked Questions

    What is PixelStrip and how does it protect my privacy?

    PixelStrip is a tool that removes hidden metadata from your photos before you share them. It strips GPS coordinates, device information, timestamps, and other EXIF data that could reveal your location, identity, or daily patterns to anyone who downloads your images.

    What personal information is hidden in my photos?

    Photos taken with smartphones contain embedded EXIF metadata including exact GPS coordinates, date and time, device model and serial number, camera settings, and sometimes your name. This data persists when you share photos and can be read by anyone with basic tools.

    Does PixelStrip work offline without uploading my photos?

    Yes, PixelStrip processes images entirely in your browser using client-side JavaScript. Your photos never leave your device and no data is sent to any server, which is essential since the whole purpose of the tool is to protect your private information.

    Which photo formats does PixelStrip support?

    PixelStrip supports JPEG files, which are the primary format that contains EXIF metadata. PNG files generally do not carry the same detailed metadata, and screenshots typically lack GPS data, so JPEG photos from cameras and smartphones are where metadata removal matters most.

    References

  • QuickShrink: Browser Image Compressor, No Uploads

    QuickShrink: Browser Image Compressor, No Uploads

    I got tired of uploading personal photos to random websites just to shrink them. So I built QuickShrink — an image compressor that runs entirely in your browser. Your images never touch a server.

    The Dirty Secret of “Free” Image Compressors

    📌 TL;DR: I got tired of uploading personal photos to random websites just to shrink them. So I built QuickShrink — an image compressor that runs entirely in your browser. Your images never touch a server.
    🎯 Quick Answer: QuickShrink compresses images up to 80% in your browser with zero server uploads. Unlike TinyPNG or Squoosh, your photos never leave your device, making it the most private image compression tool available.

    Go ahead and Google “compress image online.” You’ll find dozens of tools, all with the same pitch: drop your image, we’ll compress it, download the result.

    Here’s what they don’t tell you: your photo gets uploaded to their server. A server in a data center you’ve never seen, governed by a privacy policy you’ve never read, in a jurisdiction you might not even recognize. That family photo (which might be broadcasting your GPS location), that screenshot of your bank statement, that product image for your client — it’s now sitting on someone else’s disk.

    Some of these services explicitly state they delete uploads after an hour. Others are silent on the matter. A few have been caught in breaches. The point isn’t that they’re malicious — it’s that there’s no reason for the upload to happen in the first place.

    The Canvas API Makes Servers Unnecessary

    Modern browsers ship with the Canvas API — a powerful image processing engine built into Chrome, Firefox, Safari, and Edge. It can decode an image, manipulate its pixels, and re-encode it at any quality level. All of this happens in memory, on your device, using your CPU.

    QuickShrink uses this. When you drop an image:

    1. The browser reads the file into memory (no network request)
    2. A Canvas element renders the image at its native resolution
    3. canvas.toBlob() re-encodes it as JPEG at your chosen quality (10%–100%)
    4. You download the result directly from browser memory

    Total data transmitted over the network: zero bytes.

    Under the Hood: How Canvas API Compression Actually Works

    To understand why browser-based compression works so well, it helps to know what JPEG compression actually does under the surface. It’s not just “make the file smaller” — it’s a multi-stage pipeline that exploits how human vision works.

    JPEG compression works in five distinct stages:

    1. Color space conversion (RGB → YCbCr). Your image starts as red, green, and blue channels. The encoder converts it into luminance (brightness) and two chrominance (color) channels. This separation is key — human eyes are far more sensitive to brightness than to color.
    2. Chroma subsampling. Since our eyes barely notice color detail, the encoder reduces the resolution of the two color channels. A common scheme is 4:2:0, which halves the color resolution in both dimensions — cutting color data to 25% of its original size with almost no perceptible difference.
    3. Discrete Cosine Transform (DCT) on 8×8 pixel blocks. The image is divided into 8×8 pixel blocks, and each block is transformed from spatial data (pixel values) into frequency data (patterns of light and dark). Low-frequency components represent smooth gradients; high-frequency components represent sharp edges and fine detail.
    4. Quantization — this is where quality loss happens. The frequency data from each block is divided by a quantization matrix and rounded. High-frequency components (fine detail) get divided by larger numbers, effectively zeroing them out. This is the lossy step — and it’s where the quality parameter has its effect.
    5. Huffman encoding. Finally, the quantized data is compressed using lossless Huffman coding, which replaces common patterns with shorter bit sequences. This is the same principle behind ZIP compression — no data is lost in this step.

    When you call canvas.toBlob() with a quality parameter, the browser’s built-in JPEG encoder runs all of these steps. The quality parameter (0.0 to 1.0) controls step 4 — quantization. Lower quality means more aggressive quantization, which produces a smaller file but introduces more artifacts. Higher quality preserves more detail but results in a larger file.

    Here’s how the browser’s compression maps to these steps in practice:

    // The browser handles all the complexity behind this one call
    canvas.toBlob(
      (blob) => {
        // blob is your compressed image
        console.log(`Compressed: ${blob.size} bytes`);
      },
      'image/jpeg',
      0.8  // quality: 0.0 (max compression) to 1.0 (no compression)
    );

    That single method call triggers the entire five-stage pipeline. The browser’s native JPEG encoder — written in optimized C++ and compiled to machine code — handles color conversion, DCT transforms, quantization, and Huffman coding. You get the output of a sophisticated compression algorithm through one line of JavaScript.

    The Complete Compression Pipeline: File → Canvas → Blob → Download

    Understanding the theory is useful, but let’s look at how the full pipeline works in practice. Here’s the complete compression function that QuickShrink uses at its core:

    async function compressImage(file, quality = 0.8) {
      // Step 1: Read file into an Image object
      const img = await createImageBitmap(file);
      
      // Step 2: Create canvas at original dimensions
      const canvas = document.createElement('canvas');
      canvas.width = img.width;
      canvas.height = img.height;
      
      // Step 3: Draw image onto canvas
      const ctx = canvas.getContext('2d');
      ctx.drawImage(img, 0, 0);
      
      // Step 4: Re-encode as JPEG at target quality
      const blob = await new Promise(resolve => {
        canvas.toBlob(resolve, 'image/jpeg', quality);
      });
      
      // Step 5: Calculate savings
      const savings = ((file.size - blob.size) / file.size * 100).toFixed(1);
      console.log(`${file.name}: ${formatBytes(file.size)} → ${formatBytes(blob.size)} (${savings}% saved)`);
      
      return blob;
    }
    
    function formatBytes(bytes) {
      if (bytes === 0) return '0 B';
      const k = 1024;
      const sizes = ['B', 'KB', 'MB'];
      const i = Math.floor(Math.log(bytes) / Math.log(k));
      return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
    }

    The download trigger is equally straightforward — we create a temporary object URL, simulate a click on an anchor element, and immediately clean up:

    function downloadBlob(blob, filename) {
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = filename.replace(/\.[^.]+$/, '_compressed.jpg');
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(url); // free memory
    }

    The entire pipeline — from file selection to download — happens in under 500ms for a typical 3MB photo. No network round-trips, no upload progress bars, no waiting for a server to process your image. The bottleneck is your CPU’s JPEG encoder, which on any modern device is blazingly fast.

    EXIF Data: The Privacy Metadata You Forgot About

    Every photo your phone takes is embedded with invisible metadata called EXIF data. This includes GPS coordinates (often accurate to within a few meters), your camera model and serial number, the exact timestamp the photo was taken, and even the software used to edit it. If you’ve ever wondered how someone could figure out where a photo was taken — EXIF is the answer.

    The amount of data stored in EXIF is staggering. A typical iPhone photo contains over 40 metadata fields: latitude and longitude, altitude, lens aperture, shutter speed, ISO, focal length, white balance, flash status, orientation, color space, and device-specific identifiers. Some Android phones even include the device’s unique hardware ID. When you share that photo — compressed or not — all of that metadata travels with it unless explicitly removed.

    Here’s the problem: most “compress” tools keep EXIF data intact. Your compressed image still broadcasts your location, your device information, and your editing history. You think you’re just making a file smaller, but you’re passing along a dossier of metadata to whoever receives the image.

    QuickShrink can show you what EXIF data exists in your image before stripping it. Here’s the code that reads EXIF markers from a JPEG file:

    // Read EXIF data from JPEG to show user what's being removed
    function readExifData(file) {
      return new Promise((resolve) => {
        const reader = new FileReader();
        reader.onload = function(e) {
          const view = new DataView(e.target.result);
          
          // JPEG starts with 0xFFD8
          if (view.getUint16(0) !== 0xFFD8) {
            resolve({ hasExif: false });
            return;
          }
          
          // Find EXIF marker (0xFFE1)
          let offset = 2;
          while (offset < view.byteLength) {
            const marker = view.getUint16(offset);
            if (marker === 0xFFE1) {
              resolve({
                hasExif: true,
                exifSize: view.getUint16(offset + 2),
                message: 'EXIF data found — GPS, camera info, timestamps will be stripped'
              });
              return;
            }
            offset += 2 + view.getUint16(offset + 2);
          }
          resolve({ hasExif: false });
        };
        reader.readAsArrayBuffer(file.slice(0, 128 * 1024)); // only read first 128KB
      });
    }

    When QuickShrink draws your image onto a Canvas and re-exports it, the Canvas API creates a brand new JPEG file. EXIF data from the original doesn’t carry over. This means compression through QuickShrink doubles as a privacy tool — your compressed photos won’t contain GPS coordinates, camera serial numbers, or editing software metadata. If you want a dedicated tool for this, check out PixelStrip, which I built specifically for EXIF removal.

    Benchmarks: Real Numbers From Real Photos

    Theory is nice, but numbers are better. I ran a set of real-world photos through QuickShrink at three different quality levels to see how the compression performs across different image types:

    Test Image Original 80% Quality 60% Quality 40% Quality
    Portrait (iPhone 15) 4.2 MB 1.8 MB (57% saved) 1.1 MB (74% saved) 0.7 MB (83% saved)
    Landscape (Canon R6) 8.7 MB 3.2 MB (63% saved) 1.9 MB (78% saved) 1.2 MB (86% saved)
    Screenshot (1440p) 1.8 MB 0.4 MB (78% saved) 0.2 MB (89% saved) 0.1 MB (94% saved)
    Product Photo (studio) 5.1 MB 2.0 MB (61% saved) 1.3 MB (75% saved) 0.8 MB (84% saved)
    Drone Aerial (DJI) 12.3 MB 4.1 MB (67% saved) 2.5 MB (80% saved) 1.6 MB (87% saved)

    A few patterns emerge from these numbers. Screenshots compress the most aggressively because they contain large areas of flat color and sharp text — patterns that JPEG’s DCT transform handles efficiently. The 8×8 pixel blocks in a screenshot often contain identical or near-identical values, which quantize down to almost nothing. Photos with complex textures (landscapes, aerials) still see significant savings, but the encoder has to work harder to preserve fine detail like grass, foliage, and water ripples.

    Notice that even at 40% quality, the drone aerial drops from 12.3 MB to 1.6 MB — an 87% reduction. For web use, email, or social media, this is more than adequate. Most social platforms recompress your uploads anyway, so starting with a leaner file means faster uploads and less double-compression artifacting.

    Want to run your own benchmarks? Here’s a function that tests multiple quality levels and prints a comparison table:

    // Run your own benchmarks
    async function benchmarkCompression(file) {
      const qualities = [0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3];
      const results = [];
      
      const img = await createImageBitmap(file);
      const canvas = document.createElement('canvas');
      canvas.width = img.width;
      canvas.height = img.height;
      const ctx = canvas.getContext('2d');
      ctx.drawImage(img, 0, 0);
      
      for (const q of qualities) {
        const start = performance.now();
        const blob = await new Promise(r => canvas.toBlob(r, 'image/jpeg', q));
        const time = (performance.now() - start).toFixed(1);
        
        results.push({
          quality: `${q * 100}%`,
          size: formatBytes(blob.size),
          saved: `${((file.size - blob.size) / file.size * 100).toFixed(1)}%`,
          time: `${time}ms`
        });
      }
      
      console.table(results);
      return results;
    }

    The sweet spot for most use cases is 70–80% quality. Below 60%, text in screenshots becomes noticeably fuzzy. Above 90%, you’re barely saving any space. I personally use 75% as the default in QuickShrink because it balances file size and visual quality for the widest range of image types.

    The Results Are Surprisingly Good

    At 80% quality (the default), most photos shrink by 40–60% with no visible degradation. At 60%, you’re looking at 70–80% reduction — still good enough for web use, email attachments, and social media. Only below 30% do you start seeing compression artifacts.

    The interface shows you exact numbers: original size, compressed size, and percentage saved. No guessing.

    It’s Also a PWA

    QuickShrink is a Progressive Web App — one of several free browser tools that can replace desktop apps. On mobile, your browser will offer to “Add to Home Screen.” On desktop Chrome, you’ll see an install icon in the address bar. Once installed, it launches in its own window, works offline, and feels like a native app — because functionally, it is one.

    The entire application is a single HTML file with inline CSS and JavaScript. No build tools, no framework, no dependencies. It loads in under 200ms on any connection.

    Try It

    👉 quickshrink.orthogonal.info

    Open source, zero tracking, free forever. If you find it useful, share it with someone who’s still uploading their photos to compress them.

    Get Weekly Security & DevOps Insights

    Join 500+ engineers getting actionable tutorials on Kubernetes security, homelab builds, and trading automation. No spam, unsubscribe anytime.

    Subscribe Free →

    Delivered every Tuesday. Read by engineers at Google, AWS, and startups.

    Frequently Asked Questions

    What is QuickShrink: Browser Image Compressor, No Uploads about?

    I got tired of uploading personal photos to random websites just to shrink them. So I built QuickShrink — an image compressor that runs entirely in your browser.

    Who should read this article about QuickShrink: Browser Image Compressor, No Uploads?

    Anyone interested in learning about QuickShrink: Browser Image Compressor, No Uploads and related topics will find this article useful.

    What are the key takeaways from QuickShrink: Browser Image Compressor, No Uploads?

    Your images never touch a server. The Dirty Secret of “Free” Image Compressors Go ahead and Google “compress image online.” You’ll find dozens of tools, all with the same pitch: drop your image, we’ll

    References

  • Claude Code Review: My Honest Take After 3 Months

    Claude Code Review: My Honest Take After 3 Months

    Three months ago, I was skeptical. Another AI coding tool? I’d already tried GitHub Copilot, Cursor, and a handful of VS Code extensions that promised to “10x my productivity.” Most of them were glorified autocomplete — helpful for boilerplate, useless for anything that required actual understanding of a codebase. Then I installed Claude Code, and within the first hour, it did something none of the others had done: it read my entire project, understood the architecture, and fixed a bug I’d been ignoring for two weeks.

    This isn’t a puff piece. I’ve been using Claude Code daily on production projects — Kubernetes deployments, FastAPI services, React dashboards — and I have strong opinions about where it shines and where it still falls short. Let me walk you through what I’ve learned.

    What Makes Claude Code Different

    📌 TL;DR: Three months ago, I was skeptical. Another AI coding tool? I’d already tried GitHub Copilot, Cursor, and a handful of VS Code extensions that promised to “10x my productivity.
    🎯 Quick Answer: After 3 months of daily use, Claude Code excels at complex multi-file refactoring and architectural reasoning compared to GitHub Copilot and Cursor. Copilot is better for inline autocomplete, Cursor for IDE integration, but Claude Code handles ambiguous, large-scope tasks most effectively.

    Most AI coding assistants work at the file level. You highlight some code, ask a question, get an answer. Claude Code operates at the project level. It’s an agentic coding tool that reads your codebase, edits files, runs commands, and integrates with your development tools. It works in your terminal, IDE (VS Code and JetBrains), browser, and even as a desktop app.

    The key word here is agentic. Unlike a chatbot that answers questions and waits, Claude Code can autonomously explore your codebase, plan changes across multiple files, run tests to verify its work, and iterate until things actually pass. You describe what you want; Claude figures out how to build it.

    Here’s how I typically start a session:

    # Navigate to your project
    cd ~/projects/my-api
    
    # Launch Claude Code
    claude
    
    # Ask it something real
    > explain how authentication works in this codebase
    

    That first command is where the magic happens. Claude doesn’t just grep for “auth” — it traces the entire flow from middleware to token validation to database queries. It builds a mental model of your code that persists throughout the session.

    The Workflows That Actually Save Me Time

    1. Onboarding to Unfamiliar Code

    I recently inherited a Node.js monorepo with zero documentation. Instead of spending a week reading source files, I ran:

    > give me an overview of this codebase
    > how do these services communicate?
    > trace a user login from the API gateway to the database
    

    In 20 minutes, I had a better understanding of the architecture than I would have gotten from a week of code reading. Claude identified the service mesh pattern, pointed out the shared protobuf definitions, and even flagged a deprecated authentication path that was still being hit in production.

    💡 Pro Tip: When onboarding, start broad and narrow down. Ask about architecture first, then drill into specific components. Claude keeps context across the session, so each question builds on the last.

    2. Bug Fixing With Context

    Here’s where Claude Code absolutely destroys traditional AI tools. Instead of pasting error messages and hoping for the best, you can do this:

    > I'm seeing a 500 error when users try to reset their password.
    > The error only happens for accounts created before January 2025.
    > Find the root cause and fix it.
    

    Claude will read the relevant files, check the database migration history, identify that older accounts use a different hashing scheme, and propose a fix — complete with a migration script and updated tests. All in one shot.

    3. The Plan-Then-Execute Pattern

    For complex changes, I’ve adopted a two-phase workflow that dramatically reduces wasted effort:

    # Phase 1: Plan Mode (read-only, no changes)
    claude --permission-mode plan
    
    > I need to add OAuth2 support. What files need to change?
    > What about backward compatibility?
    > How should we handle the database migration?
    
    # Phase 2: Execute (switch to normal mode)
    # Press Shift+Tab to exit Plan Mode
    
    > Implement the OAuth flow from your plan.
    > Write tests for the callback handler.
    > Run the test suite and fix any failures.
    

    Plan Mode is like having a senior architect review your approach before you write a single line of code. Claude reads the codebase with read-only access, asks clarifying questions, and produces a detailed implementation plan. Only when you’re satisfied do you let it start coding.

    🔐 Security Note: Plan Mode is especially valuable for security-sensitive changes. I always use it before modifying authentication, authorization, or encryption code. Having Claude analyze the security implications before making changes has caught issues I would have missed.

    CLAUDE.md — Your Project’s Secret Weapon

    This is the feature that separates power users from casual users. CLAUDE.md is a special file that Claude reads at the start of every conversation. Think of it as persistent context that tells Claude how your project works, what conventions to follow, and what to avoid.

    Here’s what mine looks like for a typical project:

    # Code Style
    - Use ES modules (import/export), not CommonJS (require)
    - Destructure imports when possible
    - All API responses must use the ResponseWrapper class
    
    # Testing
    - Run tests with: npm run test:unit
    - Always run tests after making changes
    - Use vitest, not jest
    
    # Security
    - Never commit .env files
    - All API endpoints must validate JWT tokens
    - Use parameterized queries — no string interpolation in SQL
    
    # Architecture
    - Services communicate via gRPC, not REST
    - All database access goes through the repository pattern
    - Scheduled jobs use BullMQ, not cron
    

    The /init command can generate a starter CLAUDE.md by analyzing your project structure. But I’ve found that manually curating it produces much better results. Keep it concise — if it’s too long, Claude starts ignoring rules (just like humans ignore long READMEs).

    ⚠️ Gotcha: Don’t put obvious things in CLAUDE.md like “write clean code” or “use meaningful variable names.” Claude already knows that. Focus on project-specific conventions that Claude can’t infer from the code itself.

    Security Configuration — The Part Most People Skip

    As a security engineer, this is where I get opinionated. Claude Code has a solid permission system, and you should use it. The default “ask for everything” mode is fine for exploration, but for daily use, you want to configure explicit allow/deny rules.

    Here’s my .claude/settings.json for a typical project:

    {
     "permissions": {
     "allow": [
     "Bash(npm run lint)",
     "Bash(npm run test *)",
     "Bash(git diff *)",
     "Bash(git log *)"
     ],
     "deny": [
     "Read(./.env)",
     "Read(./.env.*)",
     "Read(./secrets/**)",
     "Read(./config/credentials.json)",
     "Bash(curl *)",
     "Bash(wget *)",
     "WebFetch"
     ]
     }
    }
    

    The deny rules are critical. By default, Claude can read any file in your project — including your .env files with database passwords, API keys, and secrets. The permission rules above ensure Claude never sees those files, even accidentally.

    🚨 Common Mistake: Running claude --dangerously-skip-permissions in a directory with sensitive files. This flag bypasses ALL permission checks. Only use it inside a sandboxed container with no network access and no sensitive data.

    For even stronger isolation, Claude Code supports OS-level sandboxing that restricts filesystem and network access:

    {
     "sandbox": {
     "enabled": true,
     "autoAllowBashIfSandboxed": true,
     "network": {
     "allowedDomains": ["github.com", "*.npmjs.org"],
     "allowLocalBinding": true
     }
     }
    }
    

    With sandboxing enabled, Claude can work more freely within defined boundaries — no more clicking “approve” for every npm install.

    Subagents and Parallel Execution

    One of Claude Code’s most powerful features is subagents — specialized AI assistants that run in their own context window. This is huge for context management, which is the number one performance bottleneck in long sessions.

    Here’s a custom security reviewer subagent I use on every project:

    # .claude/agents/security-reviewer.md
    ---
    name: security-reviewer
    description: Reviews code for security vulnerabilities
    tools: Read, Grep, Glob, Bash
    model: opus
    ---
    You are a senior security engineer. Review code for:
    - Injection vulnerabilities (SQL, XSS, command injection)
    - Authentication and authorization flaws
    - Secrets or credentials in code
    - Insecure data handling
    
    Provide specific line references and suggested fixes.
    

    Then in my main session:

    > use the security-reviewer subagent to audit the authentication module
    

    The subagent explores the codebase in its own context, reads all the relevant files, and reports back with findings — without cluttering my main conversation. I’ve caught three real vulnerabilities this way that I would have missed in manual review.

    CI/CD Integration — Claude in Your Pipeline

    Claude Code isn’t just an interactive tool. With claude -p "prompt", you can run it headlessly in CI/CD pipelines, pre-commit hooks, or any automated workflow.

    Here’s how I use it as an automated code reviewer:

    // package.json
    {
     "scripts": {
     "lint:claude": "claude -p 'Review the changes vs main. Check for: 1) security issues, 2) missing error handling, 3) hardcoded secrets. Report filename, line number, and issue description. No other text.' --output-format json"
     }
    }
    

    And for batch operations across many files:

    # Migrate 200 React components from class to functional
    for file in $(cat files-to-migrate.txt); do
     claude -p "Migrate $file from class component to functional with hooks. Preserve all existing tests." \
     --allowedTools "Edit,Bash(npm run test *)"
    done
    

    The --allowedTools flag is essential here — it restricts what Claude can do when running unattended, which is exactly the kind of guardrail you want in automation.

    MCP Integration — Connecting Claude to Everything

    Model Context Protocol (MCP) servers let you connect Claude Code to external tools — databases, issue trackers, monitoring dashboards, design tools. This is where things get genuinely powerful.

    # Add a GitHub MCP server
    claude mcp add github
    
    # Now Claude can directly interact with GitHub
    > create a PR for my changes with a detailed description
    > look at issue #42 and implement a fix
    

    I’ve connected Claude to our Prometheus instance, and now I can say things like “check the error rate for the auth service over the last 24 hours” and get actual data, not hallucinated numbers. The MCP ecosystem is still young, but it’s growing fast.

    What I Don’t Like (Honest Criticism)

    No tool is perfect, and Claude Code has real limitations:

    • Context window fills up fast. This is the single biggest constraint. A complex debugging session can burn through your entire context in 15-20 minutes. You need to actively manage it with /clear between tasks and /compact to summarize.
    • Cost adds up. Claude Code uses Claude’s API, and complex sessions with extended thinking can get expensive. I’ve had single sessions cost $5-10 on deep architectural refactors.
    • It can be confidently wrong. Claude sometimes produces plausible-looking code that doesn’t actually work. Always provide tests or verification criteria — don’t trust output you can’t verify.
    • Initial setup friction. Getting permissions, CLAUDE.md, and MCP servers configured takes real effort upfront. The payoff is worth it, but the first day or two can be frustrating.
    💡 Pro Tip: Track your context usage with a custom status line. Run /config and set up a status line that shows context percentage. When you’re above 80%, it’s time to /clear or /compact.

    My Daily Workflow

    After three months of daily use, here’s the pattern I’ve settled on:

    1. Morning: Start Claude Code, resume yesterday’s session with claude --continue. Review what was done, check test results.
    2. Feature work: Use Plan Mode for anything touching more than 3 files. Let Claude propose the approach, then execute.
    3. Code review: Use a security-reviewer subagent on all PRs before merging. Catches things human reviewers miss.
    4. Bug fixes: Paste the error, give Claude the reproduction steps, let it trace the root cause. Fix in one shot 80% of the time.
    5. End of day: /rename the session with a descriptive name so I can find it tomorrow.

    The productivity gain is real, but it’s not the “10x” that marketing departments love to claim. I’d estimate it’s a consistent 2-3x improvement, heavily weighted toward tasks that involve reading existing code, debugging, and refactoring. For greenfield development where I know exactly what I want, the improvement is smaller.

    🛠️ Recommended Resources:

    Tools and books mentioned in (or relevant to) this article:

    Quick Summary

    • Claude Code is an agentic tool, not autocomplete. It reads, plans, executes, and verifies. Treat it like a capable junior developer, not a fancy text expander.
    • CLAUDE.md is essential. Invest time in curating project-specific instructions. Keep it short, focused on things Claude can’t infer.
    • Configure security permissions from day one. Deny access to .env files, secrets, and credentials. Use sandboxing for automated workflows.
    • Manage context aggressively. Use /clear between tasks, subagents for investigation, and Plan Mode for complex changes.
    • Always provide verification. Tests, linting, screenshots — give Claude a way to check its own work. This is the single highest-use thing you can do.

    Have you tried Claude Code? I’d love to hear about your setup — especially if you’ve found clever ways to use CLAUDE.md, subagents, or MCP integrations. Drop a comment or ping me. Next week, I’ll dive into setting up Claude Code with custom MCP servers for homelab monitoring. Stay tuned!

    Get Weekly Security & DevOps Insights

    Join 500+ engineers getting actionable tutorials on Kubernetes security, homelab builds, and trading automation. No spam, unsubscribe anytime.

    Subscribe Free →

    Delivered every Tuesday. Read by engineers at Google, AWS, and startups.

    Frequently Asked Questions

    What is Claude Code Review: My Honest Take After 3 Months about?

    Three months ago, I was skeptical. Another AI coding tool?

    Who should read this article about Claude Code Review: My Honest Take After 3 Months?

    Anyone interested in learning about Claude Code Review: My Honest Take After 3 Months and related topics will find this article useful.

    What are the key takeaways from Claude Code Review: My Honest Take After 3 Months?

    I’d already tried GitHub Copilot, Cursor, and a handful of VS Code extensions that promised to “10x my productivity.” Most of them were glorified autocomplete — helpful for boilerplate, useless for an

    📋 Disclosure: Some links are affiliate links. If you purchase through these links, I earn a small commission at no extra cost to you. I only recommend products I’ve personally used or thoroughly evaluated. This helps support orthogonal.info and keeps the content free.

    📚 Related Articles

    📊 Free AI Market Intelligence

    Join Alpha Signal — AI-powered market research delivered daily. Narrative detection, geopolitical risk scoring, sector rotation analysis.

    Join Free on Telegram →

    Pro with stock conviction scores: $5/mo

  • Solving Homelab Bottlenecks: Why Upgrading to a 2.5G

    Solving Homelab Bottlenecks: Why Upgrading to a 2.5G

    A Costly Oversight: Lessons from My Homelab Upgrade

    📌 TL;DR: A Costly Oversight: Lessons from My Homelab Upgrade Imagine spending $800 upgrading your homelab network, only to discover that one overlooked component reduced all your shiny new hardware to a fraction of its potential.
    🎯 Quick Answer: A $50 Cat5e patch cable or unmanaged switch can bottleneck an $800 2.5GbE network upgrade to 1Gbps. Always verify every component in the chain—cables, switches, NICs, and router ports—supports 2.5GbE before assuming the upgrade is complete.

    🏠 My setup: 10GbE between TrueNAS and switch · 2.5GbE to all workstations · TrueNAS SCALE · 64GB ECC RAM · 60TB+ ZFS storage · OPNsense firewall.

    After saturating my 1GbE links with ZFS replication and nightly backups between my TrueNAS SCALE server and offsite NAS, I knew it was time to upgrade. My 60TB+ of data wasn’t going to back up itself any faster over Gigabit. Here’s how the 2.5GbE upgrade changed everything—and the one $50 mistake that almost ruined it.

    Here’s how it all started: a new Synology NAS with 2.5GbE ports, a WiFi 6 router with multi-gig backhaul, and a 2.5G PCIe NIC for my workstation. Everything was in place for faster local file transfers—or so I thought.

    But my first big test—copying a 60GB photo library to the NAS—produced speeds capped at 112 MB/s. That’s the exact throughput of a Gigabit connection. After much head-scratching and troubleshooting, I realized my old 5-port Gigabit switch was bottlenecking my entire setup. A $50 oversight had rendered my $800 investment nearly pointless.

    The Gigabit Bottleneck: Why It Matters

    Homelab enthusiasts often focus on the specs of NAS devices, routers, and workstations, but the network switch—the component connecting everything—is frequently overlooked. If your switch maxes out at 1Gbps, it doesn’t matter if your other devices support 2.5GbE or even 10GbE. The switch becomes the choke point, throttling your network at its weakest link.

    Here’s how this bottleneck impacts performance:

    • Modern NAS devices with 2.5GbE ports can theoretically transfer data at 295 MB/s. A Gigabit switch limits this to just 112 MB/s.
    • WiFi 6 routers with multi-gig backhaul can push 2.4Gbps or more, but a Gigabit switch throttles them to under 1Gbps.
    • Even affordable 2.5G PCIe NICs (available for under $20) are wasted if your switch can’t keep up with their capabilities.
    • Running multiple simultaneous workloads—such as streaming 4K content while transferring files—suffers significant slowdowns with a Gigabit switch, as it cannot handle the combined bandwidth demands.
    Pro Tip: Upgrading to a multi-gig switch doesn’t just improve single-device speeds—it unlocks better multi-device performance. Say goodbye to buffering while streaming 4K Plex content or transferring large files simultaneously.

    Choosing the Right 2.5G Switch

    Once I realized the problem, I started researching 2.5GbE switches. My requirements were simple: affordable, quiet, and easy to use. However, I was quickly overwhelmed by the variety of options available. Enterprise-grade switches offered incredible features like managed VLANs and 10G uplinks, but they were pricey and noisy—far beyond what my homelab needed.

    After comparing dozens of options, I landed on the NICGIGA 6-Port 2.5G Unmanaged Switch. It was quiet, affordable, and had future-proof capabilities, including two 10G SFP+ ports for potential upgrades.

    Key Criteria for Selecting a Switch

    Here’s what I looked for during my search:

    1. Port Configuration

    A mix of 2.5GbE Base-T ports and 10G SFP+ ports was ideal. The 2.5GbE ports supported my NAS, workstation, and WiFi 6 access point, while the SFP+ ports provided an upgrade path for future 10GbE devices or additional connections.

    2. Fanless Design

    Fan noise in a homelab can be a dealbreaker, especially if it’s near a home office. Many enterprise-grade switches include active cooling systems, which can be noisy. Instead, I prioritized a fanless switch that uses passive cooling. The NICGIGA switch operates silently, even under heavy loads.

    3. Plug-and-Play Simplicity

    I wanted an unmanaged switch—no web interface, no VLAN configuration, no firmware updates to worry about. Just plug in the cables, power it on, and let it do its job. This simplicity made the NICGIGA a perfect fit for my homelab.

    4. Build Quality

    Durability is essential for hardware in a homelab. The NICGIGA switch features a sturdy metal casing that not only protects its internal components but also provides better heat dissipation. Also, its build quality gave me peace of mind during frequent thunderstorms, as it’s resistant to power surges.

    5. Switching Capacity

    A switch’s backplane bandwidth determines how much data it can handle across all its ports simultaneously. The NICGIGA boasts a 60Gbps switching capacity, ensuring that every port can operate at full speed without bottlenecks, even during multi-device workloads.

    Installing and Testing the Switch

    Setting up the new switch was straightforward:

    1. Unplugged the old Gigabit switch and labeled the Ethernet cables for easier reconnection.
    2. Mounted the new switch on my wall-mounted rack using the included hardware.
    3. Connected the power adapter and verified that the switch powered on.
    4. Reconnected the Ethernet cables to the 2.5GbE ports, ensuring proper placement for devices like my NAS and workstation.
    5. Observed the LEDs on the switch to verify link speeds. Green indicated 2.5GbE, while orange indicated Gigabit connections.

    Within minutes, my network was upgraded. The speed difference was immediately noticeable during file transfers and streaming sessions.

    Before vs. After: Performance Metrics

    Here’s how my network performed before and after upgrading:

    Metric Gigabit Switch 2.5GbE Switch
    Transfer Speed 112 MB/s 278 MB/s
    50GB File Transfer Time 7m 26s 3m 0s
    Streaming Plex 4K Occasional buffering Smooth playback
    Multi-device Load Noticeable slowdown No impact
    ⚠️ What went wrong for me: I spent an hour troubleshooting why my workstation was stuck at 1Gbps after the switch upgrade. Turns out my Cat5 patch cables couldn’t handle 2.5GbE—they looked fine but were only rated for 100MHz. Swapping to Cat6 cables instantly jumped me to full 2.5Gbps. Check your cables before you blame the hardware.

    Common Pitfalls and Troubleshooting

    Upgrading to multi-gig networking isn’t always plug-and-play. Here are some common issues and their solutions:

    • Problem: Device only connects at Gigabit speed.
      Solution: Check if the Ethernet cable supports Cat5e or higher. Older cables may not handle 2.5Gbps.
    • Problem: SFP+ port doesn’t work.
      Solution: Ensure the module is compatible with your switch. Some switches only support specific brands of SFP+ modules.
    • Problem: No improvement in transfer speed.
      Solution: Verify your NIC settings. Some network cards default to 1Gbps unless manually configured.
    # Example: Setting NIC speed to 2.5Gbps in Linux
    sudo ethtool -s eth0 speed 2500 duplex full autoneg on
    
    Pro Tip: Use diagnostic tools like iperf3 to test network throughput. It provides detailed insights into your connection speeds and latency.

    Future-Proofing with SFP+ Ports

    The two 10G SFP+ ports on my switch are currently connected to 2.5G modules, but they offer a clear upgrade path to 10GbE. Here’s why they’re valuable:

    • Support for 10G modules allows effortless upgrades.
    • Backward compatibility with 1G and 2.5G modules ensures flexibility.
    • Fiber optic SFP+ modules enable long-distance connections, useful for larger homelabs or network setups in separate rooms.

    When 10GbE hardware becomes affordable, I’ll already have the infrastructure in place for the next big leap.

    Quick Summary

    • Old Gigabit switches are often the bottleneck in modern homelabs. Upgrading to 2.5GbE unlocks noticeable performance improvements.
    • The NICGIGA 6-Port 2.5G Unmanaged Switch offers the ideal balance of affordability, simplicity, and future-proofing.
    • Double-check device compatibility before upgrading—your NAS, router, and workstation need to support 2.5GbE.
    • Use quality Ethernet cables (Cat5e or better) to ensure full speed connections.
    • SFP+ ports provide an upgrade path to 10GbE without replacing the entire switch.
    • Diagnostic tools like iperf3 and ethtool can help troubleshoot speed and configuration issues.

    Investing in a 2.5G switch transformed my homelab experience, making file transfers, media streaming, and backups faster and smoother. If you’re still running a Gigabit network, it might be time to upgrade—and finally let your hardware breathe.

    🛠 Recommended Resources:

    Tools and books mentioned in (or relevant to) this article:

    📋 Disclosure: Some links are affiliate links. If you purchase through these links, I earn a small commission at no extra cost to you. I only recommend products I have personally used or thoroughly evaluated.


    📚 Related Articles

    📊 Free AI Market Intelligence

    Join Alpha Signal — AI-powered market research delivered daily. Narrative detection, geopolitical risk scoring, sector rotation analysis.

    Join Free on Telegram →

    Pro with stock conviction scores: $5/mo

    The Bottom Line

    The 2.5GbE upgrade was the best dollar-per-performance improvement I’ve made in my homelab. A $50 switch and a few Cat6 cables cut my large file transfer times in half. If you’re running a NAS with 2.5GbE ports and still using a Gigabit switch, you’re leaving performance on the table every single day. The upgrade takes 15 minutes and you’ll notice the difference immediately.

    Get Weekly Security & DevOps Insights

    Join 500+ engineers getting actionable tutorials on Kubernetes security, homelab builds, and trading automation. No spam, unsubscribe anytime.

    Subscribe Free →

    Delivered every Tuesday. Read by engineers at Google, AWS, and startups.

    Frequently Asked Questions

    What is Solving Homelab Bottlenecks: Why Upgrading to a 2.5G about?

    A Costly Oversight: Lessons from My Homelab Upgrade Imagine spending $800 upgrading your homelab network, only to discover that one overlooked component reduced all your shiny new hardware to a fracti

    Who should read this article about Solving Homelab Bottlenecks: Why Upgrading to a 2.5G?

    Anyone interested in learning about Solving Homelab Bottlenecks: Why Upgrading to a 2.5G and related topics will find this article useful.

    What are the key takeaways from Solving Homelab Bottlenecks: Why Upgrading to a 2.5G?

    Here’s how it all started: a new Synology NAS with 2.5GbE ports, a WiFi 6 router with multi-gig backhaul, and a 2.5G PCIe NIC for my workstation. Everything was in place for faster local file transfer

    References

  • Homelab Hardware Guide: Build Your Dream Setup 2026

    Homelab Hardware Guide: Build Your Dream Setup 2026

    Why Every Tech Enthusiast Needs a Homelab

    📌 TL;DR: Why Every Tech Enthusiast Needs a Homelab Picture this: you’re streaming your favorite movie from your personal media server, your smart home devices are smoothly automated, and your development environment is running on hardware you control—all without relying on third-party services.
    🎯 Quick Answer: For a 2026 homelab, start with a mini PC like Intel NUC or used Dell Micro (under $300) for low power draw, add a NAS with ECC RAM for data integrity, and run Proxmox or TrueNAS for virtualization. Budget $500–$1,500 for a capable starter setup.

    My homelab started with a single Raspberry Pi running Pi-hole. Today it’s a TrueNAS SCALE server with 64GB of ECC RAM, dual 10GbE NICs, and 60TB+ of ZFS storage running 30+ Docker containers. I’ve made plenty of expensive mistakes along the way—buying consumer gear I had to replace, undersizing my UPS, skipping ECC RAM to save $40. This guide is everything I wish someone had told me before I started.

    🏠 My setup: TrueNAS SCALE on a custom build · 64GB ECC RAM · dual 10GbE NICs · 60TB+ ZFS storage · OPNsense on a Protectli vault · UPS-protected · 30+ Docker containers.

    But here’s the catch: building a homelab can be overwhelming. With endless hardware options and configurations, where do you even start? Whether you’re a beginner or a seasoned pro, this guide will walk you through every step, from entry-level setups to advanced configurations. Let’s dive in.

    💡 Pro Tip: Start small and scale as your needs grow. Over-engineering your setup from day one can lead to wasted resources and unnecessary complexity.

    Step 1: Entry-Level Hardware for Beginners

    If you’re new to homelabs, starting with entry-level hardware is the smartest move. It’s cost-effective, simple to set up, and versatile enough to handle a variety of tasks.

    The Raspberry Pi Revolution

    The Raspberry Pi 5 is a big improvement in the world of single-board computers. With its quad-core processor, USB 3.0 support, and gigabit Ethernet, it’s perfect for running lightweight services like Pi-hole (network-wide ad-blocking), Home Assistant (smart home automation), or even a small web server.

    # Install Docker on Raspberry Pi 5
    curl -fsSL https://get.docker.com | sh
    sudo usermod -aG docker $USER
    
    # Run a lightweight web server
    docker run -d -p 8080:80 nginx
    

    With a power consumption of less than 15 watts, the Raspberry Pi 5 is an energy-efficient choice. Pair it with a high-quality microSD card or an external SSD for storage. If you’re feeling adventurous, you can even cluster multiple Raspberry Pis to create a Kubernetes lab for container orchestration experiments.

    ⚠️ Gotcha: Avoid using cheap, generic power supplies with your Raspberry Pi. Voltage fluctuations can cause instability and hardware damage. Stick to the official power supply for reliable performance.

    Other single-board computers like the Odroid N2+ or RockPro64 are excellent alternatives if you need more RAM or CPU power. These devices offer similar functionality with added expandability, making them ideal for slightly more demanding workloads.

    ⚠️ What went wrong for me: My first UPS was undersized—a cheap 600VA unit. During a 15-minute power outage, it drained in 8 minutes because I forgot to account for the switch, NAS, and OPNsense firewall all drawing power simultaneously. I upgraded to a 1500VA unit with a network management card so TrueNAS can trigger a clean shutdown automatically. Size your UPS for your entire rack, not just the server.

    Step 2: Centralized Storage for Your Data

    As your homelab grows, you’ll quickly realize the importance of centralized storage. A Network Attached Storage (NAS) system is the backbone of any homelab, providing a secure and organized way to store, share, and back up your data.

    Choosing the Right NAS

    The Synology DS224+ NAS is a fantastic choice for beginners and pros alike. With support for up to 32TB of storage, hardware encryption, and Docker container integration, it’s perfect for hosting a Plex media server or automating backups.

    # Set up a shared folder on a Synology NAS
    ssh admin@your-nas-ip
    mkdir /volume1/shared_data
    chmod 777 /volume1/shared_data
    

    If you prefer a DIY approach, consider repurposing old hardware or using a mini PC to build your own NAS. Tools like TrueNAS Core (formerly FreeNAS) make it easy to create a custom storage solution tailored to your needs. DIY NAS setups offer unparalleled flexibility in terms of hardware selection, redundancy, and cost.

    💡 Pro Tip: Use RAID configurations like RAID 1 or RAID 5 for data redundancy. While RAID isn’t a substitute for backups, it provides protection against single-drive failures.

    Expanding with Virtualization

    Modern NAS devices often come with virtualization capabilities. For example, Synology NAS can run virtual machines directly, enabling you to host isolated environments for testing, development, or even gaming servers. This feature is a big improvement for anyone looking to maximize their homelab’s utility.

    Step 3: Networking: The Homelab Backbone

    Your network infrastructure is the glue that holds your homelab together. Consumer-grade routers might suffice for basic setups, but upgrading to prosumer or enterprise-grade equipment can significantly improve performance and reliability.

    Routers and Firewalls

    The UniFi Dream Machine is a top-tier choice for homelab networking. It combines a high-performance router, firewall, and network controller into a single device. Features like intrusion detection and prevention (IDS/IPS) and advanced traffic analytics make it ideal for managing complex network environments.

    WiFi Coverage

    For solid wireless coverage, the TP-Link Omada EAP660 HD is an excellent option. Its WiFi 6 capabilities ensure fast and stable connections, even in device-dense environments. Pair it with a managed switch for maximum flexibility.

    ⚠️ Gotcha: Avoid double NAT setups by ensuring your ISP modem is in bridge mode when using a third-party router. Double NAT can cause connectivity issues and complicate port forwarding.

    Advanced users might consider segmenting their network using VLANs to isolate devices or services. For example, you could create separate VLANs for IoT devices, personal computers, and your NAS for improved security and organization.

    Step 4: Compute Power for Advanced Workloads

    As your homelab evolves, you’ll need more processing power for tasks like virtualization, container orchestration, and development. Mini PCs and small form factor servers are excellent options for scaling your compute resources.

    Choosing a Mini PC

    The Intel NUC 12 Pro is a powerhouse in a compact form factor. With support for Intel vPro, it excels at running multiple virtual machines or Kubernetes clusters. For budget-conscious users, the ASUS PN50 Mini PC offers excellent performance for most homelab tasks at a lower price point.

    Container Orchestration

    Once you have sufficient compute power, container orchestration tools like Kubernetes or Docker Swarm become invaluable. They allow you to manage multiple containers across your devices efficiently. Here’s an example Kubernetes deployment:

    # Example Kubernetes deployment for an NGINX service:
    apiVersion: apps/v1
    kind: Deployment
    metadata:
     name: nginx-deployment
    spec:
     replicas: 2
     selector:
     matchLabels:
     app: nginx
     template:
     metadata:
     labels:
     app: nginx
     spec:
     containers:
     - name: nginx
     image: nginx:1.21
     ports:
     - containerPort: 80
    

    Step 5: Optimizing Storage Performance

    Fast and reliable storage is essential for a high-performing homelab. For boot drives and high-transaction workloads, SSDs are the way to go.

    Choosing the Right SSD

    The Samsung 980 Pro 2TB SSD is a standout choice. Its NVMe interface delivers blazing-fast read/write speeds, making it ideal for databases, Docker images, and operating systems. SSDs ensure quicker boot times and smoother application performance, especially for tasks like video editing or compiling code.

    Step 6: Security and Remote Access

    Exposing your homelab to the internet comes with risks. Prioritizing security is non-negotiable.

    Two-Factor Authentication

    The YubiKey 5C NFC is an excellent hardware security key for adding 2FA to your accounts and services. It’s compatible with SSH, GitHub, and Google Workspace.

    VPN and Remote Access

    Set up a VPN server to securely access your homelab from anywhere. WireGuard is a lightweight and fast option. Here’s a basic installation example:

    # Install WireGuard on Debian/Ubuntu
    sudo apt update
    sudo apt install wireguard
    
    # Generate keys
    wg genkey | tee privatekey | wg pubkey > publickey
    
    🔐 Security Note: Always use strong passwords, update your software regularly, and monitor logs for suspicious activity. Security is a continuous process, not a one-time setup.

    Quick Summary

    • Start small with affordable hardware like the Raspberry Pi 5 and scale as needed.
    • Centralize your data with a pre-built NAS or DIY solution using TrueNAS Core.
    • Invest in enterprise-grade networking gear for stability and scalability.
    • Use mini PCs or small servers to handle compute-intensive tasks.
    • Prioritize security with 2FA, VPNs, and regular updates.
    • Document everything—network configurations, IP addresses, and passwords are vital for troubleshooting.

    A homelab is a journey, not a destination. Whether you’re self-hosting personal services, learning enterprise-grade technologies, or simply tinkering with hardware, the possibilities are endless. Start small, experiment, and enjoy the process of building something truly your own.

    📬 Get Daily Tech & Market Intelligence

    Join our free Alpha Signal newsletter — AI-powered market insights, security alerts, and homelab tips delivered daily.

    Join Free on Telegram →

    No spam. Unsubscribe anytime. Powered by AI.

    Related Reading

    Once your hardware is set, the next decisions matter just as much. Pick the right storage with our guide to the best drives for TrueNAS in 2026, then put your CPU to work running local LLM inference with Ollama on your homelab.

    Where to Start

    Don’t build my setup on day one—I certainly didn’t. Start with a Raspberry Pi or an old laptop running TrueNAS Core. Add a managed switch when you’re ready for VLANs. Upgrade to ECC RAM and proper server hardware only when you know your workload demands it. The most important thing is to start, learn, break things, and iterate. My homelab has been rebuilt three times and I don’t regret a single weekend I spent on it.

    Get Weekly Security & DevOps Insights

    Join 500+ engineers getting actionable tutorials on Kubernetes security, homelab builds, and trading automation. No spam, unsubscribe anytime.

    Subscribe Free →

    Delivered every Tuesday. Read by engineers at Google, AWS, and startups.

    Frequently Asked Questions

    What is Homelab Hardware Guide: Build Your Dream Setup 2026 about?

    Why Every Tech Enthusiast Needs a Homelab Picture this: you’re streaming your favorite movie from your personal media server, your smart home devices are smoothly automated, and your development envir

    Who should read this article about Homelab Hardware Guide: Build Your Dream Setup 2026?

    Anyone interested in learning about Homelab Hardware Guide: Build Your Dream Setup 2026 and related topics will find this article useful.

    What are the key takeaways from Homelab Hardware Guide: Build Your Dream Setup 2026?

    A homelab is more than just a collection of hardware; it’s a playground for tech enthusiasts, a learning platform for professionals, and a fortress of self-hosted services. But here’s the catch: build

    References

    • TrueNAS Documentation — Official documentation for the TrueNAS storage platform.
    • ServeTheHome — Expert reviews and guides for server and homelab hardware.
    • PCPartPicker — Hardware compatibility checker for building custom server configurations.
    • Ubiquiti Introduction — Networking equipment commonly used in professional homelab setups.

Also by us: StartCaaS — AI Company OS · Hype2You — AI Tech Trends