<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://lovyjain.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://lovyjain.github.io/" rel="alternate" type="text/html" /><updated>2026-04-24T19:26:37+05:30</updated><id>https://lovyjain.github.io/feed.xml</id><title type="html">Lovy Jain</title><subtitle>LovyVerse is Lovy Jain&apos;s technical blog on practical enterprise AI, cloud architecture, and Microsoft platform engineering.</subtitle><author><name>Lovy Jain</name><email>lovyjain18@gmail.com</email></author><entry><title type="html">13 Lessons from 13 Years as a Microsoft Technology Architect</title><link href="https://lovyjain.github.io/career/architecture/13-years-lessons/" rel="alternate" type="text/html" title="13 Lessons from 13 Years as a Microsoft Technology Architect" /><published>2026-04-18T00:00:00+05:30</published><updated>2026-04-18T00:00:00+05:30</updated><id>https://lovyjain.github.io/career/architecture/13-years-lessons</id><content type="html" xml:base="https://lovyjain.github.io/career/architecture/13-years-lessons/"><![CDATA[<h2 id="introduction">Introduction</h2>

<p>Over the past 13 years, I’ve worked across multiple organizations and industries, building enterprise systems at scale.</p>

<p>Here are key lessons from that journey.</p>

<hr />

<h2 id="1-simplicity-scales-better-than-complexity">1. Simplicity scales better than complexity</h2>
<h2 id="2-understand-the-business-before-writing-code">2. Understand the business before writing code</h2>
<h2 id="3-architecture-is-about-trade-offs">3. Architecture is about trade-offs</h2>
<h2 id="4-performance-and-cost-go-hand-in-hand">4. Performance and cost go hand-in-hand</h2>
<h2 id="5-communication-is-as-important-as-technical-skills">5. Communication is as important as technical skills</h2>
<h2 id="6-always-design-for-change">6. Always design for change</h2>
<h2 id="7-real-world-systems-are-messy--embrace-it">7. Real-world systems are messy — embrace it</h2>
<h2 id="8-learn-continuously-especially-with-ai-evolving">8. Learn continuously (especially with AI evolving)</h2>
<h2 id="9-documentation-matters">9. Documentation matters</h2>
<h2 id="10-build-reusable-patterns">10. Build reusable patterns</h2>
<h2 id="11-test-assumptions-early">11. Test assumptions early</h2>
<h2 id="12-focus-on-impact-not-just-technology">12. Focus on impact, not just technology</h2>
<h2 id="13-build-something-of-your-own-like-jainkwiz">13. Build something of your own (like JainKwiz)</h2>

<hr />

<h2 id="conclusion">Conclusion</h2>

<p>Technology evolves, but fundamentals remain the same — clarity, simplicity, and purpose.</p>]]></content><author><name>Lovy Jain</name><email>lovyjain18@gmail.com</email></author><category term="career" /><category term="architecture" /><category term="Career" /><category term="Architecture" /><category term="Microsoft" /><category term="Enterprise" /><category term="Lessons Learned" /><summary type="html"><![CDATA[Distilled wisdom from 13 years building enterprise systems — on simplicity, trade-offs, communication, continuous learning, and building your own products.]]></summary></entry><entry><title type="html">Welcome to the New Site</title><link href="https://lovyjain.github.io/announcements/jekyll/welcome-to-the-new-site/" rel="alternate" type="text/html" title="Welcome to the New Site" /><published>2026-04-17T13:30:00+05:30</published><updated>2026-04-17T13:30:00+05:30</updated><id>https://lovyjain.github.io/announcements/jekyll/welcome-to-the-new-site</id><content type="html" xml:base="https://lovyjain.github.io/announcements/jekyll/welcome-to-the-new-site/"><![CDATA[<p>Welcome to the new website structure. This dummy post demonstrates a working Jekyll blog post with categories, tags, and rich content.</p>

<h2 id="example-content">Example content</h2>

<p>This post can be used as a template for future articles, and it shows how posts are rendered using the new theme layout.</p>

<ul>
  <li>Item 1</li>
  <li>Item 2</li>
</ul>

<p>Enjoy publishing content!</p>]]></content><author><name>Lovy Jain</name></author><category term="announcements" /><category term="jekyll" /><category term="jekyll" /><category term="minimal-mistakes" /><category term="sample" /><summary type="html"><![CDATA[A sample post showing the new blog structure and layout]]></summary></entry><entry><title type="html">Copilot Studio + D365: Building Intelligent CRM Experiences</title><link href="https://lovyjain.github.io/ai/dynamics365/copilot/copilot-d365/" rel="alternate" type="text/html" title="Copilot Studio + D365: Building Intelligent CRM Experiences" /><published>2026-04-17T00:00:00+05:30</published><updated>2026-04-17T00:00:00+05:30</updated><id>https://lovyjain.github.io/ai/dynamics365/copilot/copilot-d365</id><content type="html" xml:base="https://lovyjain.github.io/ai/dynamics365/copilot/copilot-d365/"><![CDATA[<h2 id="introduction">Introduction</h2>

<p>AI is rapidly transforming enterprise applications, and Microsoft Copilot Studio is at the center of this shift.</p>

<hr />

<h2 id="the-opportunity">The Opportunity</h2>

<p>Traditional CRM systems require manual navigation and data entry. Copilot introduces:</p>

<ul>
  <li>Natural language interactions</li>
  <li>Automated workflows</li>
  <li>Intelligent insights</li>
</ul>

<hr />

<h2 id="architecture-pattern">Architecture Pattern</h2>

<p>A typical setup includes:</p>

<ul>
  <li>Copilot Studio for conversational interface</li>
  <li>Dataverse for structured data</li>
  <li>Power Automate for workflows</li>
  <li>Azure OpenAI for intelligence</li>
</ul>

<hr />

<h2 id="use-cases">Use Cases</h2>

<ul>
  <li>Customer query resolution</li>
  <li>Automated case creation</li>
  <li>Intelligent recommendations</li>
</ul>

<hr />

<h2 id="key-benefits">Key Benefits</h2>

<ul>
  <li>Reduced manual effort</li>
  <li>Improved user experience</li>
  <li>Faster decision-making</li>
</ul>

<hr />

<h2 id="conclusion">Conclusion</h2>

<p>Copilot + D365 is not just an enhancement — it’s a shift toward AI-first enterprise applications.</p>]]></content><author><name>Lovy Jain</name><email>lovyjain18@gmail.com</email></author><category term="ai" /><category term="dynamics365" /><category term="copilot" /><category term="Copilot Studio" /><category term="Dynamics 365" /><category term="Azure OpenAI" /><category term="Power Automate" /><category term="Dataverse" /><category term="Enterprise AI" /><summary type="html"><![CDATA[How Copilot Studio, Dataverse, Power Automate, and Azure OpenAI combine to create AI-first CRM interactions that reduce manual effort and improve decision-making.]]></summary></entry><entry><title type="html">SPFx Development Best Practices: Hard-Won Lessons from Production SharePoint Projects</title><link href="https://lovyjain.github.io/sharepoint/spfx/microsoft365/spfx-best-practices/" rel="alternate" type="text/html" title="SPFx Development Best Practices: Hard-Won Lessons from Production SharePoint Projects" /><published>2026-04-15T00:00:00+05:30</published><updated>2026-04-15T00:00:00+05:30</updated><id>https://lovyjain.github.io/sharepoint/spfx/microsoft365/spfx-best-practices</id><content type="html" xml:base="https://lovyjain.github.io/sharepoint/spfx/microsoft365/spfx-best-practices/"><![CDATA[<h2 id="introduction">Introduction</h2>

<p>SharePoint Framework (SPFx) is the modern way to extend SharePoint Online, Microsoft Teams, and Microsoft Viva Connections. After building SPFx solutions for clients including global financial organisations, logistics companies, and pharmaceutical firms — ranging from simple web parts to complex intranet platforms — I have a clear set of practices that separate production-quality work from prototype-quality work.</p>

<p>This post covers what I actually do in projects, not what the documentation suggests.</p>

<hr />

<h2 id="project-structure-that-scales">Project Structure That Scales</h2>

<p>Most tutorials show you a single web part in a single file. Real projects have multiple web parts, shared components, custom hooks, and services. Your structure needs to support this from day one.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>src/
├── webparts/
│   ├── myDashboard/
│   │   ├── MyDashboardWebPart.ts      # SPFx entry point
│   │   ├── components/
│   │   │   ├── MyDashboard.tsx        # Root component
│   │   │   ├── MyDashboard.module.scss
│   │   │   └── widgets/
│   │   │       ├── AnnouncementWidget.tsx
│   │   │       └── TaskWidget.tsx
│   │   └── hooks/
│   │       └── useDashboardData.ts
│   └── myForm/
│       └── ...
├── shared/
│   ├── components/               # Reusable UI components
│   │   ├── LoadingSpinner.tsx
│   │   ├── ErrorBoundary.tsx
│   │   └── UserCard.tsx
│   ├── hooks/                    # Shared custom hooks
│   │   ├── useGraphClient.ts
│   │   └── usePnP.ts
│   ├── services/                 # API and data services
│   │   ├── GraphService.ts
│   │   ├── SharePointService.ts
│   │   └── CacheService.ts
│   ├── models/                   # TypeScript interfaces
│   │   ├── IUser.ts
│   │   └── IAnnouncement.ts
│   └── utils/
│       ├── dateUtils.ts
│       └── permissions.ts
└── extensions/
    └── ...
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">shared/</code> folder is the key. Any code used by more than one web part lives there. This prevents duplication and makes updates — especially to API services — a single change rather than a hunt across multiple web parts.</p>

<hr />

<h2 id="typescript--use-it-properly">TypeScript — Use It Properly</h2>

<p>SPFx has always supported TypeScript, but many codebases I inherit treat it like JavaScript with optional types. This misses the core benefit: catching errors at build time, not at runtime in production.</p>

