<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mohd Salahudeen</title>
    <description>The latest articles on DEV Community by Mohd Salahudeen (@salahxd).</description>
    <link>https://dev.to/salahxd</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2486134%2Fd0befe46-62f7-4b25-b6c2-e82f771ad077.jpeg</url>
      <title>DEV Community: Mohd Salahudeen</title>
      <link>https://dev.to/salahxd</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/salahxd"/>
    <language>en</language>
    <item>
      <title>Write Once, Post Everywhere</title>
      <dc:creator>Mohd Salahudeen</dc:creator>
      <pubDate>Tue, 29 Sep 2026 12:56:30 +0000</pubDate>
      <link>https://dev.to/salahxd/publish-once-own-the-original-4nng</link>
      <guid>https://dev.to/salahxd/publish-once-own-the-original-4nng</guid>
      <description>&lt;p&gt;Most developer writing gets read on someone else's website. DEV, Hashnode, Medium, a Bluesky post, a Hacker News thread. That's where the readers are, and your own site is often the last place they land.&lt;/p&gt;

&lt;p&gt;I'm fine with that. What I'm not fine with is each of those copies competing with the original in search, because the canonical link was never set, was set slightly wrong, or was set once and never checked again.&lt;/p&gt;

&lt;p&gt;So I built castio. I publish on my own site the way I always have, push to git, and castio takes it from there. It waits until the post is actually live and shows me exactly what each platform will get. It posts where it can, hands me a ready-made kit where it can't, and then keeps checking that every copy still points home. The original is yours, and every copy should say so. That should hold every day after you post it, not just on the day itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cross-posting is easy. Cross-posting correctly is tedious.
&lt;/h2&gt;

&lt;p&gt;The fix for duplicate content is old and boring: a canonical link. A copy that says "the original lives over there" stops competing with the original. The problem is that every platform handles it differently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DEV&lt;/strong&gt; has a canonical URL field on each article.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Medium&lt;/strong&gt; and &lt;strong&gt;Hashnode&lt;/strong&gt; each have their own canonical / original-URL setting, in their own corner of their own UI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bluesky, LinkedIn, Hacker News and Reddit&lt;/strong&gt; don't copy the post at all. They're just posts that link to you.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that is hard. It's tedious, which is worse. Tedious work gets done by hand, a little differently each time, and then forgotten. A missing or slightly-off canonical doesn't throw an error or show a warning. It just sits there.&lt;/p&gt;

&lt;p&gt;And nobody goes back three months later to re-check.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7wd45z1i599sfw61y08s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7wd45z1i599sfw61y08s.png" alt="salahxd.dev as the original, with DEV, Hashnode and Medium carrying canonical links back to it, and Bluesky, LinkedIn, Hacker News and Reddit linking to it" width="799" height="372"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;salahxd.dev is the original. The copies point back to it, and the posts link to it.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The first thing castio caught was my own site
&lt;/h2&gt;

&lt;p&gt;Building castio's canonical check meant looking hard at my own site first, and it didn't pass.&lt;/p&gt;

&lt;p&gt;My site linked to posts without a trailing slash, but the canonical tag on each post used the URL &lt;em&gt;with&lt;/em&gt; one. So every post had two URLs, &lt;code&gt;/blog/some-post&lt;/code&gt; and &lt;code&gt;/blog/some-post/&lt;/code&gt;. A person would call those the same page. A crawler sees two different URLs.&lt;/p&gt;

&lt;p&gt;castio compares the live canonical against the expected URL exactly, so none of my posts would ever have been marked Ready until the two agreed. I fixed the site, and links now end in &lt;code&gt;/&lt;/code&gt;. The dashboard now says it right under the URL pattern field: "The trailing slash matters." I wrote that line for myself first.&lt;/p&gt;

&lt;p&gt;The other honest one: I have an older DEV article whose canonical I still need to fix by hand. castio only manages what it publishes, so that one's on me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Push, wait, approve, publish, verify
&lt;/h2&gt;

