<?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: Arslan Ahmad</title>
    <description>The latest articles on DEV Community by Arslan Ahmad (@arslan3535).</description>
    <link>https://dev.to/arslan3535</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%2F4034992%2F8c52bea1-cd49-49c7-a33d-bb4f8d4c71c0.png</url>
      <title>DEV Community: Arslan Ahmad</title>
      <link>https://dev.to/arslan3535</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/arslan3535"/>
    <language>en</language>
    <item>
      <title>How We Built a Multi-Tenant SaaS Using SQLite and Node.js</title>
      <dc:creator>Arslan Ahmad</dc:creator>
      <pubDate>Sat, 18 Jul 2026 09:16:34 +0000</pubDate>
      <link>https://dev.to/arslan3535/how-we-built-a-multi-tenant-saas-using-sqlite-and-nodejs-16pa</link>
      <guid>https://dev.to/arslan3535/how-we-built-a-multi-tenant-saas-using-sqlite-and-nodejs-16pa</guid>
      <description>&lt;p&gt;When building a multi-tenant SaaS, the default industry advice is to boot up a distributed PostgreSQL cluster, configure complex connection pools, and manage microservices. But for a bootstrapped team, this architecture represents a massive developer tax and cash leak.&lt;/p&gt;

&lt;p&gt;For &lt;strong&gt;Fintasko&lt;/strong&gt; (our project and financial workspace OS), we took a different route: a single local SQLite database running on a low-cost VPS.&lt;/p&gt;

&lt;p&gt;Here is how we made SQLite safe, fast, and secure for a multi-tenant SaaS.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Engine: Node.js + better-sqlite3 + WAL Mode
&lt;/h2&gt;

&lt;p&gt;SQLite is often dismissed as a file-system toy. This is a mistake. When paired with &lt;code&gt;better-sqlite3&lt;/code&gt; and configured in Write-Ahead Logging (WAL) mode, SQLite easily handles concurrent read-write workloads.&lt;/p&gt;

&lt;p&gt;By default, SQLite locks the database file on writes. WAL mode changes this. It allows multiple readers to read the database while another process writes to it. We execute this during initialization:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Database&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;better-sqlite3&lt;/span&gt;&lt;span class="dl"&gt;'&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;db&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;Database&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;server/db/fintasko.sqlite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Enable WAL mode for high concurrency&lt;/span&gt;
&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pragma&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;journal_mode = WAL&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pragma&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;synchronous = NORMAL&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;This simple setup eliminates network latency. Because the database sits locally on the same SSD as our Node server, database queries execute in &lt;strong&gt;microseconds (under 0.1ms)&lt;/strong&gt; rather than the 5-15ms required to query a managed cloud database.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Multi-Tenant Security: Strict Workspace Scoping
&lt;/h2&gt;

&lt;p&gt;In a SaaS database, the biggest risk is data leakage. We enforce strict data isolation at the query level. Every table (projects, tasks, clients, invoices) contains a &lt;code&gt;workspace_id&lt;/code&gt; column.&lt;/p&gt;

&lt;p&gt;Every backend route is guarded by auth middleware that extracts the tenant's &lt;code&gt;workspace_id&lt;/code&gt; from their secure JWT session cookie. No database query is allowed to run without a &lt;code&gt;workspace_id = ?&lt;/code&gt; check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;tasks&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;project_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;workspace_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a user tries to query a task ID belonging to another company, the query returns nothing. We treat workspace-scoping as a security hard block.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Self-Healing Schema Migrations
&lt;/h2&gt;

&lt;p&gt;Deploying schema updates to production shouldn't require manual database operations. We built a self-healing migration layer. When the app boots, it executes a migration runner that auto-adds columns or tables if they don't exist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// server/db/migrate.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Database&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;better-sqlite3&lt;/span&gt;&lt;span class="dl"&gt;'&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;db&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;Database&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;server/db/fintasko.sqlite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Safe migrations&lt;/span&gt;
  &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ALTER TABLE blogs ADD COLUMN scheduled_at TEXT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Column already exists, ignore error&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This prevents database crashes during automated deployments and makes staging/production parity seamless.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Zero-Network Security Profile
&lt;/h2&gt;

&lt;p&gt;Because SQLite runs in-process, it does not open any network ports. There is no database port to attack, no firewall rule to misconfigure, and no network packet sniffer that can inspect query results. The standard filesystem permissions of our Ubuntu node protect the database file, providing enterprise-grade security with zero operational overhead.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Automated backups
&lt;/h2&gt;

&lt;p&gt;A single-file database makes backups simple. Instead of setting up replica nodes, we run a daily cron task that zips the sqlite file and sends it to offsite encrypted storage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;zip &lt;span class="nt"&gt;-r&lt;/span&gt; /backups/db-&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%F&lt;span class="si"&gt;)&lt;/span&gt;.zip /home/fintasko/fintasko/server/db/fintasko.sqlite
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;By avoiding distributed databases and focusing on SQLite, we reduced our hosting infrastructure cost to a &lt;strong&gt;$10/month cloud node&lt;/strong&gt; while delivering speeds that blow complex microservice setups out of the water. &lt;/p&gt;

&lt;p&gt;Check out &lt;a href="https://www.fintasko.com" rel="noopener noreferrer"&gt;Fintasko&lt;/a&gt; to see our work in action.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>database</category>
      <category>sqlite</category>
    </item>
  </channel>
</rss>