<p><strong>Rules I enforce:</strong></p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// tsconfig.json additions</span>
<span class="p">{</span>
  <span class="dl">"</span><span class="s2">compilerOptions</span><span class="dl">"</span><span class="p">:</span> <span class="p">{</span>
    <span class="dl">"</span><span class="s2">strict</span><span class="dl">"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>           <span class="c1">// Enable all strict checks</span>
    <span class="dl">"</span><span class="s2">noImplicitAny</span><span class="dl">"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>    <span class="c1">// No implicit any types</span>
    <span class="dl">"</span><span class="s2">strictNullChecks</span><span class="dl">"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="c1">// Null and undefined must be handled explicitly</span>
    <span class="dl">"</span><span class="s2">noUnusedLocals</span><span class="dl">"</span><span class="p">:</span> <span class="kc">true</span>    <span class="c1">// Catch dead code</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong>Model every API response:</strong></p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Don't do this</span>
<span class="kd">const</span> <span class="nx">getData</span> <span class="o">=</span> <span class="k">async </span><span class="p">():</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="kr">any</span><span class="o">&gt;</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>

<span class="c1">// Do this</span>
<span class="kr">interface</span> <span class="nx">ISharePointListItem</span> <span class="p">{</span>
  <span class="nl">Id</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span>
  <span class="nl">Title</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
  <span class="nl">Author</span><span class="p">:</span> <span class="p">{</span> <span class="na">Title</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">EMail</span><span class="p">:</span> <span class="kr">string</span> <span class="p">};</span>
  <span class="nl">Created</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
  <span class="nl">Modified</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
<span class="p">}</span>

<span class="kd">const</span> <span class="nx">getData</span> <span class="o">=</span> <span class="k">async </span><span class="p">():</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="nx">ISharePointListItem</span><span class="p">[]</span><span class="o">&gt;</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>
</code></pre></div></div>

<p>When a SharePoint column is renamed or removed, TypeScript will tell you at compile time. Without types, you find out when a production user sees <code class="language-plaintext highlighter-rouge">undefined</code> in their browser.</p>

<hr />

<h2 id="microsoft-graph-and-pnpjs-know-when-to-use-each">Microsoft Graph and PnPjs: Know When to Use Each</h2>

<p>This is a question I get asked constantly. Both can access SharePoint data — the difference is about what you are doing:</p>

<table>
  <thead>
    <tr>
      <th>Use Case</th>
      <th>Recommended</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>SharePoint lists, libraries, items</td>
      <td>PnPjs (much simpler API)</td>
    </tr>
    <tr>
      <td>User profiles, Groups, Teams</td>
      <td>Microsoft Graph</td>
    </tr>
    <tr>
      <td>Teams app context</td>
      <td>Microsoft Graph</td>
    </tr>
    <tr>
      <td>Permissions, roles, policies</td>
      <td>Microsoft Graph</td>
    </tr>
    <tr>
      <td>Caml/FetchXML queries</td>
      <td>PnPjs</td>
    </tr>
    <tr>
      <td>Files and folders</td>
      <td>PnPjs (wraps Graph internally)</td>
    </tr>
  </tbody>
</table>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// PnPjs: clean, typed, handles pagination automatically</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">spfi</span><span class="p">,</span> <span class="nx">SPFx</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@pnp/sp</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="dl">"</span><span class="s2">@pnp/sp/lists</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="dl">"</span><span class="s2">@pnp/sp/items</span><span class="dl">"</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">sp</span> <span class="o">=</span> <span class="nf">spfi</span><span class="p">().</span><span class="nf">using</span><span class="p">(</span><span class="nc">SPFx</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">context</span><span class="p">));</span>

<span class="kd">const</span> <span class="nx">items</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">sp</span><span class="p">.</span><span class="nx">web</span><span class="p">.</span><span class="nx">lists</span>
  <span class="p">.</span><span class="nf">getByTitle</span><span class="p">(</span><span class="dl">"</span><span class="s2">Announcements</span><span class="dl">"</span><span class="p">)</span>
  <span class="p">.</span><span class="nx">items</span>
  <span class="p">.</span><span class="nf">select</span><span class="p">(</span><span class="dl">"</span><span class="s2">Id</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">Title</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">Body</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">ExpiryDate</span><span class="dl">"</span><span class="p">)</span>
  <span class="p">.</span><span class="nf">filter</span><span class="p">(</span><span class="dl">"</span><span class="s2">ExpiryDate ge datetime'</span><span class="dl">"</span> <span class="o">+</span> <span class="k">new</span> <span class="nc">Date</span><span class="p">().</span><span class="nf">toISOString</span><span class="p">()</span> <span class="o">+</span> <span class="dl">"</span><span class="s2">'</span><span class="dl">"</span><span class="p">)</span>
  <span class="p">.</span><span class="nf">orderBy</span><span class="p">(</span><span class="dl">"</span><span class="s2">Created</span><span class="dl">"</span><span class="p">,</span> <span class="kc">false</span><span class="p">)</span>
  <span class="p">.</span><span class="nf">top</span><span class="p">(</span><span class="mi">10</span><span class="p">)();</span>

<span class="c1">// Microsoft Graph: for M365 user data</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">MSGraphClientV3</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@microsoft/sp-http</span><span class="dl">"</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">client</span><span class="p">:</span> <span class="nx">MSGraphClientV3</span> <span class="o">=</span> <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">context</span><span class="p">.</span><span class="nx">msGraphClientFactory</span><span class="p">.</span><span class="nf">getClient</span><span class="p">(</span><span class="dl">"</span><span class="s2">3</span><span class="dl">"</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">me</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">client</span><span class="p">.</span><span class="nf">api</span><span class="p">(</span><span class="dl">"</span><span class="s2">/me</span><span class="dl">"</span><span class="p">).</span><span class="nf">select</span><span class="p">(</span><span class="dl">"</span><span class="s2">displayName,mail,department</span><span class="dl">"</span><span class="p">).</span><span class="nf">get</span><span class="p">();</span>
</code></pre></div></div>

<hr />

<h2 id="performance-the-problems-nobody-talks-about">Performance: The Problems Nobody Talks About</h2>

<p>SPFx web parts run in an iFrame inside SharePoint. Every API call adds latency. Here is where performance is lost and how to recover it:</p>

<h3 id="problem-1-waterfall-api-calls">Problem 1: Waterfall API Calls</h3>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// BAD: sequential — each call waits for the previous</span>
<span class="kd">const</span> <span class="nx">user</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">getUser</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">items</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">getItems</span><span class="p">(</span><span class="nx">user</span><span class="p">.</span><span class="nx">id</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">metadata</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">getMetadata</span><span class="p">(</span><span class="nx">items</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">id</span><span class="p">);</span>

<span class="c1">// GOOD: parallel where possible</span>
<span class="kd">const</span> <span class="p">[</span><span class="nx">user</span><span class="p">,</span> <span class="nx">recentItems</span><span class="p">]</span> <span class="o">=</span> <span class="k">await</span> <span class="nb">Promise</span><span class="p">.</span><span class="nf">all</span><span class="p">([</span>
  <span class="nf">getUser</span><span class="p">(),</span>
  <span class="nf">getRecentItems</span><span class="p">()</span>
<span class="p">]);</span>
<span class="kd">const</span> <span class="nx">details</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">getDetails</span><span class="p">(</span><span class="nx">user</span><span class="p">.</span><span class="nx">id</span><span class="p">,</span> <span class="nx">recentItems</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">id</span><span class="p">);</span>
</code></pre></div></div>

<h3 id="problem-2-no-caching">Problem 2: No Caching</h3>

<p>SharePoint API calls from SPFx are expensive in latency terms. Cache aggressively for data that does not need to be real-time.</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">CacheService</span> <span class="p">{</span>
  <span class="k">private</span> <span class="k">static</span> <span class="nx">cache</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Map</span><span class="o">&lt;</span><span class="kr">string</span><span class="p">,</span> <span class="p">{</span> <span class="na">data</span><span class="p">:</span> <span class="nx">unknown</span><span class="p">;</span> <span class="nl">expiry</span><span class="p">:</span> <span class="kr">number</span> <span class="p">}</span><span class="o">&gt;</span><span class="p">();</span>
  
  <span class="k">static</span> <span class="k">async</span> <span class="kd">get</span><span class="o">&lt;</span><span class="nx">T</span><span class="o">&gt;</span><span class="p">(</span>
    <span class="nx">key</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> 
    <span class="nx">fetcher</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="nx">T</span><span class="o">&gt;</span><span class="p">,</span> 
    <span class="nx">ttlSeconds</span><span class="p">:</span> <span class="kr">number</span> <span class="o">=</span> <span class="mi">300</span>
  <span class="p">):</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="nx">T</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">cached</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">cache</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="nx">key</span><span class="p">);</span>
    <span class="k">if </span><span class="p">(</span><span class="nx">cached</span> <span class="o">&amp;&amp;</span> <span class="nx">cached</span><span class="p">.</span><span class="nx">expiry</span> <span class="o">&gt;</span> <span class="nb">Date</span><span class="p">.</span><span class="nf">now</span><span class="p">())</span> <span class="p">{</span>
      <span class="k">return</span> <span class="nx">cached</span><span class="p">.</span><span class="nx">data</span> <span class="kd">as </span><span class="nx">T</span><span class="p">;</span>
    <span class="p">}</span>
    
    <span class="kd">const</span> <span class="nx">data</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">fetcher</span><span class="p">();</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">cache</span><span class="p">.</span><span class="nf">set</span><span class="p">(</span><span class="nx">key</span><span class="p">,</span> <span class="p">{</span> <span class="nx">data</span><span class="p">,</span> <span class="na">expiry</span><span class="p">:</span> <span class="nb">Date</span><span class="p">.</span><span class="nf">now</span><span class="p">()</span> <span class="o">+</span> <span class="nx">ttlSeconds</span> <span class="o">*</span> <span class="mi">1000</span> <span class="p">});</span>
    <span class="k">return</span> <span class="nx">data</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// Usage</span>
<span class="kd">const</span> <span class="nx">announcements</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">CacheService</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span>
  <span class="dl">'</span><span class="s1">announcements</span><span class="dl">'</span><span class="p">,</span>
  <span class="p">()</span> <span class="o">=&gt;</span> <span class="nx">sp</span><span class="p">.</span><span class="nx">web</span><span class="p">.</span><span class="nx">lists</span><span class="p">.</span><span class="nf">getByTitle</span><span class="p">(</span><span class="dl">"</span><span class="s2">Announcements</span><span class="dl">"</span><span class="p">).</span><span class="nx">items</span><span class="p">.</span><span class="nf">top</span><span class="p">(</span><span class="mi">10</span><span class="p">)(),</span>
  <span class="mi">300</span> <span class="c1">// 5 minute cache</span>
<span class="p">);</span>
</code></pre></div></div>

<h3 id="problem-3-rendering-everything-on-load">Problem 3: Rendering Everything on Load</h3>

<p>Use lazy loading for below-the-fold content. SPFx web parts are often positioned low on long SharePoint pages — do not fetch data until the user scrolls to them.</p>