&lt;p&gt;Here's the whole flow, in the order it happens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Push.&lt;/strong&gt; I write the way I always have: an Astro site, posts as files in &lt;code&gt;src/content/blog&lt;/code&gt;, in a GitHub repo. castio is a GitHub App installed on that one repo with read-only Contents access. When I push, a webhook tells castio a post was added or changed. castio verifies the webhook signature before it trusts any of the payload.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wait.&lt;/strong&gt; A push isn't a deploy. castio keeps checking the live URL until the post is actually up &lt;em&gt;and&lt;/em&gt; its &lt;code&gt;&amp;lt;link rel="canonical"&amp;gt;&lt;/code&gt; matches the expected URL exactly. Only then is the article Ready. It never distributes a post that isn't live yet, so no cross-post ever points at a 404.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Approve.&lt;/strong&gt; The preview shows exactly what each platform will receive. Nothing goes out without my approval. I can send it now or schedule it, in my timezone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Publish.&lt;/strong&gt; Two platforms are fully automatic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DEV&lt;/strong&gt; goes through the API with &lt;code&gt;canonical_url&lt;/code&gt; set. I can publish or save as a draft, with an optional "Originally published at" footer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bluesky&lt;/strong&gt; gets a post with a link card, built from a template with &lt;code&gt;{title}&lt;/code&gt; and &lt;code&gt;{excerpt}&lt;/code&gt;. It connects with proper atproto OAuth, with no app passwords.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;LinkedIn, Hacker News, Reddit, Medium and Hashnode get copy-paste kits. castio prepares everything, then I post it and mark it done. I never set out to automate every platform. What I wanted was to know where every copy lives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verify.&lt;/strong&gt; After publishing, castio fetches every copy and checks that its canonical points back to the original. Then it checks again every day. If a copy drifts, it gets flagged as needing action and I get a notification, plus an email if I've turned that on.&lt;/p&gt;

&lt;p&gt;That last step is the one I actually care about. Everything before it saves me time. Verification is the part I'd never do myself.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F55tbmpo1ua88h6biul3f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F55tbmpo1ua88h6biul3f.png" alt="The castio pipeline: git push, GitHub webhook, castio waits until the post is live with a correct canonical, Ready, preview and approve, DEV and Bluesky posted automatically, copy-paste kits for the rest, every canonical verified and re-checked daily" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;From git push to verified copies. The only manual steps are approving, and posting the kits.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;One more boring detail I'm glad I handled early: secrets like the DEV API key are stored encrypted under a versioned keyring, so keys can be rotated, and they're never shown again once saved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failures are per platform, not all-or-nothing
&lt;/h2&gt;

&lt;p&gt;The easy way to build this is one big "distribute" job that either works or doesn't. That's the wrong shape. If Bluesky has a bad minute, DEV shouldn't care.&lt;/p&gt;

&lt;p&gt;So every platform gets its own publication with its own state:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The automatic path is &lt;code&gt;draft → queued → publishing → published → verified&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If publishing fails, it goes to &lt;code&gt;failed&lt;/code&gt;, then back to &lt;code&gt;queued&lt;/code&gt; for a retry.&lt;/li&gt;
&lt;li&gt;If something goes wrong while publishing, or later with a published or verified copy, it lands in &lt;code&gt;needs_action&lt;/code&gt;. From there it goes back to &lt;code&gt;queued&lt;/code&gt;, or on to &lt;code&gt;verified&lt;/code&gt; once it's sorted.&lt;/li&gt;
&lt;li&gt;Copy-paste kits take their own path: &lt;code&gt;draft → manual_pending → manual_done → verified&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Anything not sent yet can be &lt;code&gt;skipped&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Written down, that's a lot of states. In practice it means a failure is one row with one clear next step. I never have to wonder what did and didn't go out.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fthszvytj0pnyty0ygxxx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fthszvytj0pnyty0ygxxx.png" alt="The per-platform publication state machine, covering the automatic path, retries through failed, needs_action, the manual copy-paste path, and skipped" width="800" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;One state machine per platform. A failure on one never blocks the others.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  One Docker image, three jobs
&lt;/h2&gt;

&lt;p&gt;The stack is a pnpm + Turborepo monorepo. There's a NestJS API, a BullMQ worker, Postgres via Drizzle, and Redis. The dashboard is Next.js with the App Router and TanStack Query.&lt;/p&gt;

&lt;p&gt;The API and the worker ship as the same Docker image. So does the migration step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node dist/start.js api
node dist/start.js worker
node dist/start.js migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Migrations run on start behind a Postgres advisory lock, so two containers booting at the same moment never both try to migrate.&lt;/p&gt;

