<?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>Forem: Atul Srivastava</title>
    <description>The latest articles on Forem by Atul Srivastava (@imatulsrivas).</description>
    <link>https://forem.com/imatulsrivas</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%2F3858888%2Fa4d42c97-360d-47b0-a8dd-7b2fdaccd718.jpg</url>
      <title>Forem: Atul Srivastava</title>
      <link>https://forem.com/imatulsrivas</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/imatulsrivas"/>
    <language>en</language>
    <item>
      <title>Building BloomNest: A Full-Stack Childcare Management Platform with Angular 18 &amp; Appwrite</title>
      <dc:creator>Atul Srivastava</dc:creator>
      <pubDate>Mon, 06 Apr 2026 12:19:18 +0000</pubDate>
      <link>https://forem.com/imatulsrivas/building-bloomnest-a-full-stack-childcare-management-platform-with-angular-18-appwrite-199</link>
      <guid>https://forem.com/imatulsrivas/building-bloomnest-a-full-stack-childcare-management-platform-with-angular-18-appwrite-199</guid>
      <description>&lt;p&gt;Childcare centers run on spreadsheets, WhatsApp messages, and paper forms. I wanted to see if I could replace all of that with a single web app. The result is &lt;strong&gt;BloomNest&lt;/strong&gt; — a full-stack management platform for nurseries and kindergartens.&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://bloomnest-mu.vercel.app/" rel="noopener noreferrer"&gt;https://bloomnest-mu.vercel.app/&lt;/a&gt;&lt;br&gt;
📦 &lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/atul0016/BloomNest" rel="noopener noreferrer"&gt;https://github.com/atul0016/BloomNest&lt;/a&gt;&lt;/p&gt;




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

&lt;p&gt;BloomNest handles everything a childcare facility needs day-to-day:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Child enrollment&lt;/strong&gt; — profiles, groups, allergies, parent linkage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Staff management&lt;/strong&gt; — roles, status tracking, CRUD operations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smart scheduling&lt;/strong&gt; — weekly grid, shift blueprints, conflict detection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time tracking&lt;/strong&gt; — clock in/out, daily logs, monthly summaries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Absence management&lt;/strong&gt; — request/approve workflow with calendar view&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analytics dashboard&lt;/strong&gt; — charts for attendance trends and staff coverage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parent portal&lt;/strong&gt; — view-only access to their own child's data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Push notifications&lt;/strong&gt; — real-time alerts via Firebase Cloud Messaging&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dark mode&lt;/strong&gt; — because obviously&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Five distinct role dashboards: Admin, Board, Manager, Educator, and Parent — each seeing only what they need.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tech stack
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Frontend&lt;/td&gt;
&lt;td&gt;Angular 18 (standalone components)&lt;/td&gt;
&lt;td&gt;Strong typing, scalable architecture&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI&lt;/td&gt;
&lt;td&gt;Bootstrap 5.3 + Bootstrap Icons&lt;/td&gt;
&lt;td&gt;Fast, consistent layout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Charts&lt;/td&gt;
&lt;td&gt;Chart.js 4.4&lt;/td&gt;
&lt;td&gt;Lightweight, flexible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database + Auth storage&lt;/td&gt;
&lt;td&gt;Appwrite Cloud&lt;/td&gt;
&lt;td&gt;Real-time subscriptions, file storage, easy SDK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authentication&lt;/td&gt;
&lt;td&gt;Firebase Auth&lt;/td&gt;
&lt;td&gt;Reliable, free tier, easy social login&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Push notifications&lt;/td&gt;
&lt;td&gt;Firebase Cloud Messaging&lt;/td&gt;
&lt;td&gt;Battle-tested for web push&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hosting&lt;/td&gt;
&lt;td&gt;Vercel&lt;/td&gt;
&lt;td&gt;Zero-config Angular deploys&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Angular 18's standalone components (no NgModule) made the folder structure much cleaner. Lazy loading each feature module kept the initial bundle small.&lt;/p&gt;




&lt;h2&gt;
  
  
  The interesting architectural decisions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Splitting auth between Firebase and Appwrite
&lt;/h3&gt;

&lt;p&gt;Firebase handles identity (sign-in, password reset, token). Appwrite handles everything else — user profiles, role assignments, all app data. The two talk through a thin service layer that keeps the rest of the app unaware of which provider does what.&lt;/p&gt;

&lt;p&gt;This sounds like it adds complexity, but in practice it meant I could swap either provider independently. Firebase Auth's email verification flow is excellent; Appwrite's database permissions model is excellent. Both together = best of both worlds.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Role-based routing without a library
&lt;/h3&gt;