<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">IntersectionWrapper</span><span class="p">:</span> <span class="nx">React</span><span class="p">.</span><span class="nx">FC</span><span class="o">&lt;</span><span class="p">{</span> <span class="na">children</span><span class="p">:</span> <span class="nx">React</span><span class="p">.</span><span class="nx">ReactNode</span> <span class="p">}</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">({</span> <span class="nx">children</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="p">[</span><span class="nx">isVisible</span><span class="p">,</span> <span class="nx">setIsVisible</span><span class="p">]</span> <span class="o">=</span> <span class="nx">React</span><span class="p">.</span><span class="nf">useState</span><span class="p">(</span><span class="kc">false</span><span class="p">);</span>
  <span class="kd">const</span> <span class="nx">ref</span> <span class="o">=</span> <span class="nx">React</span><span class="p">.</span><span class="nx">useRef</span><span class="o">&lt;</span><span class="nx">HTMLDivElement</span><span class="o">&gt;</span><span class="p">(</span><span class="kc">null</span><span class="p">);</span>
  
  <span class="nx">React</span><span class="p">.</span><span class="nf">useEffect</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">observer</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">IntersectionObserver</span><span class="p">(</span>
      <span class="p">([</span><span class="nx">entry</span><span class="p">])</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="k">if </span><span class="p">(</span><span class="nx">entry</span><span class="p">.</span><span class="nx">isIntersecting</span><span class="p">)</span> <span class="nf">setIsVisible</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span> <span class="p">},</span>
      <span class="p">{</span> <span class="na">threshold</span><span class="p">:</span> <span class="mf">0.1</span> <span class="p">}</span>
    <span class="p">);</span>
    <span class="k">if </span><span class="p">(</span><span class="nx">ref</span><span class="p">.</span><span class="nx">current</span><span class="p">)</span> <span class="nx">observer</span><span class="p">.</span><span class="nf">observe</span><span class="p">(</span><span class="nx">ref</span><span class="p">.</span><span class="nx">current</span><span class="p">);</span>
    <span class="k">return </span><span class="p">()</span> <span class="o">=&gt;</span> <span class="nx">observer</span><span class="p">.</span><span class="nf">disconnect</span><span class="p">();</span>
  <span class="p">},</span> <span class="p">[]);</span>
  
  <span class="k">return</span> <span class="p">&lt;</span><span class="nt">div</span> <span class="na">ref</span><span class="p">=</span><span class="si">{</span><span class="nx">ref</span><span class="si">}</span><span class="p">&gt;</span><span class="si">{</span><span class="nx">isVisible</span> <span class="p">?</span> <span class="nx">children</span> <span class="p">:</span> <span class="p">&lt;</span><span class="nc">Skeleton</span> <span class="p">/&gt;</span><span class="si">}</span><span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;;</span>
<span class="p">};</span>
</code></pre></div></div>

<hr />

<h2 id="permissions-and-security-content-security-policy-csp">Permissions and Security Content Security Policy (CSP)</h2>

<p>SharePoint Online enforces a strict Content Security Policy (CSP). This breaks web parts that try to load external resources — scripts, fonts, images, API calls — without proper configuration.</p>

<p><strong>Common CSP violations I see in production:</strong></p>

<ul>
  <li>Inline styles set via JavaScript (<code class="language-plaintext highlighter-rouge">element.style.cssText = ...</code>)</li>
  <li>External Google Fonts loaded via <code class="language-plaintext highlighter-rouge">&lt;link&gt;</code> in web part HTML</li>
  <li>Direct calls to third-party APIs without proper CORS headers</li>
</ul>

<p><strong>Solutions:</strong></p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Use CSS modules or SCSS — never inline styles</span>
<span class="k">import</span> <span class="nx">styles</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./MyWebPart.module.scss</span><span class="dl">'</span><span class="p">;</span>
<span class="c1">// &lt;div className={styles.container}&gt;...</span>

<span class="c1">// Load external fonts via the SPFx loadScript API, not inline</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">SPComponentLoader</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@microsoft/sp-loader</span><span class="dl">"</span><span class="p">;</span>

<span class="nx">SPComponentLoader</span><span class="p">.</span><span class="nf">loadCss</span><span class="p">(</span><span class="dl">"</span><span class="s2">https://fonts.googleapis.com/css2?family=Inter:wght@400;600&amp;display=swap</span><span class="dl">"</span><span class="p">);</span>
</code></pre></div></div>

<p>For custom API endpoints, configure CORS properly on your Azure Function or API:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Azure Function CORS (via host.json or Azure Portal)</span>
<span class="c1">// host.json</span>
<span class="p">{</span>
  <span class="s">"extensions"</span><span class="p">:</span> <span class="p">{</span>
    <span class="s">"http"</span><span class="p">:</span> <span class="p">{</span>
      <span class="s">"routePrefix"</span><span class="p">:</span> <span class="s">"api"</span><span class="p">,</span>
      <span class="s">"cors"</span><span class="p">:</span> <span class="p">{</span>
        <span class="s">"allowedOrigins"</span><span class="p">:</span> <span class="p">[</span>
          <span class="s">"https://&lt;tenant&gt;.sharepoint.com"</span>
        <span class="p">]</span>
      <span class="p">}</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<hr />

<h2 id="error-handling-and-telemetry">Error Handling and Telemetry</h2>

<p>Production SPFx web parts must handle errors gracefully and surface actionable information. A blank white box with no explanation destroys user trust.</p>

<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">ErrorBoundary</span> <span class="kd">extends</span> <span class="nc">React</span><span class="p">.</span><span class="nx">Component</span><span class="o">&lt;</span>
  <span class="p">{</span> <span class="na">children</span><span class="p">:</span> <span class="nx">React</span><span class="p">.</span><span class="nx">ReactNode</span><span class="p">;</span> <span class="nl">webPartTitle</span><span class="p">:</span> <span class="kr">string</span> <span class="p">},</span>
  <span class="p">{</span> <span class="na">hasError</span><span class="p">:</span> <span class="nx">boolean</span><span class="p">;</span> <span class="nl">error</span><span class="p">?:</span> <span class="nb">Error</span> <span class="p">}</span>
<span class="o">&gt;</span> <span class="p">{</span>
  <span class="nx">state</span> <span class="o">=</span> <span class="p">{</span> <span class="na">hasError</span><span class="p">:</span> <span class="kc">false</span> <span class="p">};</span>
  
  <span class="k">static</span> <span class="nf">getDerivedStateFromError</span><span class="p">(</span><span class="na">error</span><span class="p">:</span> <span class="nb">Error</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="p">{</span> <span class="na">hasError</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="nx">error</span> <span class="p">};</span>
  <span class="p">}</span>
  
  <span class="nf">componentDidCatch</span><span class="p">(</span><span class="na">error</span><span class="p">:</span> <span class="nb">Error</span><span class="p">,</span> <span class="na">info</span><span class="p">:</span> <span class="nx">React</span><span class="p">.</span><span class="nx">ErrorInfo</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// Log to Application Insights</span>
    <span class="nx">appInsights</span><span class="p">.</span><span class="nf">trackException</span><span class="p">({</span> 
      <span class="na">exception</span><span class="p">:</span> <span class="nx">error</span><span class="p">,</span>
      <span class="na">properties</span><span class="p">:</span> <span class="p">{</span> <span class="na">webPart</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">props</span><span class="p">.</span><span class="nx">webPartTitle</span><span class="p">,</span> <span class="na">componentStack</span><span class="p">:</span> <span class="nx">info</span><span class="p">.</span><span class="nx">componentStack</span> <span class="p">}</span>
    <span class="p">});</span>
  <span class="p">}</span>
  
  <span class="nf">render</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">if </span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">state</span><span class="p">.</span><span class="nx">hasError</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">return </span><span class="p">(</span>
        <span class="p">&lt;</span><span class="nt">div</span> <span class="na">className</span><span class="p">=</span><span class="si">{</span><span class="nx">styles</span><span class="p">.</span><span class="nx">errorState</span><span class="si">}</span><span class="p">&gt;</span>
          <span class="p">&lt;</span><span class="nc">Icon</span> <span class="na">iconName</span><span class="p">=</span><span class="s">"ErrorBadge"</span> <span class="p">/&gt;</span>
          <span class="p">&lt;</span><span class="nc">Text</span><span class="p">&gt;</span>Something went wrong loading <span class="si">{</span><span class="k">this</span><span class="p">.</span><span class="nx">props</span><span class="p">.</span><span class="nx">webPartTitle</span><span class="si">}</span>.<span class="p">&lt;/</span><span class="nc">Text</span><span class="p">&gt;</span>
          <span class="p">&lt;</span><span class="nc">Text</span> <span class="na">variant</span><span class="p">=</span><span class="s">"small"</span><span class="p">&gt;</span>Please refresh the page or contact support.<span class="p">&lt;/</span><span class="nc">Text</span><span class="p">&gt;</span>
        <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
      <span class="p">);</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">props</span><span class="p">.</span><span class="nx">children</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>I connect every production SPFx solution to Azure Application Insights. Page load times, API call durations, and JavaScript exceptions are tracked automatically. When a client reports “the web part is broken,” I can pinpoint the cause before they finish the sentence.</p>

<hr />

<h2 id="deployment-and-solution-packaging">Deployment and Solution Packaging</h2>

<p><strong>Always use tenant-scoped deployment for shared components.</strong> Site-scoped deployment requires manual activation on every site — this is unmaintainable at scale.</p>

<p>In <code class="language-plaintext highlighter-rouge">package-solution.json</code>:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"solution"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ACME-Intranet"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"..."</span><span class="p">,</span><span class="w">
    </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1.0.0.0"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"includeClientSideAssets"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"skipFeatureDeployment"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">    </span><span class="err">//</span><span class="w"> </span><span class="err">Tenant-wide</span><span class="w"> </span><span class="err">deployment</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"paths"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"zippedPackage"</span><span class="p">:</span><span class="w"> </span><span class="s2">"solution/acme-intranet.sppkg"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><strong>Versioning</strong>: Increment the version in <code class="language-plaintext highlighter-rouge">package-solution.json</code> on every deployment. SharePoint uses this to decide whether to update cached assets. Without version bumps, users may see stale JavaScript long after you deploy a fix.</p>

<hr />

<h2 id="accessibility">Accessibility</h2>

<p>SharePoint intranets serve everyone — including users with accessibility needs. SPFx solutions are subject to WCAG 2.1 AA requirements at most enterprise clients.</p>

<p>Non-negotiables I include in every project:</p>

<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Keyboard navigation and ARIA labels</span>
<span class="p">&lt;</span><span class="nt">button</span>
  <span class="na">aria-label</span><span class="p">=</span><span class="s">"Open announcements panel"</span>
  <span class="na">onClick</span><span class="p">=</span><span class="si">{</span><span class="nx">openPanel</span><span class="si">}</span>
  <span class="na">onKeyDown</span><span class="p">=</span><span class="si">{</span><span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">e</span><span class="p">.</span><span class="nx">key</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">Enter</span><span class="dl">'</span> <span class="o">&amp;&amp;</span> <span class="nf">openPanel</span><span class="p">()</span><span class="si">}</span>
<span class="p">&gt;</span>
  <span class="p">&lt;</span><span class="nc">Icon</span> <span class="na">iconName</span><span class="p">=</span><span class="s">"News"</span> <span class="na">aria-hidden</span><span class="p">=</span><span class="s">"true"</span> <span class="p">/&gt;</span>
<span class="p">&lt;/</span><span class="nt">button</span><span class="p">&gt;</span>

<span class="c1">// Focus management when panels open</span>
<span class="nx">React</span><span class="p">.</span><span class="nf">useEffect</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">if </span><span class="p">(</span><span class="nx">isPanelOpen</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">panelTitleRef</span><span class="p">.</span><span class="nx">current</span><span class="p">?.</span><span class="nf">focus</span><span class="p">();</span>
  <span class="p">}</span>
<span class="p">},</span> <span class="p">[</span><span class="nx">isPanelOpen</span><span class="p">]);</span>

<span class="c1">// Sufficient colour contrast — use Fluent UI theme tokens</span>
<span class="c1">// Don't hard-code colors; use var(--neutralPrimary) etc.</span>
</code></pre></div></div>

<p>Run Accessibility Insights for Web (Microsoft’s free tool) on every web part before release. It catches the majority of issues before a manual audit.</p>

<hr />

<h2 id="a-note-on-keeping-up">A Note on Keeping Up</h2>

<p>SPFx evolves rapidly. The team ships major framework updates that change how properties, contexts, and APIs work. The mistakes I see most often in inherited projects:</p>

<ul>
  <li>Using deprecated <code class="language-plaintext highlighter-rouge">sp-pnp-js</code> v2 (current is <code class="language-plaintext highlighter-rouge">@pnp/sp</code> v4)</li>
  <li>Using the old <code class="language-plaintext highlighter-rouge">GraphHttpClient</code> instead of <code class="language-plaintext highlighter-rouge">MSGraphClientV3</code></li>
  <li>Not updating the SPFx yeoman generator before scaffolding new projects</li>
</ul>

<p>Sign up for the <a href="https://devblogs.microsoft.com/microsoft365dev/">Microsoft 365 Developer Blog</a> and the <a href="https://learn.microsoft.com/en-us/sharepoint/dev/spfx/roadmap">SPFx release notes</a>. SPFx updates rarely break existing solutions — but staying current means accessing performance improvements and new APIs as they ship.</p>

<hr />

<h2 id="conclusion">Conclusion</h2>

<p>SPFx development at enterprise scale requires disciplined structure, typed APIs, performance awareness, and production-grade error handling from the start. The shortcuts that seem convenient during development always surface as maintenance problems after go-live.</p>

<p>The best SPFx codebase I have worked on had these properties: a clear shared component library, every API call cached appropriately, full TypeScript coverage, and Application Insights on every production deployment. When something broke, we found it in our monitoring before the client found it in production. That is the standard to aim for.</p>]]></content><author><name>Lovy Jain</name><email>lovyjain18@gmail.com</email></author><category term="sharepoint" /><category term="spfx" /><category term="microsoft365" /><category term="SPFx" /><category term="SharePoint Online" /><category term="React" /><category term="TypeScript" /><category term="M365" /><category term="Microsoft Graph" /><category term="Performance" /><summary type="html"><![CDATA[Project structure, TypeScript discipline, performance patterns, CSP handling, and deployment practices for enterprise-grade SharePoint Framework solutions.]]></summary></entry><entry><title type="html">JainKwiz: What Building a SaaS Product Taught Me That Enterprise Work Never Did</title><link href="https://lovyjain.github.io/product/saas/azure/jainkwiz-saas-journey/" rel="alternate" type="text/html" title="JainKwiz: What Building a SaaS Product Taught Me That Enterprise Work Never Did" /><published>2026-04-14T00:00:00+05:30</published><updated>2026-04-14T00:00:00+05:30</updated><id>https://lovyjain.github.io/product/saas/azure/jainkwiz-saas-journey</id><content type="html" xml:base="https://lovyjain.github.io/product/saas/azure/jainkwiz-saas-journey/"><![CDATA[<h2 id="introduction">Introduction</h2>