&lt;p&gt;The browser only ever talks to one origin. The dashboard lives on Vercel at castio.salahxd.dev and rewrites &lt;code&gt;/api/*&lt;/code&gt; to the API at castio-api.salahxd.dev. The API runs next to the worker on my VPS, deployed with Dokploy. The GitHub push webhook skips the dashboard and goes straight to the API domain.&lt;/p&gt;

&lt;p&gt;Tests run against real Postgres and Redis through Testcontainers, with no mocked database quietly agreeing with everything.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsi6czva4uahswmvuub2v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsi6czva4uahswmvuub2v.png" alt="castio deployment: the browser talks to the Next.js dashboard on Vercel, which rewrites /api/\* to the NestJS API on a VPS behind Dokploy and Traefik; the API and a BullMQ worker from the same image share Postgres and Redis; the GitHub webhook goes directly to the API" width="800" height="492"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The dashboard on Vercel, the API and worker on my VPS. One image, one browser origin.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Four things that broke on the way to production
&lt;/h2&gt;

&lt;p&gt;None of these were bugs in the logic. All of them showed up at deploy time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. GitHub webhooks are bigger than you'd expect.&lt;/strong&gt; A push webhook can be up to 25 MB. Express and Nest default to a 100 kb JSON body limit. I gave only the webhook route a 25 MB parser, and every other route keeps 100 kb. The gotcha is that Nest silently skips registering its own body parsers if it finds a middleware function named &lt;code&gt;jsonParser&lt;/code&gt;. So the wrapper got a different name.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Multi-line secrets don't survive every env editor.&lt;/strong&gt; I pasted the GitHub App's PEM private key into Dokploy's env editor, and only the first line survived. To its credit, Octokit's error said exactly that: the key "contains only the first line". The fix was to store it on one line with literal &lt;code&gt;\n&lt;/code&gt; and let the config turn those back into real newlines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Certificates can race your DNS.&lt;/strong&gt; Let's Encrypt tried to issue the API's certificate before the DNS record existed. It got NXDOMAIN, and Traefik fell back to serving its default cert. Nothing retries on its own. Once DNS resolves, you have to nudge Traefik into trying again, which in Dokploy means re-saving the domain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Next.js rewrites are decided at build time.&lt;/strong&gt; Rewrites are baked in when you build, so the API URL has to exist during the build. Turborepo's strict env mode silently drops any env var not declared in &lt;code&gt;turbo.json&lt;/code&gt;. One line fixed it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"API_INTERNAL_URL"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three of those four failed quietly, with no crash and no warning. Things just weren't right. Silent drift is exactly the problem castio exists to catch in canonicals.&lt;/p&gt;

&lt;h2&gt;
  
  
  This post is the first one castio ships
&lt;/h2&gt;

&lt;p&gt;castio is v0. I built it for my own site first, and that's the only site it runs on. If it earns its keep here, it might turn into something other developers can use. No promises.&lt;/p&gt;

&lt;p&gt;If you're reading this on DEV or Bluesky, castio put it there, and the canonical points back to salahxd.dev. View the source if you like. castio already checked it, and it'll check again tomorrow.&lt;/p&gt;

&lt;p&gt;If you'd want something like this for your own blog, tell me. That's the only signal I'm looking for.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://salahxd.dev/blog/castio-publish-once/" rel="noopener noreferrer"&gt;salahxd.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>developertools</category>
      <category>blogging</category>
      <category>seo</category>
      <category>nestjs</category>
    </item>
    <item>
      <title>Meet Archify</title>
      <dc:creator>Mohd Salahudeen</dc:creator>
      <pubDate>Wed, 01 Jul 2026 19:31:45 +0000</pubDate>
      <link>https://dev.to/salahxd/meet-archify-3j57</link>
      <guid>https://dev.to/salahxd/meet-archify-3j57</guid>
      <description>&lt;p&gt;A couple of weeks ago I wrote that Archify was coming and asked people to join a waitlist. That post ended with "coming soon."&lt;/p&gt;

&lt;p&gt;This one doesn't. Archify is live. It's a Chrome extension, you can install it right now, and it does the thing I promised: you hover over any web app and it tells you how the app is built — without a single byte leaving your browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ten seconds, not an afternoon
&lt;/h2&gt;

&lt;p&gt;The old routine for understanding an unfamiliar app is archaeology. Open DevTools, inspect elements, dig through the network tab, hunt for source maps, trace API calls across a dozen tabs until a mental model finally clicks.&lt;/p&gt;

&lt;p&gt;Archify collapses that into a hover. Install it, open any site, and move your cursor over the page. An overlay follows along, and for whatever you're pointing at it shows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;framework&lt;/strong&gt; behind it — React, Next.js, Vue, Angular, Svelte&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;component type&lt;/strong&gt; — dialog, menu, button, input, tooltip&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;UI library&lt;/strong&gt; — MUI, Ant, Bootstrap, Chakra, Radix&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;recent API calls&lt;/strong&gt; the page has made — method, path, status, latency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Click to lock the overlay in place so you can read it properly. Press &lt;code&gt;Escape&lt;/code&gt; to let it follow your cursor again. That's the whole interaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every claim comes with a confidence — and it never makes names up
&lt;/h2&gt;

&lt;p&gt;This is the part I care about most, and it's the reason Archify took longer than a weekend.&lt;/p&gt;

&lt;p&gt;Most detection tools guess confidently and are wrong just often enough to waste your time. Archify does the opposite. Every claim it makes carries a confidence score, and when it can't actually prove something, it says &lt;code&gt;unknown&lt;/code&gt; instead of inventing an answer.&lt;/p&gt;

&lt;p&gt;On a development build it can be specific. On a minified production bundle — where the component was named &lt;code&gt;a&lt;/code&gt; and the class is &lt;code&gt;css-1q2w3e&lt;/code&gt; — it will not fabricate a component name to look smart. It tells you what the runtime can actually prove and marks the rest honestly.&lt;/p&gt;

&lt;p&gt;My favorite example is shadcn/ui. At runtime it's just Radix primitives with Tailwind classes pasted in — genuinely indistinguishable from raw Radix once it's shipped. So Archify says &lt;strong&gt;Radix&lt;/strong&gt;, and offers shadcn only as a low-confidence hint with a note explaining why it can't be sure. A tool that admits what it doesn't know is worth more than one that's confidently wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The second tab I didn't mention last time: Security
&lt;/h2&gt;

&lt;p&gt;The waitlist post was all about architecture. Here's the part I saved: there's a second tab, and it changes the question from "how is this built?" to "what is this page doing to me?"&lt;/p&gt;

&lt;p&gt;Switch to &lt;strong&gt;Security&lt;/strong&gt; and, for the page you're on, you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Third-party scripts&lt;/strong&gt; — how many of the scripts running here come from someone other than the site itself&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outbound calls&lt;/strong&gt; — how many network requests go out, and to how many third-party domains&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Form and payment field access&lt;/strong&gt; — and this is the sharp one: which scripts are in a position to read your &lt;strong&gt;password&lt;/strong&gt;, &lt;strong&gt;card number&lt;/strong&gt;, or &lt;strong&gt;CVC&lt;/strong&gt; fields&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one works by watching which script attaches a listener to a sensitive input, and naming where that script came from. It's not an accusation — a script reading a field can be perfectly legitimate. It just makes visible something that is normally invisible: who is &lt;em&gt;technically able&lt;/em&gt; to see what you type. Point it at a checkout page you don't run and you may find that list longer than you'd like.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nothing leaves your browser
&lt;/h2&gt;

&lt;p&gt;Everything above happens locally. There is no cloud step, no server-side analysis, no source upload, no account, and no telemetry. Archify intercepts &lt;code&gt;fetch&lt;/code&gt; and &lt;code&gt;XHR&lt;/code&gt;, reads the framework globals, and inspects the DOM — all inside your tab — and renders the result right there.&lt;/p&gt;

&lt;p&gt;That isn't a privacy promise stapled onto the marketing. It's the architecture. There is no "other end" to send your code to, because I never built one. The systems you explore stay entirely yours.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's not in it yet
&lt;/h2&gt;

&lt;p&gt;Honesty cuts both ways, so: v1 is deliberately small. There's no AI "explain this app" mode, no architecture graph, and no exports. It's Chrome-only for now. Those are on the roadmap, not in the box — and I'd rather ship something small and honest than something impressive and vague.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install it
&lt;/h2&gt;

&lt;p&gt;If you've ever opened DevTools and wished the app would just explain itself, this is for you. It's free, it's local, and it installs in a click.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://archify.salahxd.dev" rel="noopener noreferrer"&gt;Install Archify at archify.salahxd.dev&lt;/a&gt; — then open any site, hover, and see what it's actually made of. Software shouldn't be a black box.&lt;/p&gt;

</description>
      <category>developertools</category>
      <category>browserextension</category>
      <category>security</category>
      <category>devtool</category>
    </item>
    <item>
      <title>See Through Any Web App</title>
      <dc:creator>Mohd Salahudeen</dc:creator>
      <pubDate>Mon, 22 Jun 2026 14:36:49 +0000</pubDate>
      <link>https://dev.to/salahxd/see-through-any-web-app-40cg</link>
      <guid>https://dev.to/salahxd/see-through-any-web-app-40cg</guid>
      <description>&lt;p&gt;For most of software's history, writing code was the bottleneck. Today, understanding it is.&lt;/p&gt;

&lt;p&gt;AI can generate thousands of lines in seconds. Frameworks abstract away the hard parts, libraries hide their internals, and a single page pulls in dependencies that almost nobody ever opens and reads. We got very good at producing software, and we did almost nothing to make it easier to understand. That gap is the problem I kept running into — so I built a tool for it: Archify.&lt;/p&gt;

&lt;h2&gt;
  
  
  The questions that should take seconds
&lt;/h2&gt;

&lt;p&gt;Every day, developers lose hours to questions that have exact answers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What component is rendering this?&lt;/li&gt;
&lt;li&gt;Where is this data coming from?&lt;/li&gt;
&lt;li&gt;Which API powers this feature?&lt;/li&gt;
&lt;li&gt;What third-party scripts are running on this page?&lt;/li&gt;
&lt;li&gt;What can actually reach user data?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The answers all exist. They're just buried under layers of abstraction — scattered across DevTools panels, source maps, network tabs, and a repository you may not even have access to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding software shouldn't feel like archaeology
&lt;/h2&gt;

&lt;p&gt;When you land on an unfamiliar app, the routine is always the same. Open DevTools. Inspect elements. Dig through network requests. Search the source maps. Trace the API calls. Jump between docs, repos, and a dozen browser tabs until a mental model finally clicks into place.&lt;/p&gt;

&lt;p&gt;The information was never the hard part. Modern tooling is excellent at exposing implementation details — the individual requests, the minified bundles, the raw responses. It's far worse at exposing architecture: how those pieces actually connect. That's the part you end up reconstructing by hand, every single time.&lt;/p&gt;

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

&lt;p&gt;Archify is a browser extension that shows you what's actually happening behind any web app. Hover over an element and you instantly see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The component rendering it&lt;/li&gt;
&lt;li&gt;The framework or library behind it&lt;/li&gt;
&lt;li&gt;The API connections powering it&lt;/li&gt;
&lt;li&gt;The scripts running on the page&lt;/li&gt;
&lt;li&gt;How those parts relate to each other&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of spending an afternoon tracing behavior across tabs and files, you get the architecture directly — not just what exists, but how it fits together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Built for understanding, not surveillance
&lt;/h2&gt;

&lt;p&gt;Most developer tools start from the assumption that your data should leave your machine. Archify doesn't. It runs entirely inside your browser:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No cloud processing&lt;/li&gt;
&lt;li&gt;No server-side analysis&lt;/li&gt;
&lt;li&gt;No source code uploads&lt;/li&gt;
&lt;li&gt;No accounts&lt;/li&gt;
&lt;li&gt;No hidden telemetry&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The systems you explore stay yours. That isn't a marketing line — it's the architecture. There's nothing on the other end to send your code to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters now
&lt;/h2&gt;

&lt;p&gt;The industry is mid-shift. Generating code is becoming nearly free, but generated code still has to be reviewed, dependencies still have to be trusted, and applications still have to be understood. The ability to build is accelerating. The ability to comprehend isn't. Every day, that gap widens.&lt;/p&gt;

&lt;p&gt;I think the next generation of developer tools won't be judged by how much code they can write. They'll be judged by how quickly they help you understand what already exists. That's what Archify is for: making the act of understanding a piece of software as fast as the act of using it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coming soon
&lt;/h2&gt;

&lt;p&gt;Archify is getting ready for launch, and I'm inviting early users in now. If you've ever opened DevTools and wished the app would just explain itself, you're exactly who I'm building this for.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://archify.salahxd.dev" rel="noopener noreferrer"&gt;Join the waitlist at archify.salahxd.dev&lt;/a&gt; — be among the first to try it, and help shape what it becomes. Because software shouldn't be a black box.&lt;/p&gt;

</description>
      <category>developertools</category>
      <category>extensions</category>
      <category>devtools</category>
      <category>webdev</category>
    </item>
    <item>
      <title>My Site Has No Database, But It Has a CMS, Comments, Likes, and Views</title>
      <dc:creator>Mohd Salahudeen</dc:creator>
      <pubDate>Thu, 11 Jun 2026 18:30:00 +0000</pubDate>
      <link>https://dev.to/salahxd/this-site-has-no-database-but-it-has-a-cms-comments-likes-and-views-317b</link>
      <guid>https://dev.to/salahxd/this-site-has-no-database-but-it-has-a-cms-comments-likes-and-views-317b</guid>
      <description>&lt;p&gt;The title isn't a trick. My &lt;a href="https://salahxd.dev" rel="noopener noreferrer"&gt;site&lt;/a&gt; has a full CMS with an admin panel at &lt;code&gt;/keystatic&lt;/code&gt;, real comments under every post, a like button that remembers you, and view counters that tick up. And there is no database behind any of it — at least, none that I run.&lt;/p&gt;

&lt;p&gt;No server I deploy, no database I provisioned. I've never written a migration for this site, never tuned a connection pool, and there's no disk that can fill up at 3am and page me.&lt;/p&gt;

&lt;p&gt;That last part was the whole design goal. I'm building a startup, and whatever attention I have left over is not going toward babysitting infrastructure for a personal blog. So I gave myself one rule when building this: if a feature needs me to &lt;em&gt;operate&lt;/em&gt; something, it doesn't ship.&lt;/p&gt;

&lt;p&gt;What surprised me is how little I had to give up. The whole thing builds to static files and sits on a CDN — but every piece of state you can touch here still gets saved somewhere durable. The state didn't disappear. I just stopped owning the places where it lands.&lt;/p&gt;

&lt;p&gt;This is not a genius-architecture post. Most of it is me being lazy in a useful direction. But it works well enough that people ask about it, so here's how each piece fits together.&lt;/p&gt;

&lt;h2&gt;
  
  
  The CMS is just git
&lt;/h2&gt;

&lt;p&gt;The blog runs on &lt;a href="https://keystatic.com/" rel="noopener noreferrer"&gt;Keystatic&lt;/a&gt;. When I open &lt;code&gt;/keystatic&lt;/code&gt; in production and hit save, it doesn't write to a database. It makes a commit to this repo through a GitHub App:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// keystatic.config.ts&lt;/span&gt;
&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DEV&lt;/span&gt;
  &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;local&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;github&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Salah-XD/personal-portfolio&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In dev it writes files to my local disk. In production it commits straight to GitHub. The post you're reading is a &lt;code&gt;.md&lt;/code&gt; file in &lt;code&gt;src/content/blog/&lt;/code&gt;. My &lt;code&gt;/now&lt;/code&gt; and &lt;code&gt;/uses&lt;/code&gt; pages are JSON files in the same repo, edited through the same admin UI.&lt;/p&gt;

&lt;p&gt;So my "content database" is the git history. Publishing is a commit. Fixing a typo is a commit. Undoing a bad edit is &lt;code&gt;git revert&lt;/code&gt;. Vercel sees the push and rebuilds. I get diffs, blame, and backups for free, from the version control I was going to use anyway.&lt;/p&gt;

&lt;p&gt;The catch: writes are slow in the way a deploy is slow. Hitting save kicks off a rebuild, so "publish" takes about a minute. For a blog, I genuinely don't care. If that minute bothered me, this whole setup would be the wrong choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The comments are GitHub Discussions in a trench coat
&lt;/h2&gt;

&lt;p&gt;The comment box at the bottom of each post is &lt;a href="https://giscus.app/" rel="noopener noreferrer"&gt;Giscus&lt;/a&gt;, a thin shell over GitHub Discussions. Each post maps to a Discussion thread in the repo. When you comment, you're posting to GitHub, signed in as yourself.&lt;/p&gt;

&lt;p&gt;So my "comments table" is a Discussions tab, my spam protection is GitHub's, and my auth is OAuth somebody else built. If the env vars aren't wired up, the component renders a placeholder instead of breaking:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{giscusReady ? (
  &amp;lt;Comments ... /&amp;gt;
) : (
  &amp;lt;p&amp;gt;Comments will appear here once Giscus is configured.&amp;lt;/p&amp;gt;
)}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's a real downside here and I want to be upfront about it: you need a GitHub account to comment. For a blog where the readers are mostly developers, that filter costs me almost nothing and kills spam dead. If I were writing for a general audience, it would be a terrible choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Okay, confession: the like button does touch a server
&lt;/h2&gt;

&lt;p&gt;The likes and views from the title are the one kind of state on this site I couldn't fake at build time — a view happens when a person shows up, and there's no way around counting it live. So those two features hit &lt;a href="https://upstash.com/" rel="noopener noreferrer"&gt;Upstash Redis&lt;/a&gt; through the only two routes on the site that aren't static:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prerender&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// this route becomes a Vercel Function&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;incr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`likes:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the entire data model. Two integer keys per post — &lt;code&gt;likes:slug&lt;/code&gt; and &lt;code&gt;views:slug&lt;/code&gt; — and the only operation is &lt;code&gt;INCR&lt;/code&gt;. No schema, no ORM, no tables. Deduping is a cookie, not a query: 30 minutes for views, a year for likes. Someone clearing their cookies can like a post twice. I can live with that.&lt;/p&gt;

&lt;p&gt;And the Redis layer is optional. If the keys aren't set, it degrades instead of crashing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/redis.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;KV_REST_API_URL&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;UPSTASH_REDIS_REST_URL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;KV_REST_API_TOKEN&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;UPSTASH_REDIS_REST_TOKEN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;_redis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Redis&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;_redis&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;_redis&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isRedisConfigured&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;_redis&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So yes — strictly speaking, "no backend" is false. There are two serverless functions and a managed key-value store. But Upstash speaks plain HTTP and bills per request, so there's nothing to keep alive between visitors. I provisioned it by clicking a button on a marketplace page. I have never operated it. That feels meaningfully different from "running a database," even if a pedant would disagree.&lt;/p&gt;

&lt;h2&gt;
  
  
  Everything else happens before anyone visits
&lt;/h2&gt;

&lt;p&gt;The rest of what looks dynamic is work done once, at build time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Search&lt;/strong&gt; is &lt;a href="https://pagefind.app/" rel="noopener noreferrer"&gt;Pagefind&lt;/a&gt; — it indexes the built HTML and ships a static index the browser queries. No search server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The OG image&lt;/strong&gt; on every post is rendered with satori + resvg during the build. No screenshot service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RSS and the sitemap&lt;/strong&gt; are just generated files.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a request comes in, almost none of this code runs. It already ran, once, on my machine and on Vercel's build servers.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this actually costs me
&lt;/h2&gt;

&lt;p&gt;None of this is free, it's just paid in a different currency:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No instant writes.&lt;/strong&gt; Publishing is a deploy. Fine for a blog, useless for anything interactive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No queries.&lt;/strong&gt; I can increment a counter. I can't ask "which posts did people who liked X also read." If I ever want that, Redis with two keys per post isn't the answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More vendors, not fewer.&lt;/strong&gt; Instead of one database I run, I depend on GitHub, Upstash, Vercel, and Buttondown for the newsletter. My bet is that each of them runs their slice better than I'd run all of it — and if one of them dies or gets unbearable, I'm swapping one small piece, not migrating a monolith.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When I'd throw this whole thing out
&lt;/h2&gt;

&lt;p&gt;The moment this site needs user accounts, per-user data, anything transactional, or writes that have to show up in the same second — I'd set up a real database and not feel bad about it. This setup works &lt;em&gt;because&lt;/em&gt; a personal blog is read-heavy, write-rare, and has exactly one author. Those are narrow conditions.&lt;/p&gt;

&lt;p&gt;But they describe most personal sites and content sites. And for those, the best backend is the one you stop thinking about after the first week. Mine has been a rebuild and an &lt;code&gt;INCR&lt;/code&gt; ever since, and I haven't thought about it once.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>serverless</category>
      <category>opensource</category>
      <category>git</category>
    </item>
  </channel>
</rss>