&lt;p&gt;Rather than pulling in a full RBAC library, I implemented guard-based routing using Angular's CanActivate. Each route declares which roles can access it. A RoleGuard checks the current user's role from a service and redirects if unauthorized.&lt;/p&gt;

&lt;p&gt;Simple, type-safe, zero extra dependencies.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Shift blueprints for scheduling efficiency
&lt;/h3&gt;

&lt;p&gt;Managers spend a lot of time building the same weekly schedule. I added "shift blueprints" — reusable shift templates that can be applied to any week with one click. It stores the pattern, not the instances, so changing a blueprint does not retroactively alter past schedules.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Real-time updates without polling
&lt;/h3&gt;

&lt;p&gt;Appwrite's real-time subscriptions push changes instantly to connected clients. When an admin approves an absence request, the educator's view updates without a page refresh. The subscription teardown on ngOnDestroy prevents memory leaks.&lt;/p&gt;




&lt;h2&gt;
  
  
  Challenges
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Appwrite permissions model&lt;/strong&gt; — Appwrite uses document-level permissions. Getting role-based read/write to work correctly (especially for the parent portal's read-only access to child documents) required careful collection design. I ended up using server-side functions for anything sensitive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chart.js with live data&lt;/strong&gt; — Chart.js does not automatically re-render when Angular's change detection runs. I had to call chart.update() manually after data changes, which meant tracking chart instances across components.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Angular 18 signals&lt;/strong&gt; — I started using Angular signals for reactive state in a few components. The mental model is cleaner than RxJS for simple derived values (computed()), but integrating signals with Appwrite's observable SDK took some bridging work.&lt;/p&gt;




&lt;h2&gt;
  
  
  Demo accounts
&lt;/h2&gt;

&lt;p&gt;You can log in and explore any role:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Email&lt;/th&gt;
&lt;th&gt;Password&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Admin&lt;/td&gt;
&lt;td&gt;&lt;a href="mailto:aria.thornewood@bloomnest.app"&gt;aria.thornewood@bloomnest.app&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;admin123&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manager&lt;/td&gt;
&lt;td&gt;&lt;a href="mailto:caspian.drake@bloomnest.app"&gt;caspian.drake@bloomnest.app&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;manager123&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Educator&lt;/td&gt;
&lt;td&gt;&lt;a href="mailto:elara.finch@bloomnest.app"&gt;elara.finch@bloomnest.app&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;educator123&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parent&lt;/td&gt;
&lt;td&gt;&lt;a href="mailto:diana.whitmore@bloomnest.app"&gt;diana.whitmore@bloomnest.app&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;parent123&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Export reports to PDF/Excel&lt;/li&gt;
&lt;li&gt;Mobile app (Ionic + Capacitor, same Angular codebase)&lt;/li&gt;
&lt;li&gt;Multi-facility support (one account, multiple locations)&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;If you're building with Angular 18 or Appwrite and have questions about any of the patterns above, drop them in the comments. Happy to go deeper on any section.&lt;/p&gt;

</description>
      <category>angular</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Building an Offline-First Multi-Tenant SaaS with React 19 and Dexie.js</title>
      <dc:creator>Atul Srivastava</dc:creator>
      <pubDate>Sat, 04 Apr 2026 15:40:40 +0000</pubDate>
      <link>https://forem.com/imatulsrivas/building-an-offline-first-multi-tenant-saas-with-react-19-and-dexiejs-4f1k</link>
      <guid>https://forem.com/imatulsrivas/building-an-offline-first-multi-tenant-saas-with-react-19-and-dexiejs-4f1k</guid>
      <description>&lt;p&gt;When I set out to build &lt;strong&gt;ParkManager&lt;/strong&gt; — a multi-tenant parking management SaaS — I knew "works offline" wasn't optional. Parking lots don't shut down when the internet goes out.&lt;/p&gt;

&lt;p&gt;Here's how I architected an offline-first, multi-tenant application using &lt;strong&gt;React 19&lt;/strong&gt;, &lt;strong&gt;Dexie.js&lt;/strong&gt; (IndexedDB wrapper), and &lt;strong&gt;Appwrite&lt;/strong&gt; as the cloud backend.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Most SaaS apps assume always-on connectivity. But in the real world:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Internet can be spotty at parking lots&lt;/li&gt;
&lt;li&gt;Attendants need to issue tickets regardless of connection&lt;/li&gt;
&lt;li&gt;Payment records can't be lost&lt;/li&gt;
&lt;li&gt;Multi-tenant data must stay isolated&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Stack Overview
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Frontend&lt;/td&gt;
&lt;td&gt;React 19 + Vite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local DB&lt;/td&gt;
&lt;td&gt;Dexie.js (IndexedDB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud Backend&lt;/td&gt;
&lt;td&gt;Appwrite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Desktop&lt;/td&gt;
&lt;td&gt;Electron&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PWA&lt;/td&gt;
&lt;td&gt;Service Workers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth&lt;/td&gt;
&lt;td&gt;Role-Based Access Control (5 roles)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Offline-First with Dexie.js
&lt;/h3&gt;

&lt;p&gt;The core idea: &lt;strong&gt;write locally first, sync to cloud when available&lt;/strong&gt;.&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;// Dexie database schema&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;Dexie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ParkManagerDB&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;version&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;stores&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;tickets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;++id, vehicleNumber, entryTime, exitTime, status, tenantId, synced&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;payments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;++id, ticketId, amount, method, tenantId, synced&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;users&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;++id, email, role, tenantId&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;Every record gets a &lt;code&gt;synced&lt;/code&gt; flag. When online, a background sync job pushes unsynced records to Appwrite:&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;syncToCloud&lt;/span&gt;&lt;span class="p"&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;unsynced&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tickets&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;synced&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toArray&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;for &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;ticket&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;unsynced&lt;/span&gt;&lt;span class="p"&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;appwrite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;databases&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createDocument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;DB_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;TICKETS_COLLECTION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ticket&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tickets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;synced&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&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;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Will retry on next sync cycle&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="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Run sync every 30 seconds when online&lt;/span&gt;
&lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onLine&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;syncToCloud&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;30000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Multi-Tenancy
&lt;/h3&gt;

&lt;p&gt;Each record includes a &lt;code&gt;tenantId&lt;/code&gt;. The Dexie queries always filter by tenant:&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getTickets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tickets&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tenantId&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reverse&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sortBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;entryTime&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the Appwrite side, collection-level permissions ensure tenants can only access their own data.&lt;/p&gt;

&lt;h3&gt;
  
  
  5 RBAC Roles
&lt;/h3&gt;

&lt;p&gt;ParkManager supports 5 distinct roles with different permissions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Owner&lt;/strong&gt; — Full system access, can manage tenants&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Admin&lt;/strong&gt; — Manage parking lots, users, and reports&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operator&lt;/strong&gt; — Day-to-day operations, view reports&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attendant&lt;/strong&gt; — Issue/close tickets, collect payments&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accountant&lt;/strong&gt; — View-only access to financial reports
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PERMISSIONS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;manage_lots&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;manage_users&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;view_reports&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;manage_tickets&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;operator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;manage_tickets&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;view_reports&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;attendant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;create_ticket&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;close_ticket&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;collect_payment&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;accountant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;view_reports&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;view_payments&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;hasPermission&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&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;perms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;PERMISSIONS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;perms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;perms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  PWA + Electron = Everywhere
&lt;/h3&gt;

&lt;p&gt;The same React codebase runs as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PWA&lt;/strong&gt; — installable on any device with a browser&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Electron app&lt;/strong&gt; — native desktop experience with system tray, auto-updates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web app&lt;/strong&gt; — standard browser access&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Electron wrapper is thin:&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;BrowserWindow&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;electron&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createWindow&lt;/span&gt;&lt;span class="p"&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;win&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;BrowserWindow&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;webPreferences&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;nodeIntegration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;contextIsolation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// In production, load the built React app&lt;/span&gt;
  &lt;span class="nx"&gt;win&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loadFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dist/index.html&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="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;whenReady&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;createWindow&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Offline-first isn't hard&lt;/strong&gt; — Dexie.js makes IndexedDB pleasant to work with&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sync conflicts are the real challenge&lt;/strong&gt; — use timestamps and "last write wins" for simple cases&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-tenancy at the data layer&lt;/strong&gt; — tenant isolation should be enforced at every query&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RBAC from day one&lt;/strong&gt; — retrofitting permissions is painful&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;React 19 + Vite&lt;/strong&gt; — blazing fast dev experience with HMR&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live Demo&lt;/strong&gt;: &lt;a href="https://park-my-vehicle.vercel.app" rel="noopener noreferrer"&gt;park-my-vehicle.vercel.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/atul0016/park-manager" rel="noopener noreferrer"&gt;github.com/atul0016/park-manager&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;If you're building SaaS products that need to work in unreliable network conditions, offline-first architecture is worth the investment. The user experience improvement is dramatic.&lt;/p&gt;

&lt;p&gt;What offline-first patterns have you used in your projects? Drop a comment below!&lt;/p&gt;

</description>
      <category>react</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How I Built a Full-Scale ERP System as a Solo Developer</title>
      <dc:creator>Atul Srivastava</dc:creator>
      <pubDate>Fri, 03 Apr 2026 08:30:22 +0000</pubDate>
      <link>https://forem.com/imatulsrivas/how-i-built-a-full-scale-erp-system-as-a-solo-developer-4dpj</link>
      <guid>https://forem.com/imatulsrivas/how-i-built-a-full-scale-erp-system-as-a-solo-developer-4dpj</guid>
      <description>&lt;p&gt;Most developers avoid ERP. It's complex, boring, and usually built by teams of 20+. I built one alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why?
&lt;/h2&gt;

&lt;p&gt;Indian small businesses run on disconnected spreadsheets, Tally, and WhatsApp. I wanted to build one clean desktop app that handles everything — finance, inventory, sales, purchase, GST, manufacturing, HRM, and reporting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Electron&lt;/strong&gt; — Desktop delivery (Windows)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;React + TypeScript&lt;/strong&gt; — UI layer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQLite&lt;/strong&gt; — Local-first data storage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node.js&lt;/strong&gt; — IPC handlers and service layer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why desktop + SQLite instead of a web app? Indian SMEs often have unreliable internet. A local-first app that just works, every time, was the right call.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Inside — 8 Modules
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Finance:&lt;/strong&gt; Chart of accounts, journal entries, general ledger, trial balance, P&amp;amp;L, balance sheet. Double-entry accounting built from scratch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inventory:&lt;/strong&gt; Item master, warehouse structure, stock movement, valuations, low-stock alerts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sales:&lt;/strong&gt; Customer management, sales orders, GST-compliant invoicing, receipt tracking, aging analysis.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Purchase:&lt;/strong&gt; Vendor management, purchase orders, goods receipt, purchase invoicing, payment tracking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Manufacturing:&lt;/strong&gt; Bill of materials, work centers, production orders, MRP planning, job work, quality control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HRM:&lt;/strong&gt; Employee records, attendance, leave management, payroll, tax declarations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GST &amp;amp; Compliance:&lt;/strong&gt; E-invoice, e-way bill, GSTR reports, ITC reconciliation, HSN summaries. Built for Indian tax workflows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reports:&lt;/strong&gt; Financial, sales, purchase, inventory, and GST reports with export options.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hard Parts
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Domain modeling
&lt;/h3&gt;

&lt;p&gt;ERP isn't one problem, it's 15 interconnected problems. A stock movement affects inventory valuations, which affects financial reports, which affects GST filings.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Double-entry accounting
&lt;/h3&gt;

&lt;p&gt;Getting the chart of accounts, journal entries, and ledger posting right took longer than any UI work.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. GST compliance
&lt;/h3&gt;

&lt;p&gt;Indian GST rules change constantly. Building a flexible structure that handles e-invoicing, reverse charges, and ITC was the most research-heavy part.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Keeping it simple
&lt;/h3&gt;

&lt;p&gt;The biggest temptation in ERP is feature creep. I forced myself to keep the UI clean: clear navigation, card-based actions, search-first header.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Build the data model first.&lt;/strong&gt; If your chart of accounts is wrong, everything downstream breaks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQLite is underrated.&lt;/strong&gt; For local-first business software, it's fast, reliable, and zero-config.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ERP is a product exercise, not just a coding exercise.&lt;/strong&gt; You need to understand business operations, not just API design.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ship module by module.&lt;/strong&gt; I built Finance first, then Inventory, then Sales — each one tested before moving on.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://sa-erp.netlify.app" rel="noopener noreferrer"&gt;sa-erp.netlify.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source code:&lt;/strong&gt; &lt;a href="https://github.com/atul0016/sa-erp" rel="noopener noreferrer"&gt;github.com/atul0016/sa-erp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Portfolio:&lt;/strong&gt; &lt;a href="https://beimatulportfolio.tech" rel="noopener noreferrer"&gt;beimatulportfolio.tech&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're thinking about building business software as a solo developer — do it. The domain complexity is what makes it valuable, both as a product and on your portfolio.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm Atul Srivastava, a full-stack developer building ERP systems, SaaS platforms, and Chrome extensions. Open to freelance and remote work. Reach me at &lt;a href="mailto:imatulsrivas@gmail.com"&gt;imatulsrivas@gmail.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>typescript</category>
      <category>electron</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