<p>After 13 years of building enterprise software for other people’s organisations, I did something that felt both obvious and terrifying: I built something of my own.</p>

<p><a href="https://jainkwiz.com">JainKwiz</a> is a gamified learning platform for Jain philosophy — real-time multiplayer quiz tournaments, leaderboards, AI-assisted learning, and a progressive web app that runs beautifully on any device. It is live, it has real users, and it has taught me more about software product thinking in one year than most of my enterprise career combined.</p>

<p>This is the story of that build — the architecture choices, the surprises, and the lessons that changed how I think about software.</p>

<hr />

<h2 id="why-jainkwiz">Why JainKwiz?</h2>

<p>The Jain community has a rich tradition of religious education through Pathshalas (community schools). Children learn Jain scripture, history, philosophy, and practice — but mostly through textbooks, rote memorisation, and periodic tests. Engagement is a constant challenge, especially for younger generations growing up with smartphones and short attention spans.</p>

<p>I saw a real problem: meaningful content, motivated teachers, but a delivery mechanism stuck in the 1980s. The technology to fix this already existed. I just needed to build the bridge.</p>

<p>The mission became clear: make Jain education as engaging as Duolingo and as competitive as a game, while preserving the depth and authenticity of the tradition.</p>

<hr />

<h2 id="architecture-decisions-and-why-i-made-them">Architecture Decisions (and Why I Made Them)</h2>

<h3 id="frontend-react--ionic">Frontend: React + Ionic</h3>

<p>I chose React with Ionic because I needed one codebase that could serve a mobile-quality experience on both iOS-like and Android-like surfaces, plus the web — without going the full React Native route with its native build complexity.</p>

<p>Ionic gives you native-feeling UI components (bottom tabs, swipeable sheets, haptic feedback patterns) that compile as a Progressive Web App. Users install JainKwiz to their home screen and get an experience that feels native. No app store friction. No review delays. Instant updates.</p>

<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Tournament lobby with real-time participant list</span>
<span class="kd">const</span> <span class="nx">TournamentLobby</span> <span class="o">=</span> <span class="p">({</span> <span class="nx">tournamentId</span> <span class="p">}:</span> <span class="nx">Props</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="p">{</span> <span class="nx">participants</span><span class="p">,</span> <span class="nx">isConnected</span> <span class="p">}</span> <span class="o">=</span> <span class="nf">useSignalR</span><span class="p">(</span><span class="nx">tournamentId</span><span class="p">);</span>
  
  <span class="k">return </span><span class="p">(</span>
    <span class="p">&lt;</span><span class="nc">IonPage</span><span class="p">&gt;</span>
      <span class="p">&lt;</span><span class="nc">IonContent</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nc">ParticipantList</span> <span class="na">participants</span><span class="p">=</span><span class="si">{</span><span class="nx">participants</span><span class="si">}</span> <span class="p">/&gt;</span>
        <span class="p">&lt;</span><span class="nc">ConnectionStatus</span> <span class="na">connected</span><span class="p">=</span><span class="si">{</span><span class="nx">isConnected</span><span class="si">}</span> <span class="p">/&gt;</span>
        <span class="si">{</span><span class="nx">isHost</span> <span class="o">&amp;&amp;</span> <span class="p">&lt;</span><span class="nc">StartTournamentButton</span> <span class="na">tournamentId</span><span class="p">=</span><span class="si">{</span><span class="nx">tournamentId</span><span class="si">}</span> <span class="p">/&gt;</span><span class="si">}</span>
      <span class="p">&lt;/</span><span class="nc">IonContent</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nc">IonPage</span><span class="p">&gt;</span>
  <span class="p">);</span>
<span class="p">};</span>
</code></pre></div></div>

<p><strong>What I would change</strong>: I underestimated the complexity of managing Ionic’s navigation stack alongside React Router. If I were starting again I would evaluate whether a simple React + Tailwind PWA would have been simpler to maintain without meaningfully sacrificing UX quality.</p>

<h3 id="real-time-azure-signalr-service">Real-time: Azure SignalR Service</h3>

<p>The live tournament feature — where 20 people simultaneously answer the same question within a 15-second window — required genuine real-time infrastructure. I evaluated WebSocket libraries, Pusher, Socket.io (self-hosted), and Azure SignalR.</p>

<p>Azure SignalR won because:</p>
<ul>
  <li>Serverless mode integrates cleanly with Azure Functions (no persistent server required)</li>
  <li>It scales automatically across WebSocket connections</li>
  <li>The Azure Functions trigger/binding system handles negotiation and message dispatch without boilerplate</li>
  <li>It fits the Azure Free tier for development and is cost-predictable in production</li>
</ul>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Azure Function: broadcast next question to tournament room</span>
<span class="p">[</span><span class="nf">FunctionName</span><span class="p">(</span><span class="s">"BroadcastQuestion"</span><span class="p">)]</span>
<span class="k">public</span> <span class="k">static</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">Run</span><span class="p">(</span>
    <span class="p">[</span><span class="nf">TimerTrigger</span><span class="p">(</span><span class="s">"*/15 * * * * *"</span><span class="p">)]</span> <span class="n">TimerInfo</span> <span class="n">timer</span><span class="p">,</span>
    <span class="p">[</span><span class="nf">SignalR</span><span class="p">(</span><span class="n">HubName</span> <span class="p">=</span> <span class="s">"tournament"</span><span class="p">)]</span> <span class="n">IAsyncCollector</span><span class="p">&lt;</span><span class="n">SignalRMessage</span><span class="p">&gt;</span> <span class="n">signalRMessages</span><span class="p">,</span>
    <span class="n">ILogger</span> <span class="n">log</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">activeTournaments</span> <span class="p">=</span> <span class="k">await</span> <span class="n">_tournamentService</span><span class="p">.</span><span class="nf">GetActiveAsync</span><span class="p">();</span>
    
    <span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">tournament</span> <span class="k">in</span> <span class="n">activeTournaments</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">nextQuestion</span> <span class="p">=</span> <span class="k">await</span> <span class="n">_questionService</span><span class="p">.</span><span class="nf">GetNextAsync</span><span class="p">(</span><span class="n">tournament</span><span class="p">.</span><span class="n">Id</span><span class="p">);</span>
        
        <span class="k">await</span> <span class="n">signalRMessages</span><span class="p">.</span><span class="nf">AddAsync</span><span class="p">(</span><span class="k">new</span> <span class="n">SignalRMessage</span>
        <span class="p">{</span>
            <span class="n">GroupName</span> <span class="p">=</span> <span class="n">tournament</span><span class="p">.</span><span class="n">Id</span><span class="p">,</span>
            <span class="n">Target</span> <span class="p">=</span> <span class="s">"nextQuestion"</span><span class="p">,</span>
            <span class="n">Arguments</span> <span class="p">=</span> <span class="k">new</span><span class="p">[]</span> <span class="p">{</span> <span class="n">nextQuestion</span> <span class="p">}</span>
        <span class="p">});</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="database-cosmos-db">Database: Cosmos DB</h3>

