<?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 (@salah-xd).</description>
    <link>https://dev.to/salah-xd</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2486134%2Fd0befe46-62f7-4b25-b6c2-e82f771ad077.jpeg</url>
      <title>DEV Community: Mohd Salahudeen</title>
      <link>https://dev.to/salah-xd</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/salah-xd"/>
    <language>en</language>
    <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/salah-xd/this-site-has-no-database-but-it-has-a-cms-comments-likes-and-views-317b</link>
      <guid>https://dev.to/salah-xd/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>
