<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Rolina Vorster</title>
    <description>The latest articles on DEV Community by Rolina Vorster (@rolinavorster).</description>
    <link>https://dev.to/rolinavorster</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3951937%2F8a8acab4-158c-4f11-b8da-efd3fb1738dc.jpeg</url>
      <title>DEV Community: Rolina Vorster</title>
      <link>https://dev.to/rolinavorster</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rolinavorster"/>
    <language>en</language>
    <item>
      <title>I'm building an invoicing SaaS for South African freelancers &amp; small agencies — here's where I am so far</title>
      <dc:creator>Rolina Vorster</dc:creator>
      <pubDate>Tue, 26 May 2026 08:03:04 +0000</pubDate>
      <link>https://dev.to/rolinavorster/im-building-an-invoicing-saas-for-south-african-freelancers-heres-where-i-am-so-far-po6</link>
      <guid>https://dev.to/rolinavorster/im-building-an-invoicing-saas-for-south-african-freelancers-heres-where-i-am-so-far-po6</guid>
      <description>&lt;p&gt;I wanted a portfolio project that goes beyond CRUD — something with real multi-tenancy, role-based permissions, payment integration, and deployment. So I'm building TracKeee, an invoicing app tailored for South African freelancers and small agencies.&lt;/p&gt;

&lt;p&gt;It handles SA VAT (15%), ZAR currency, integrates with Yoco for payments, and complies with POPIA. Still a work in progress, but I've learned enough to share.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backend:&lt;/strong&gt; ASP.NET Core MVC (.NET 8)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database:&lt;/strong&gt; Azure SQL + Entity Framework Core&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth:&lt;/strong&gt; ASP.NET Core Identity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PDFs:&lt;/strong&gt; QuestPDF&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email:&lt;/strong&gt; Brevo SMTP&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payments:&lt;/strong&gt; Yoco Checkout API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Charts:&lt;/strong&gt; Chart.js&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hosting:&lt;/strong&gt; Azure App Service (South Africa North)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The biggest lesson: plan for teams from day one
&lt;/h2&gt;

&lt;p&gt;I started by building everything around a single user. Each user had their own clients, projects, invoices. Simple and it worked.&lt;/p&gt;

&lt;p&gt;Then I realised a small agency needs multiple people sharing the same workspace — an owner, an accountant, a project manager. So I refactored the entire data model from user-based to organisation-based. Every model, every controller, every query changed.&lt;/p&gt;

&lt;p&gt;It worked, but it would've been way easier if I'd designed for it from the start.&lt;/p&gt;

&lt;h2&gt;
  
  
  Centralised permissions saved me hours
&lt;/h2&gt;

&lt;p&gt;I have five roles: Owner, Admin, Manager, Accountant, Employee. Instead of checking roles everywhere, I built one method that controls everything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;HasPermission&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OrganizationRole&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="k"&gt;switch&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;"ManageClients"&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;OrganizationRole&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Owner&lt;/span&gt; 
            &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;OrganizationRole&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Admin&lt;/span&gt; 
            &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;OrganizationRole&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Manager&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"ViewFinancials"&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;OrganizationRole&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Owner&lt;/span&gt; 
            &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;OrganizationRole&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Admin&lt;/span&gt; 
            &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;OrganizationRole&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Accountant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"Delete"&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;OrganizationRole&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Owner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every controller and view calls this one method. Adding a new role means updating one file.&lt;/p&gt;

&lt;p&gt;I initially used shortcuts like &lt;code&gt;role != Employee&lt;/code&gt; but switched to explicit positive checks — because adding a new role would silently inherit all those permissions. Small thing, but the kind of mistake that causes real security issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's working so far
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Clients, projects, time entries with organisation-level isolation&lt;/li&gt;
&lt;li&gt;Live start/stop timer in the navbar&lt;/li&gt;
&lt;li&gt;Invoice generation from uninvoiced time entries with 15% VAT&lt;/li&gt;
&lt;li&gt;Branded PDF invoices with business profile and payment links&lt;/li&gt;
&lt;li&gt;Email invoices directly to clients with PDF attached&lt;/li&gt;
&lt;li&gt;Yoco payment integration — each freelancer uses their own account&lt;/li&gt;
&lt;li&gt;Dashboard with charts (hours by month, revenue, hours by client)&lt;/li&gt;
&lt;li&gt;Reports page with date range filtering&lt;/li&gt;
&lt;li&gt;Search and filtering on all list pages&lt;/li&gt;
&lt;li&gt;Five team roles with permission-based UI&lt;/li&gt;
&lt;li&gt;POPIA-compliant legal pages and cookie consent&lt;/li&gt;
&lt;li&gt;Account lockout and session security&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Client portal&lt;/li&gt;
&lt;li&gt;CSV/Excel exports&lt;/li&gt;
&lt;li&gt;Activity log&lt;/li&gt;
&lt;li&gt;Replacing Bootstrap with a custom UI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've built multi-tenant SaaS with .NET, I'd love to hear how you handled permissions and tenant isolation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/RolinaVorster0101/TracKeee" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://trackeee-app-f4auhacrcdhqbxbn.southafricanorth-01.azurewebsites.net" rel="noopener noreferrer"&gt;Live demo&lt;/a&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>azure</category>
      <category>buildinpublic</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