<p>I chose Cosmos DB because the data access patterns for JainKwiz are heavily read-biased and document-oriented:</p>

<ul>
  <li>Quiz questions with rich metadata (category, difficulty, language, tags)</li>
  <li>User profiles with gamification state (XP, level, streak, badges)</li>
  <li>Tournament sessions with participant lists and scores</li>
  <li>Leaderboard snapshots at weekly and global scopes</li>
</ul>

<p>The partition key design was the hardest decision. I ended up with:</p>

<table>
  <thead>
    <tr>
      <th>Container</th>
      <th>Partition Key</th>
      <th>Reasoning</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">questions</code></td>
      <td><code class="language-plaintext highlighter-rouge">/category</code></td>
      <td>Queries always filter by category</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">users</code></td>
      <td><code class="language-plaintext highlighter-rouge">/userId</code></td>
      <td>Single-user reads dominate</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">tournaments</code></td>
      <td><code class="language-plaintext highlighter-rouge">/tournamentId</code></td>
      <td>All operations scoped to one tournament</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">leaderboard</code></td>
      <td><code class="language-plaintext highlighter-rouge">/period</code></td>
      <td>Weekly vs. global isolation</td>
    </tr>
  </tbody>
</table>

<p>Cosmos DB’s free tier (1000 RU/s, 25GB) covers JainKwiz’s current scale comfortably. The serverless tier is an option for bursty workloads — I am evaluating the switch for the seasonal event peaks (Paryushana, Diwali).</p>

<h3 id="gamification-sm-2-spaced-repetition">Gamification: SM-2 Spaced Repetition</h3>

<p>One feature I am particularly proud of is the spaced repetition engine built on the SM-2 algorithm. Rather than showing users random questions, JainKwiz adapts to their performance — questions they struggle with appear more frequently; mastered questions space out over days and weeks.</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nf">calculateNextReview</span><span class="p">(</span><span class="nx">card</span><span class="p">,</span> <span class="nx">quality</span><span class="p">)</span> <span class="p">{</span>
  <span class="c1">// quality: 0-5 (0=blackout, 5=perfect)</span>
  <span class="k">if </span><span class="p">(</span><span class="nx">quality</span> <span class="o">&lt;</span> <span class="mi">3</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="p">{</span> <span class="na">interval</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="na">easeFactor</span><span class="p">:</span> <span class="nb">Math</span><span class="p">.</span><span class="nf">max</span><span class="p">(</span><span class="mf">1.3</span><span class="p">,</span> <span class="nx">card</span><span class="p">.</span><span class="nx">easeFactor</span> <span class="o">-</span> <span class="mf">0.2</span><span class="p">)</span> <span class="p">};</span>
  <span class="p">}</span>
  
  <span class="kd">const</span> <span class="nx">newEaseFactor</span> <span class="o">=</span> <span class="nx">card</span><span class="p">.</span><span class="nx">easeFactor</span> <span class="o">+</span> <span class="p">(</span><span class="mf">0.1</span> <span class="o">-</span> <span class="p">(</span><span class="mi">5</span> <span class="o">-</span> <span class="nx">quality</span><span class="p">)</span> <span class="o">*</span> <span class="p">(</span><span class="mf">0.08</span> <span class="o">+</span> <span class="p">(</span><span class="mi">5</span> <span class="o">-</span> <span class="nx">quality</span><span class="p">)</span> <span class="o">*</span> <span class="mf">0.02</span><span class="p">));</span>
  <span class="kd">const</span> <span class="nx">newInterval</span> <span class="o">=</span> <span class="nx">card</span><span class="p">.</span><span class="nx">repetitions</span> <span class="o">===</span> <span class="mi">0</span> <span class="p">?</span> <span class="mi">1</span>
    <span class="p">:</span> <span class="nx">card</span><span class="p">.</span><span class="nx">repetitions</span> <span class="o">===</span> <span class="mi">1</span> <span class="p">?</span> <span class="mi">6</span>
    <span class="p">:</span> <span class="nb">Math</span><span class="p">.</span><span class="nf">round</span><span class="p">(</span><span class="nx">card</span><span class="p">.</span><span class="nx">interval</span> <span class="o">*</span> <span class="nx">newEaseFactor</span><span class="p">);</span>
  
  <span class="k">return</span> <span class="p">{</span>
    <span class="na">interval</span><span class="p">:</span> <span class="nx">newInterval</span><span class="p">,</span>
    <span class="na">easeFactor</span><span class="p">:</span> <span class="nb">Math</span><span class="p">.</span><span class="nf">max</span><span class="p">(</span><span class="mf">1.3</span><span class="p">,</span> <span class="nx">newEaseFactor</span><span class="p">),</span>
    <span class="na">repetitions</span><span class="p">:</span> <span class="nx">card</span><span class="p">.</span><span class="nx">repetitions</span> <span class="o">+</span> <span class="mi">1</span><span class="p">,</span>
    <span class="na">nextReview</span><span class="p">:</span> <span class="nf">addDays</span><span class="p">(</span><span class="k">new</span> <span class="nc">Date</span><span class="p">(),</span> <span class="nx">newInterval</span><span class="p">)</span>
  <span class="p">};</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Users do not know they are interacting with an algorithm — they just feel like JainKwiz “knows” what they need to study. That invisibility is good product design.</p>

<hr />

<h2 id="what-enterprise-work-did-not-prepare-me-for">What Enterprise Work Did Not Prepare Me For</h2>

<h3 id="1-you-are-the-product-manager-designer-developer-and-support-team">1. You are the product manager, designer, developer, and support team</h3>

<p>In enterprise work, there is always a business analyst, a project manager, a dedicated designer, and a QA team. Building JainKwiz solo meant making every product decision myself — and living with the consequences.</p>

<p>The hardest decisions were not technical. They were:</p>
<ul>
  <li>Which features to build first (and which 20 features to say no to)</li>
  <li>How to structure the onboarding so a 12-year-old from Jalgaon can use it without instructions</li>
  <li>Whether to add Hindi language support immediately or defer it</li>
</ul>

<p>These decisions have no correct answer and no senior stakeholder to escalate to. You just decide, ship, and learn.</p>

<h3 id="2-real-users-are-brutally-honest">2. Real users are brutally honest</h3>

<p>Enterprise users are polite in feedback sessions. Real users either use your app or they do not. Retention numbers tell you the truth that user interviews sometimes hide.</p>

<p>My first version had a beautiful UI but the quiz flow had three taps too many before reaching the first question. Retention at day 3 was poor. I removed the friction, retention improved. No amount of stakeholder presentations would have taught me that — only shipping and measuring did.</p>

<h3 id="3-cost-is-not-a-rounding-error">3. Cost is not a rounding error</h3>

<p>In enterprise projects, cloud spend is somebody else’s budget line. When it is your Azure subscription and your credit card, every resource decision becomes real.</p>

<p>I rewrote my indexing pipeline three times to reduce Azure Function invocation costs. I switched from Azure Blob Storage triggers (which poll constantly) to Event Grid triggers (which fire only on change). I moved from Cosmos DB provisioned throughput to serverless to cut idle costs by 80%.</p>

<p>This cost-consciousness has made me a better architect for enterprise clients — I now challenge provisioned capacity assumptions that previously felt unimportant.</p>

<h3 id="4-content-is-as-hard-as-code">4. Content is as hard as code</h3>

<p>Building the question database was underestimated. Good quiz questions require:</p>
<ul>
  <li>Factual accuracy (which means expert review by community knowledge-holders)</li>
  <li>Appropriate difficulty calibration</li>
  <li>Language sensitivity (Jain terminology varies across regional traditions)</li>
  <li>Diversity of question format (not just multiple choice)</li>
</ul>

<p>I ended up building a separate question editor and review workflow — effectively a mini CMS — so trusted community contributors can submit and review questions. This content infrastructure took longer than the gamification engine.</p>

<hr />

<h2 id="the-ai-layer">The AI Layer</h2>

<p>JainKwiz has an AI chatbot powered by Azure OpenAI (GPT-4o) that users can ask questions about Jain philosophy. The chatbot is grounded on a curated knowledge base of Jain texts and explanations — a RAG implementation similar to what I described in my earlier post on SharePoint RAG pipelines.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>User: "What is the difference between Digambara and Shvetambara?"
Bot: [Retrieves relevant context from Jain knowledge base]
     "The Digambara and Shvetambara are the two main sects of Jainism..."
     [Cited from: Jain Philosophy Fundamentals, Chapter 3]
</code></pre></div></div>

<p>The grounding is critical — the general LLM has surface-level knowledge of Jainism but makes errors on specific philosophical points, canonical texts, and practice differences. The curated knowledge base corrects this.</p>

<hr />

<h2 id="jainkwiz-as-a-mirror">JainKwiz as a Mirror</h2>

<p>The most unexpected outcome of building JainKwiz has been what it taught me about my enterprise work.</p>

<p>When I design a feature for JainKwiz, I am the user. I feel the friction. I experience the delight. I understand why decisions matter. In enterprise work, the feedback loop is longer and often filtered through multiple layers.</p>

<p>Building JainKwiz made me a better architect. I question default assumptions more. I care more about what users actually experience rather than what the requirements document says. I design for change because I know I will need to change things.</p>

<p>If you are an enterprise developer considering building something of your own — I strongly encourage it. Pick a problem you care about, keep the scope small, and ship something real. The learning per hour invested will astonish you.</p>

<hr />

<h2 id="current-status-and-what-is-next">Current Status and What Is Next</h2>

<p>JainKwiz is live at <a href="https://jainkwiz.com">jainkwiz.com</a>. Current features:</p>

<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />Quiz engine with 500+ questions across 8 categories</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />XP and level progression system</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />Daily streaks and reminders</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />Global and weekly leaderboards</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />AI chatbot (beta)</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />Jain festival calendar</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />PWA (installable on home screen)</li>
</ul>

<p>In progress:</p>

<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Live multiplayer tournaments (SignalR backend complete, frontend integration underway)</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Hindi language support</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Pathshala classroom mode (teacher dashboard + student roster)</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Mobile apps on Play Store / App Store via Capacitor</li>
</ul>

<p>The classroom mode is the feature I am most excited about. It will let Pathshala teachers create assignments, track student progress, and run live class quiz sessions — bringing the technology directly into the community education system it was built to serve.</p>

<hr />

<h2 id="conclusion">Conclusion</h2>

<p>JainKwiz is my proof that an enterprise architect can build a real product, for real users, solving a real problem. It is not perfect — no product is. But it is live, it is growing, and it matters to the people who use it.</p>

<p>If you are from the Jain community and want to contribute questions, review content, or help with translations, please reach out. And if you want to know more about the technical architecture or the product journey, I would love to have that conversation.</p>]]></content><author><name>Lovy Jain</name><email>lovyjain18@gmail.com</email></author><category term="product" /><category term="saas" /><category term="azure" /><category term="JainKwiz" /><category term="SaaS" /><category term="Azure" /><category term="Product Development" /><category term="Entrepreneurship" /><category term="SignalR" /><category term="Cosmos DB" /><summary type="html"><![CDATA[The architecture, product decisions, and hard lessons from building a gamified Jain learning platform from zero to production as a solo founder.]]></summary></entry><entry><title type="html">Power Platform Governance at Scale: Lessons from Global Enterprise Deployments</title><link href="https://lovyjain.github.io/power-platform/governance/enterprise/power-platform-governance/" rel="alternate" type="text/html" title="Power Platform Governance at Scale: Lessons from Global Enterprise Deployments" /><published>2026-04-12T00:00:00+05:30</published><updated>2026-04-12T00:00:00+05:30</updated><id>https://lovyjain.github.io/power-platform/governance/enterprise/power-platform-governance</id><content type="html" xml:base="https://lovyjain.github.io/power-platform/governance/enterprise/power-platform-governance/"><![CDATA[<h2 id="introduction">Introduction</h2>

<p>The Power Platform democratises application development. That is both its greatest strength and its biggest operational challenge. After implementing governance frameworks for large global enterprises — including organisations with thousands of makers and hundreds of environments — I have seen what works and what creates expensive technical debt.</p>

<p>This post covers the governance patterns I rely on in production, moving from “anyone can build anything” chaos to a structured, secure, and scalable maker ecosystem.</p>

<hr />

<h2 id="why-governance-cannot-be-an-afterthought">Why Governance Cannot Be an Afterthought</h2>

<p>Most organisations discover the need for Power Platform governance only after something goes wrong:</p>

<ul>
  <li>A business-critical flow breaks and nobody knows who owns it</li>
  <li>Sensitive customer data flows through an unapproved connector to a personal OneDrive</li>
  <li>The default environment has 400 apps, 90% unmaintained, consuming capacity</li>
  <li>A maker leaves the company and their flows stop running because they were the only owner</li>
</ul>

<p>These are not hypothetical scenarios — I have seen all of them firsthand. The good news is that Microsoft provides comprehensive tooling to prevent them. The bad news is that tooling requires deliberate configuration and a governance model to back it up.</p>

<hr />

<h2 id="the-governance-pillars">The Governance Pillars</h2>

<p>A mature Power Platform governance framework rests on four pillars:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌─────────────────────────────────────────────────────────┐
│                  GOVERNANCE FRAMEWORK                   │
│                                                         │
│  1. Environment Strategy  │  2. Data Loss Prevention   │
│  ─────────────────────── │  ─────────────────────────  │
│  • Default isolation      │  • Connector classification │
│  • Dept environments      │  • Business / Non-Business  │
│  • Sandbox / Production   │  • Blocked connectors       │
│                           │                             │
│  3. CoE Starter Kit       │  4. ALM &amp; DevOps           │
│  ─────────────────────── │  ─────────────────────────  │
│  • Inventory &amp; visibility │  • Solution-based packaging │
│  • Compliance monitoring  │  • GitHub Actions CI/CD     │
│  • Maker enablement       │  • Managed environments     │
└─────────────────────────────────────────────────────────┘
</code></pre></div></div>

<hr />

<h2 id="pillar-1-environment-strategy">Pillar 1: Environment Strategy</h2>

<p>The most common mistake is allowing everything to happen in the default environment. This environment cannot be deleted and has relaxed defaults — it becomes a dumping ground.</p>

<p><strong>My recommended environment topology:</strong></p>

<table>
  <thead>
    <tr>
      <th>Environment</th>
      <th>Purpose</th>
      <th>DLP Policy</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Default</td>
      <td>Learning &amp; experimentation only</td>
      <td>Strict — no business connectors</td>
    </tr>
    <tr>
      <td>Department (per BU)</td>
      <td>Team solutions, shared resources</td>
      <td>Moderate</td>
    </tr>
    <tr>
      <td>Production</td>
      <td>Approved, monitored solutions</td>
      <td>Strict</td>
    </tr>
    <tr>
      <td>Developer (personal)</td>
      <td>Individual maker sandbox</td>
      <td>Moderate</td>
    </tr>
    <tr>
      <td>Sandbox</td>
      <td>Pre-production testing</td>
      <td>Mirrors production</td>
    </tr>
  </tbody>
</table>

<p><strong>Implementation via PowerShell / PAC CLI:</strong></p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Create a department environment with managed environment features</span><span class="w">
</span><span class="n">New-AdminPowerAppEnvironment</span><span class="w"> </span><span class="se">`
</span><span class="w">  </span><span class="nt">-DisplayName</span><span class="w"> </span><span class="s2">"Finance - Production"</span><span class="w"> </span><span class="se">`
</span><span class="w">  </span><span class="nt">-LocationName</span><span class="w"> </span><span class="s2">"unitedkingdom"</span><span class="w"> </span><span class="se">`
</span><span class="w">  </span><span class="nt">-EnvironmentSku</span><span class="w"> </span><span class="nx">Production</span><span class="w"> </span><span class="se">`
</span><span class="w">  </span><span class="nt">-ProvisionDatabase</span><span class="w"> </span><span class="bp">$true</span><span class="w"> </span><span class="se">`
</span><span class="w">  </span><span class="nt">-SecurityGroupId</span><span class="w"> </span><span class="nv">$financeGroupId</span><span class="w">
</span></code></pre></div></div>

<p>Managed Environments (premium) are essential at scale — they unlock usage insights, weekly digest emails to admins, and maker welcome content customisation.</p>

<hr />

<h2 id="pillar-2-data-loss-prevention-dlp-policies">Pillar 2: Data Loss Prevention (DLP) Policies</h2>

<p>DLP policies are the security backbone of the platform. They control which connectors can work together, preventing data from leaking between trusted and untrusted services.</p>

<p><strong>Three-tier classification:</strong></p>

<ul>
  <li><strong>Business</strong>: SharePoint, Outlook, Teams, Dataverse, Azure services — approved for business data</li>
  <li><strong>Non-Business</strong>: Personal OneDrive, Twitter, Google Sheets — cannot mix with Business connectors</li>
  <li><strong>Blocked</strong>: Connectors that should never be used (e.g., certain social media platforms, shadow IT services)</li>
</ul>

<p><strong>DLP Policy Design Principles I follow:</strong></p>

<ol>
  <li><strong>Layer policies</strong> — apply a tenant-wide base policy, then environment-specific overrides. The most restrictive policy wins.</li>
  <li><strong>Block HTTP connector in production</strong> — the HTTP connector is an escape hatch that bypasses DLP entirely. Block it except in developer environments with explicit exception process.</li>
  <li><strong>Audit before enforcing</strong> — use audit mode first to understand the blast radius of a new DLP policy before enforcing it.</li>
</ol>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"displayName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Global Base Policy"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"environments"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"filterType"</span><span class="p">:</span><span class="w"> </span><span class="s2">"AllEnvironments"</span><span class="w"> </span><span class="p">},</span><span class="w">
  </span><span class="nl">"connectorGroups"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="w">
      </span><span class="nl">"classification"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Confidential"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"connectors"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="p">{</span><span class="w"> </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/providers/Microsoft.PowerApps/apis/shared_sharepointonline"</span><span class="w"> </span><span class="p">},</span><span class="w">
        </span><span class="p">{</span><span class="w"> </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/providers/Microsoft.PowerApps/apis/shared_office365"</span><span class="w"> </span><span class="p">},</span><span class="w">
        </span><span class="p">{</span><span class="w"> </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/providers/Microsoft.PowerApps/apis/shared_commondataservice"</span><span class="w"> </span><span class="p">}</span><span class="w">
      </span><span class="p">]</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="p">{</span><span class="w">
      </span><span class="nl">"classification"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Blocked"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"connectors"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="p">{</span><span class="w"> </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/providers/Microsoft.PowerApps/apis/shared_twitter"</span><span class="w"> </span><span class="p">}</span><span class="w">
      </span><span class="p">]</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<hr />

<h2 id="pillar-3-centre-of-excellence-coe-starter-kit">Pillar 3: Centre of Excellence (CoE) Starter Kit</h2>

<p>The CoE Starter Kit is a collection of Power Apps, Power Automate flows, and Power BI dashboards that give admins full visibility into what is happening across the tenant.</p>

<p><strong>What it gives you out of the box:</strong></p>

<ul>
  <li><strong>Inventory app</strong>: Every app, flow, maker, and connector across all environments</li>
  <li><strong>Risk score</strong>: Automated risk assessment based on connector usage and sharing settings</li>
  <li><strong>Compliance process</strong>: Makers receive automated emails asking them to justify their apps</li>
  <li><strong>Maker onboarding</strong>: Guided training and self-service environment request process</li>
</ul>

<p><strong>What I customise in every deployment:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>CoE Customisations:
├── Custom risk scoring weights (adjust for your data sensitivity)
├── Integration with ServiceNow / Jira for environment request approvals
├── Power BI executive dashboard (tenant health at a glance)
├── Automated orphaned resource cleanup (flows with no owners)
└── Custom welcome email with internal training links
</code></pre></div></div>

<p>A well-configured CoE shifts the admin from reactive firefighting to proactive quality management.</p>

<hr />

<h2 id="pillar-4-alm-with-solutions-and-devops">Pillar 4: ALM with Solutions and DevOps</h2>

<p>Ungoverned apps are built directly in environments — no source control, no review, no deployment pipeline. The antidote is Solutions-based development combined with CI/CD.</p>

<p><strong>The golden rule: if it matters, it is in a solution.</strong></p>

<p>My standard ALM pipeline using GitHub Actions:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># .github/workflows/deploy-power-platform.yml</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">Deploy Power Platform Solution</span>

<span class="na">on</span><span class="pi">:</span>
  <span class="na">push</span><span class="pi">:</span>
    <span class="na">branches</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">main</span><span class="pi">]</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">export-and-deploy</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v4</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Install PAC CLI</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">microsoft/powerplatform-actions/actions-install@v1</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Export solution from dev</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">microsoft/powerplatform-actions/export-solution@v1</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">environment-url</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">app-id</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">client-secret</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">solution-name</span><span class="pi">:</span> <span class="s">MyEnterpriseApp</span>
          <span class="na">solution-output-file</span><span class="pi">:</span> <span class="s">solutions/MyEnterpriseApp.zip</span>
          <span class="na">managed</span><span class="pi">:</span> <span class="kc">false</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Unpack solution</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">microsoft/powerplatform-actions/unpack-solution@v1</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">solution-file</span><span class="pi">:</span> <span class="s">solutions/MyEnterpriseApp.zip</span>
          <span class="na">solution-folder</span><span class="pi">:</span> <span class="s">src/MyEnterpriseApp</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Import to production</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">microsoft/powerplatform-actions/import-solution@v1</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">environment-url</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">app-id</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">client-secret</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">solution-file</span><span class="pi">:</span> <span class="s">solutions/MyEnterpriseApp_managed.zip</span>
          <span class="na">managed</span><span class="pi">:</span> <span class="kc">true</span>
</code></pre></div></div>

<p><strong>Naming conventions I enforce:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Solution:  [OrgCode]_[Domain]_[AppName]_v[Major]
App:       [OrgCode]_[Domain]_[AppName]
Flow:      [OrgCode]_[Trigger]_[Action]_[Entity]
Table:     [orgcode]_[entityname]
Column:    [orgcode]_[columnname]
</code></pre></div></div>

<p>Consistent naming sounds trivial. After 18 months in a tenant with 500+ assets, it becomes the difference between a maintainable platform and an unmaintainable one.</p>

<hr />

<h2 id="operational-playbook-common-scenarios">Operational Playbook: Common Scenarios</h2>

<h3 id="scenario-1-maker-wants-to-use-an-unapproved-connector">Scenario 1: Maker wants to use an unapproved connector</h3>

<p><strong>Without governance</strong>: They figure out a workaround (HTTP connector, custom connector) or just use the free tier of the external service.</p>

<p><strong>With governance</strong>: Submit a connector exception request via the CoE portal → admin review within 2 business days → approved connectors added to the relevant DLP policy → maker gets a notification.</p>

<h3 id="scenario-2-critical-flow-breaks-because-the-owner-left">Scenario 2: Critical flow breaks because the owner left</h3>

<p><strong>Without governance</strong>: Production outage. Nobody knows the credentials. Flow runs under a personal account.</p>

<p><strong>With governance</strong>:</p>
<ul>
  <li>All flows use service principal connections (not personal accounts)</li>
  <li>All flows have at least two owners</li>
  <li>Orphan detection in CoE alerts admins when a maker leaves</li>
  <li>Automated ownership transfer workflow triggered by HR offboarding</li>
</ul>

<h3 id="scenario-3-capacity-planning-for-dataverse">Scenario 3: Capacity planning for Dataverse</h3>

<p><strong>Without governance</strong>: You hit the Dataverse storage limit. Emergency purchase under pressure.</p>

<p><strong>With governance</strong>:</p>
<ul>
  <li>CoE reports monthly on storage trends per environment</li>
  <li>Alerts at 70% capacity thresholds</li>
  <li>Automated cleanup of completed flow run history (largest hidden storage consumer)</li>
</ul>

<hr />

<h2 id="metrics-that-matter">Metrics That Matter</h2>

<p>After implementing governance, track these KPIs monthly:</p>

<table>
  <thead>
    <tr>
      <th>Metric</th>
      <th>What it Tells You</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>% flows with service principal connections</td>
      <td>Reliability of production automations</td>
    </tr>
    <tr>
      <td>% apps with 2+ owners</td>
      <td>Bus factor risk</td>
    </tr>
    <tr>
      <td>DLP policy violations (audit mode)</td>
      <td>Shadow IT indicator</td>
    </tr>
    <tr>
      <td>Orphaned resources</td>
      <td>Governance process health</td>
    </tr>
    <tr>
      <td>Maker count by environment type</td>
      <td>Is the default environment being misused?</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="the-cultural-side">The Cultural Side</h2>

<p>Technology alone does not create a governed platform. The hardest part is always change management:</p>

<ul>
  <li><strong>Run Power Platform Office Hours</strong> — 30 minutes weekly where makers can get help. This surfaces governance friction early.</li>
  <li><strong>Celebrate makers</strong> — a monthly “Power Platform showcase” where teams demo their apps builds positive culture around the platform.</li>
  <li><strong>Self-service, not bureaucracy</strong> — make environment requests and DLP exceptions easy and fast. If the process is painful, makers go around it.</li>
</ul>

<hr />

<h2 id="conclusion">Conclusion</h2>

<p>Power Platform governance is not about restricting innovation — it is about making innovation sustainable. The organisations that invest in governance early end up with a thriving, trusted maker ecosystem. Those that do not end up with a shadow IT problem disguised as digital transformation.</p>

<p>The pattern that works: strict defaults, easy exceptions, full visibility, and strong maker enablement. Start with the CoE Starter Kit, build your environment strategy, and layer DLP policies with care. The first 3 months of governance work will save years of remediation.</p>]]></content><author><name>Lovy Jain</name><email>lovyjain18@gmail.com</email></author><category term="power-platform" /><category term="governance" /><category term="enterprise" /><category term="Power Platform" /><category term="Power Apps" /><category term="Power Automate" /><category term="Governance" /><category term="CoE" /><category term="DLP" /><category term="Microsoft 365" /><summary type="html"><![CDATA[Lessons from implementing governance frameworks for global enterprises — environment strategy, DLP policies, CoE Starter Kit, and ALM with DevOps.]]></summary></entry><entry><title type="html">Building RAG Pipelines with Azure OpenAI and SharePoint</title><link href="https://lovyjain.github.io/azure/ai/sharepoint/azure-openai-sharepoint-rag/" rel="alternate" type="text/html" title="Building RAG Pipelines with Azure OpenAI and SharePoint" /><published>2026-04-10T00:00:00+05:30</published><updated>2026-04-10T00:00:00+05:30</updated><id>https://lovyjain.github.io/azure/ai/sharepoint/azure-openai-sharepoint-rag</id><content type="html" xml:base="https://lovyjain.github.io/azure/ai/sharepoint/azure-openai-sharepoint-rag/"><![CDATA[<h2 id="introduction">Introduction</h2>

<p>Retrieval-Augmented Generation (RAG) has emerged as the go-to pattern for grounding large language models in enterprise knowledge bases. After building several production RAG systems on Azure, I want to share the architecture decisions, pitfalls, and patterns that actually work in practice — specifically when SharePoint Online is your primary knowledge store.</p>

<hr />

<h2 id="why-rag--sharepoint-makes-sense">Why RAG + SharePoint Makes Sense</h2>

<p>Most enterprises already have years of institutional knowledge locked inside SharePoint — policies, procedures, project documentation, wikis, and more. The challenge has always been discoverability. People simply don’t know where to look, or the search results are too noisy to be useful.</p>

<p>RAG changes this fundamentally. Instead of keyword search, users ask natural language questions and get synthesised answers drawn directly from authoritative documents. When you pair this with SharePoint as the data source, you unlock:</p>

<ul>
  <li><strong>Existing governance</strong> — SharePoint permissions map naturally to RAG access control</li>
  <li><strong>Familiar content management</strong> — content owners keep managing content as they always have</li>
  <li><strong>M365 integration</strong> — surfaced inside Teams, Copilot Studio, or custom apps via Graph API</li>
</ul>

<hr />

<h2 id="the-architecture">The Architecture</h2>

<p>Here is the architecture I have deployed across multiple enterprise clients:</p>

<p><img src="/assets/images/posts/azure-openai-sharepoint-rag.svg" alt="Azure OpenAI + SharePoint RAG architecture" /></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>SharePoint Online
      │
      ▼
  MS Graph API  ←── delta queries for incremental sync
      │
      ▼
  Azure Function (Indexer)
      │
      ├── Chunk &amp; clean text (overlap sliding window)
      ├── Generate embeddings (Azure OpenAI text-embedding-3-large)
      └── Upsert to Azure AI Search (vector + keyword hybrid index)
                          │
                          ▼
              Copilot Studio / Custom App
                          │
                          ├── User Query → Embedding
                          ├── Hybrid Search (vector + BM25)
                          ├── Rerank top-k results
                          └── Azure OpenAI GPT-4o (grounded prompt)
                                      │
                                      ▼
                              Cited Answer to User
</code></pre></div></div>

<p><strong>Key components:</strong></p>

<table>
  <thead>
    <tr>
      <th>Component</th>
      <th>Purpose</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>MS Graph API</td>
      <td>Delta sync of SharePoint files</td>
    </tr>
    <tr>
      <td>Azure Function (Timer)</td>
      <td>Incremental indexing pipeline</td>
    </tr>
    <tr>
      <td>Azure AI Search</td>
      <td>Hybrid vector + keyword retrieval</td>
    </tr>
    <tr>
      <td>Azure OpenAI Embeddings</td>
      <td>text-embedding-3-large</td>
    </tr>
    <tr>
      <td>Azure OpenAI Chat</td>
      <td>GPT-4o for answer generation</td>
    </tr>
    <tr>
      <td>Semantic Kernel</td>
      <td>Orchestration layer</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="step-1--indexing-sharepoint-content">Step 1 — Indexing SharePoint Content</h2>

<p>The indexer runs as an Azure Function on a timer trigger. It uses the Microsoft Graph API <code class="language-plaintext highlighter-rouge">delta</code> endpoint to fetch only files changed since the last run — this keeps costs low and indexing fast.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Delta query to get changed SharePoint files</span>
<span class="kt">var</span> <span class="n">deltaQuery</span> <span class="p">=</span> <span class="k">await</span> <span class="n">_graphClient</span>
    <span class="p">.</span><span class="n">Sites</span><span class="p">[</span><span class="n">siteId</span><span class="p">]</span>
    <span class="p">.</span><span class="n">Drive</span><span class="p">.</span><span class="n">Root</span><span class="p">.</span><span class="n">Delta</span>
    <span class="p">.</span><span class="nf">GetAsDeltaGetResponseAsync</span><span class="p">(</span><span class="n">req</span> <span class="p">=&gt;</span>
    <span class="p">{</span>
        <span class="n">req</span><span class="p">.</span><span class="n">QueryParameters</span><span class="p">.</span><span class="n">Select</span> <span class="p">=</span> <span class="k">new</span><span class="p">[]</span> <span class="p">{</span> <span class="s">"id"</span><span class="p">,</span> <span class="s">"name"</span><span class="p">,</span> <span class="s">"lastModifiedDateTime"</span><span class="p">,</span> <span class="s">"file"</span> <span class="p">};</span>
    <span class="p">});</span>
</code></pre></div></div>

<p>For each changed file I:</p>

<ol>
  <li>Download the content (PDF, DOCX, PPTX supported via Document Intelligence)</li>
  <li>Split into overlapping chunks (600 tokens, 100 overlap — tuned empirically)</li>
  <li>Generate embeddings with <code class="language-plaintext highlighter-rouge">text-embedding-3-large</code></li>
  <li>Upsert into Azure AI Search with metadata (file name, URL, last modified, SharePoint permissions)</li>
</ol>

<hr />

<h2 id="step-2--chunking-strategy-matters">Step 2 — Chunking Strategy Matters</h2>

<p>This is where most RAG projects go wrong. Naive chunking by character count destroys semantic coherence. What I use:</p>

<ul>
  <li><strong>Semantic chunking</strong>: Split at paragraph and sentence boundaries, not arbitrary character counts</li>
  <li><strong>Overlapping windows</strong>: 15–20% overlap ensures context isn’t lost at boundaries</li>
  <li><strong>Metadata injection</strong>: Prepend document title and section heading to each chunk — this dramatically improves retrieval relevance</li>
  <li><strong>Chunk size</strong>: 500–800 tokens for SharePoint content (policy docs, wikis). Smaller for Q&amp;A content.</li>
</ul>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">chunk_document</span><span class="p">(</span><span class="n">text</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">title</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">section</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">[</span><span class="nb">str</span><span class="p">]:</span>
    <span class="sh">"""</span><span class="s">Semantic chunking with metadata prefix.</span><span class="sh">"""</span>
    <span class="n">sentences</span> <span class="o">=</span> <span class="nf">split_by_sentence</span><span class="p">(</span><span class="n">text</span><span class="p">)</span>
    <span class="n">chunks</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="n">current</span> <span class="o">=</span> <span class="sa">f</span><span class="sh">"</span><span class="s">Document: </span><span class="si">{</span><span class="n">title</span><span class="si">}</span><span class="se">\n</span><span class="s">Section: </span><span class="si">{</span><span class="n">section</span><span class="si">}</span><span class="se">\n\n</span><span class="sh">"</span>
    
    <span class="k">for</span> <span class="n">sentence</span> <span class="ow">in</span> <span class="n">sentences</span><span class="p">:</span>
        <span class="k">if</span> <span class="nf">token_count</span><span class="p">(</span><span class="n">current</span> <span class="o">+</span> <span class="n">sentence</span><span class="p">)</span> <span class="o">&gt;</span> <span class="n">MAX_CHUNK_TOKENS</span><span class="p">:</span>
            <span class="n">chunks</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">current</span><span class="p">.</span><span class="nf">strip</span><span class="p">())</span>
            <span class="c1"># Overlap: keep last sentence
</span>            <span class="n">current</span> <span class="o">=</span> <span class="sa">f</span><span class="sh">"</span><span class="s">Document: </span><span class="si">{</span><span class="n">title</span><span class="si">}</span><span class="se">\n</span><span class="s">Section: </span><span class="si">{</span><span class="n">section</span><span class="si">}</span><span class="se">\n\n</span><span class="si">{</span><span class="n">sentence</span><span class="si">}</span><span class="s"> </span><span class="sh">"</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="n">current</span> <span class="o">+=</span> <span class="n">sentence</span> <span class="o">+</span> <span class="sh">"</span><span class="s"> </span><span class="sh">"</span>
    
    <span class="k">if</span> <span class="n">current</span><span class="p">.</span><span class="nf">strip</span><span class="p">():</span>
        <span class="n">chunks</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">current</span><span class="p">.</span><span class="nf">strip</span><span class="p">())</span>
    
    <span class="k">return</span> <span class="n">chunks</span>
</code></pre></div></div>

<hr />

<h2 id="step-3--hybrid-retrieval-in-azure-ai-search">Step 3 — Hybrid Retrieval in Azure AI Search</h2>

<p>Pure vector search misses exact keyword matches. Pure keyword search misses semantic similarity. Hybrid search with Reciprocal Rank Fusion (RRF) gives the best of both worlds.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"search"</span><span class="p">:</span><span class="w"> </span><span class="s2">"leave policy maternity"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"vectorQueries"</span><span class="p">:</span><span class="w"> </span><span class="p">[{</span><span class="w">
    </span><span class="nl">"kind"</span><span class="p">:</span><span class="w"> </span><span class="s2">"vector"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"vector"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="err">/*</span><span class="w"> </span><span class="err">query</span><span class="w"> </span><span class="err">embedding</span><span class="w"> </span><span class="err">*/</span><span class="p">],</span><span class="w">
    </span><span class="nl">"fields"</span><span class="p">:</span><span class="w"> </span><span class="s2">"contentVector"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"k"</span><span class="p">:</span><span class="w"> </span><span class="mi">50</span><span class="w">
  </span><span class="p">}],</span><span class="w">
  </span><span class="nl">"queryType"</span><span class="p">:</span><span class="w"> </span><span class="s2">"semantic"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"semanticConfiguration"</span><span class="p">:</span><span class="w"> </span><span class="s2">"my-semantic-config"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"select"</span><span class="p">:</span><span class="w"> </span><span class="s2">"title, content, sourceUrl, permissions"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"top"</span><span class="p">:</span><span class="w"> </span><span class="mi">5</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">semanticConfiguration</code> enables Azure AI Search’s built-in re-ranking model, which re-scores the top 50 results using a cross-encoder. This two-stage retrieval (vector → rerank) consistently outperforms single-stage approaches.</p>

<hr />

<h2 id="step-4--permission-aware-retrieval">Step 4 — Permission-Aware Retrieval</h2>

<p>This is critical in enterprise settings and often overlooked in tutorials. Users must only get answers based on content they are permitted to see. I handle this by:</p>

<ol>
  <li>Storing SharePoint permission groups alongside each chunk in the index</li>
  <li>Passing the user’s group memberships (from Graph API) as a filter at query time</li>
</ol>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">filter</span> <span class="p">=</span> <span class="s">$"permissions/any(p: p eq '</span><span class="p">{</span><span class="kt">string</span><span class="p">.</span><span class="nf">Join</span><span class="p">(</span><span class="s">"' or p eq '"</span><span class="p">,</span> <span class="n">userGroups</span><span class="p">)}</span><span class="s">')"</span><span class="p">;</span>
</code></pre></div></div>

<p>This ensures the RAG system respects existing SharePoint governance — no re-implementation of access control needed.</p>

<hr />

<h2 id="step-5--grounded-prompt-engineering">Step 5 — Grounded Prompt Engineering</h2>

<p>The system prompt is critical. A few principles that work well:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>You are an enterprise knowledge assistant. Answer questions based ONLY on the 
provided context documents. 

Rules:
- Always cite the source document name and URL for every fact you state
- If the context does not contain enough information, say so clearly
- Never make up information or use knowledge outside the provided context
- Respond in the same language as the user's question
</code></pre></div></div>

<p>Forcing citations reduces hallucination and builds user trust. When the answer references a specific SharePoint page, users can click through to the source — this is the enterprise trust signal that matters most.</p>

<hr />

<h2 id="performance-and-cost-optimisation">Performance and Cost Optimisation</h2>

<p>After running these systems in production for several months:</p>

<ul>
  <li><strong>Embedding model</strong>: <code class="language-plaintext highlighter-rouge">text-embedding-3-large</code> over <code class="language-plaintext highlighter-rouge">ada-002</code> — better retrieval quality for approximately the same cost</li>
  <li><strong>Caching</strong>: Cache embeddings for unchanged chunks — the majority of re-indexing runs touch &lt;5% of content</li>
  <li><strong>Batch embedding</strong>: Use Azure OpenAI batch API for bulk indexing (60–70% cost reduction vs. real-time)</li>
  <li><strong>Index partitioning</strong>: Partition the search index by SharePoint site — enables independent scaling and faster tenant-scoped queries</li>
</ul>

<hr />

<h2 id="what-i-would-do-differently">What I Would Do Differently</h2>

<ol>
  <li><strong>Start with Semantic Kernel</strong> — do not hand-roll the orchestration. SK’s memory and plugin abstractions save weeks of plumbing</li>
  <li><strong>Instrument from day one</strong> — log retrieval scores, user feedback, and latency. You cannot improve what you cannot measure</li>
  <li><strong>Test with real user queries</strong> — the queries people actually ask are very different from what developers imagine during build</li>
  <li><strong>Plan for document lifecycle</strong> — deleted documents must be removed from the index. Delta sync covers updates but not deletions without explicit handling</li>
</ol>

<hr />

<h2 id="conclusion">Conclusion</h2>

<p>RAG on SharePoint is one of the highest-ROI AI investments an enterprise can make. The knowledge is already there — it just needs to be made accessible. The architecture I have outlined is production-tested, permission-aware, and cost-efficient. The key differentiator is treating chunking, retrieval quality, and citation as first-class concerns rather than afterthoughts.</p>

<p>If you are building something similar or have questions about scaling this pattern, feel free to reach out — I am always happy to discuss enterprise AI architecture.</p>]]></content><author><name>Lovy Jain</name><email>lovyjain18@gmail.com</email></author><category term="azure" /><category term="ai" /><category term="sharepoint" /><category term="Azure OpenAI" /><category term="RAG" /><category term="SharePoint" /><category term="Semantic Kernel" /><category term="Enterprise AI" /><category term="Azure AI Search" /><summary type="html"><![CDATA[Production-tested architecture for grounding LLMs in enterprise SharePoint knowledge — permission-aware retrieval, hybrid search, and citation-first prompt design.]]></summary></entry></feed>