<?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: Aswani Kumar</title>
    <description>The latest articles on DEV Community by Aswani Kumar (@ashwin31).</description>
    <link>https://dev.to/ashwin31</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3799673%2F51ecf151-19a9-4868-9272-5583aa83b159.jpg</url>
      <title>DEV Community: Aswani Kumar</title>
      <link>https://dev.to/ashwin31</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ashwin31"/>
    <language>en</language>
    <item>
      <title>Using .h and .cpp files in an online C++ compiler (and why most of them fail)</title>
      <dc:creator>Aswani Kumar</dc:creator>
      <pubDate>Mon, 09 Nov 2026 08:53:06 +0000</pubDate>
      <link>https://dev.to/ashwin31/using-h-and-cpp-files-in-an-online-c-compiler-and-why-most-of-them-fail-lag</link>
      <guid>https://dev.to/ashwin31/using-h-and-cpp-files-in-an-online-c-compiler-and-why-most-of-them-fail-lag</guid>
      <description>&lt;p&gt;Most online C++ compilers give you one editor, and one editor means one file. That's fine until your course splits a class into &lt;code&gt;Student.h&lt;/code&gt; and &lt;code&gt;Student.cpp&lt;/code&gt;. Then the second line of &lt;code&gt;main.cpp&lt;/code&gt; stops the build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;code.cc:2:10: fatal error: Student.h: No such file or directory
    2 | #include "Student.h"
      |          ^~~~~~~~~~~
compilation terminated.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's why that happens, three ways around it, and the linker errors you'll meet on the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the include fails
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;#include "Student.h"&lt;/code&gt; tells the compiler to read &lt;code&gt;Student.h&lt;/code&gt; from the folder of the file that includes it. A one-file online compiler saves your code on its own in an empty folder, so there is no &lt;code&gt;Student.h&lt;/code&gt; to read.&lt;/p&gt;

&lt;p&gt;Pasting in just the header doesn't fix it either. The header only &lt;em&gt;declares&lt;/em&gt; the class; the member functions are &lt;em&gt;defined&lt;/em&gt; in &lt;code&gt;Student.cpp&lt;/code&gt;. If that file isn't compiled too, the linker stops instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/usr/bin/ld: /tmp/ccdg4WQe.o: in function `main':
code.cc:(.text.startup+0x33): undefined reference to `Student::Student(std::__cxx11::basic_string&amp;lt;char, std::char_traits&amp;lt;char&amp;gt;, std::allocator&amp;lt;char&amp;gt; &amp;gt; const&amp;amp;, int)'
...
collect2: error: ld returned 1 exit status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The program needs both files, compiled and linked together.&lt;/p&gt;

&lt;h2&gt;
  
  
  The example
&lt;/h2&gt;

&lt;p&gt;A small class, split the usual way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Student.h&lt;/span&gt;
&lt;span class="cp"&gt;#ifndef STUDENT_H
#define STUDENT_H
&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Student&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="nl"&gt;public:&lt;/span&gt;
    &lt;span class="n"&gt;Student&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;passed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nl"&gt;private:&lt;/span&gt;
    &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="n"&gt;name_&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;score_&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="cp"&gt;#endif
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Student.cpp&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;"Student.h"&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="n"&gt;Student&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Student&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;name_&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;score_&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Student&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;const&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;name_&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;Student&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;passed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;const&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;score_&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// main.cpp&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;iostream&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"Student.h"&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Student&lt;/span&gt; &lt;span class="n"&gt;ada&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Ada"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;91&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;Student&lt;/span&gt; &lt;span class="n"&gt;tim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Tim"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;cout&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;ada&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ada&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;passed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s"&gt;" passed"&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;" failed"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;cout&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;tim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;passed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s"&gt;" passed"&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;" failed"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On your own computer you build both &lt;code&gt;.cpp&lt;/code&gt; files into one program:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;g++ main.cpp Student.cpp &lt;span class="nt"&gt;-o&lt;/span&gt; main
./main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ada passed
Tim failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Option 1: paste the class above &lt;code&gt;main&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;This works in any online compiler. Put the class declaration and its member functions in the same file, above &lt;code&gt;main&lt;/code&gt;, and drop the &lt;code&gt;#include "Student.h"&lt;/code&gt; line. It's fine for a quick check, but you lose the very structure your course is trying to teach, and it doesn't scale past one or two classes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 2: use an online IDE with projects
&lt;/h2&gt;

&lt;p&gt;If you want to click Run in a browser with a tab per file, use a tool that has projects. OnlineGDB lets you add files to a project (and has a debugger), Playcode handles &lt;code&gt;.cpp&lt;/code&gt; and &lt;code&gt;.h&lt;/code&gt; files in folders, and Replit builds every C++ file in a project into one program. Check each one's current plans and limits yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 3: send the files to a compiler API
&lt;/h2&gt;

&lt;p&gt;If the code comes from a script, a course platform or an autograder rather than a person typing in a browser, send the files with the program. OnlineCompiler.io's API (which I build) takes &lt;code&gt;main.cpp&lt;/code&gt; as the program and every other file in a &lt;code&gt;files&lt;/code&gt; list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.onlinecompiler.io/api/run-code-sync/ &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: YOUR_API_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "compiler": "g++-15",
    "code": "#include &amp;lt;iostream&amp;gt;\n#include \"Student.h\"\n\nint main() {\n    Student ada(\"Ada\", 91);\n    Student tim(\"Tim\", 42);\n    std::cout &amp;lt;&amp;lt; ada.name() &amp;lt;&amp;lt; (ada.passed() ? \" passed\" : \" failed\") &amp;lt;&amp;lt; \"\\n\";\n    std::cout &amp;lt;&amp;lt; tim.name() &amp;lt;&amp;lt; (tim.passed() ? \" passed\" : \" failed\") &amp;lt;&amp;lt; \"\\n\";\n}\n",
    "files": [
      {"name": "Student.h", "content": "#ifndef STUDENT_H\n#define STUDENT_H\n\n#include &amp;lt;string&amp;gt;\n\nclass Student {\npublic:\n    Student(const std::string&amp;amp; name, int score);\n    std::string name() const;\n    bool passed() const;\n\nprivate:\n    std::string name_;\n    int score_;\n};\n\n#endif\n"},
      {"name": "Student.cpp", "content": "#include \"Student.h\"\n\nStudent::Student(const std::string&amp;amp; name, int score) : name_(name), score_(score) {}\n\nstd::string Student::name() const { return name_; }\n\nbool Student::passed() const { return score_ &amp;gt;= 50; }\n"}
    ]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response's &lt;code&gt;output&lt;/code&gt; is &lt;code&gt;Ada passed\nTim failed\n&lt;/code&gt; and &lt;code&gt;exit_code&lt;/code&gt; is 0. What happens on the server:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every &lt;code&gt;.cpp&lt;/code&gt;, &lt;code&gt;.cc&lt;/code&gt; and &lt;code&gt;.cxx&lt;/code&gt; file you send is compiled and linked with the program, here as &lt;code&gt;g++ -O2 -Wall code.cc Student.cpp -o code.out&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Headers aren't compiled on their own; they're read where a file includes them.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;name&lt;/code&gt; must match your &lt;code&gt;#include&lt;/code&gt; exactly, capital letters included: &lt;code&gt;student.h&lt;/code&gt; is not &lt;code&gt;Student.h&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Folders work (&lt;code&gt;lib/Student.h&lt;/code&gt;), up to 20 files and 1 MB besides the program.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;g++-15&lt;/code&gt; is C++17; use &lt;code&gt;g++-15-cpp20&lt;/code&gt; for C++20. Add &lt;code&gt;"input"&lt;/code&gt; when the program reads from &lt;code&gt;cin&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same thing from Python, reading the files from the folder you run it in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Student.h&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Student.cpp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.onlinecompiler.io/api/run-code-sync/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;compiler&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;g++-15&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;main.cpp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;files&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exit code:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exit_code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What if your course says &lt;code&gt;#include "Student.cpp"&lt;/code&gt;?
&lt;/h2&gt;

&lt;p&gt;Some courses have &lt;code&gt;main.cpp&lt;/code&gt; include the &lt;code&gt;.cpp&lt;/code&gt; file instead of the header, so the class arrives with its code in one step. Change the include and send the same two files: it prints the same output. The rule that makes both styles work is that a file included with &lt;code&gt;#include "..."&lt;/code&gt; is compiled only through that include, never again on its own, so each function is compiled exactly once.&lt;/p&gt;

&lt;p&gt;On your own computer the two styles need different commands. With the header, build both files (&lt;code&gt;g++ main.cpp Student.cpp&lt;/code&gt;). With &lt;code&gt;#include "Student.cpp"&lt;/code&gt;, build only &lt;code&gt;g++ main.cpp&lt;/code&gt;. Build both in that style and the class is compiled twice, which the linker refuses (shortened):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ g++ main.cpp Student.cpp -o main
/usr/bin/ld: /tmp/cciho97q.o: in function `Student::Student(...)':
Student.cpp:(.text+0x0): multiple definition of `Student::Student(...)'; /tmp/ccteijRb.o:main.cpp:(.text+0xb0): first defined here
collect2: error: ld returned 1 exit status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An IDE that builds every &lt;code&gt;.cpp&lt;/code&gt; file in a project does exactly that, so there, include the header. Headers are how larger C++ programs are built anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  The errors, in one table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Error&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fatal error: Student.h: No such file or directory&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The header isn't next to the program, or its name differs (case counts)&lt;/td&gt;
&lt;td&gt;Add it under the exact name you include&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;undefined reference to `Student::Student(...)'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The header was found, but nothing defines the functions it declares&lt;/td&gt;
&lt;td&gt;Compile and link &lt;code&gt;Student.cpp&lt;/code&gt; too&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redefinition of 'class Student'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;One file read the header twice, and it has no include guard&lt;/td&gt;
&lt;td&gt;Wrap it in &lt;code&gt;#ifndef STUDENT_H&lt;/code&gt; / &lt;code&gt;#define STUDENT_H&lt;/code&gt; / &lt;code&gt;#endif&lt;/code&gt;, or use &lt;code&gt;#pragma once&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;multiple definition of `passMark()'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A function body lives in a header that two &lt;code&gt;.cpp&lt;/code&gt; files include&lt;/td&gt;
&lt;td&gt;Move the body to the &lt;code&gt;.cpp&lt;/code&gt;, or mark it &lt;code&gt;inline&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;multiple definition of `main'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A second file (often a &lt;code&gt;.cpp&lt;/code&gt; with a leftover test &lt;code&gt;main&lt;/code&gt;) defines &lt;code&gt;main&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Keep one &lt;code&gt;main&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Grading multi-file C++ automatically
&lt;/h2&gt;

&lt;p&gt;If you teach, the same &lt;code&gt;files&lt;/code&gt; list works with the API's run-tests endpoint: send the student's &lt;code&gt;Student.cpp&lt;/code&gt;, your own &lt;code&gt;main.cpp&lt;/code&gt; that exercises it, and a list of inputs with expected outputs, and you get a verdict for each test and a score. That's how you can auto-grade a class assignment without setting up a judge server. The free plan has 100,000 runs a month, no credit card.&lt;/p&gt;

&lt;p&gt;The full guide, with every output and the C, Java and folder variations, is here: &lt;a href="https://onlinecompiler.io/guides/cpp-header-and-source-files-online" rel="noopener noreferrer"&gt;C++ header files in an online compiler&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I build OnlineCompiler.io. Options 1 and 2 above work without it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cpp</category>
      <category>beginners</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I Built a Multi-Tenant CRM with PostgreSQL Row-Level Security — Here's What I Learned</title>
      <dc:creator>Aswani Kumar</dc:creator>
      <pubDate>Sun, 01 Mar 2026 09:07:11 +0000</pubDate>
      <link>https://dev.to/ashwin31/i-built-a-multi-tenant-crm-with-postgresql-row-level-security-heres-what-i-learned-27c5</link>
      <guid>https://dev.to/ashwin31/i-built-a-multi-tenant-crm-with-postgresql-row-level-security-heres-what-i-learned-27c5</guid>
      <description>&lt;p&gt;Every CRM I've ever used has made me angry.&lt;/p&gt;

&lt;p&gt;Not the "this button is in the wrong place" kind of angry. The "I'm paying $65/user/month and I can't even export my own data" kind of angry.&lt;/p&gt;

&lt;p&gt;I'm Ashwin Kumar. I ran &lt;a href="https://micropyramid.com" rel="noopener noreferrer"&gt;MicroPyramid&lt;/a&gt;, a software consulting firm with 100+ engineers and clients across the US, Australia, Israel, and the UK. Over a decade, we built custom CRMs, ERPs, and SaaS platforms for dozens of companies. And every single time, the conversation started the same way:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"We're using Salesforce/HubSpot/Zoho, but it's too expensive, too locked-in, and we can't customize it."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So we'd build them something custom. Again and again. After the fifth time building essentially the same multi-tenant CRM from scratch, I thought — why not build it once, properly, and open-source it?&lt;/p&gt;

&lt;p&gt;That became &lt;a href="https://github.com/MicroPyramid/Django-CRM" rel="noopener noreferrer"&gt;Django-CRM&lt;/a&gt; (now &lt;strong&gt;BottleCRM&lt;/strong&gt;). It's got 2,100+ stars on GitHub, it's MIT-licensed, and it runs in production. But the most interesting technical decision — the one I want to talk about — is how we handle multi-tenancy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Multi-Tenancy Problem
&lt;/h2&gt;

&lt;p&gt;If you're building any SaaS, you need multi-tenancy. Multiple organizations sharing the same application, each seeing only their own data. There are three common approaches:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Separate databases per tenant&lt;/strong&gt; — bulletproof isolation, operational nightmare&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema-per-tenant&lt;/strong&gt; (the &lt;code&gt;django-tenants&lt;/code&gt; approach) — each tenant gets a PostgreSQL schema, shared database&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared schema with Row-Level Security&lt;/strong&gt; — one schema, one set of tables, PostgreSQL enforces isolation at the row level&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When I started, &lt;code&gt;django-tenants&lt;/code&gt; was the obvious choice. It's mature, well-documented, and the Django community loves it. I almost went with it.&lt;/p&gt;

&lt;p&gt;Then I did the math.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Schema-Per-Tenant Broke Down
&lt;/h2&gt;

&lt;p&gt;With &lt;code&gt;django-tenants&lt;/code&gt;, every &lt;code&gt;migrate&lt;/code&gt; runs against every schema. Got 500 tenants? That's 500 migration runs. Add a column? Hope you've got time.&lt;/p&gt;

&lt;p&gt;Here's what we actually hit in production with schema-per-tenant on a previous project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Migration time scaled linearly&lt;/strong&gt; with tenant count. 200 tenants meant 45+ minute deploys.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connection pooling was painful.&lt;/strong&gt; PgBouncer doesn't love &lt;code&gt;SET search_path&lt;/code&gt; on every request. We burned hours debugging stale connections.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-tenant queries were nearly impossible.&lt;/strong&gt; Want to build an admin dashboard showing all tenants? You're writing raw SQL with &lt;code&gt;UNION ALL&lt;/code&gt; across schemas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Django's ORM assumed one schema.&lt;/strong&gt; We were constantly fighting the framework instead of using it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;RLS flips the model entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Row-Level Security: Let PostgreSQL Do the Hard Work
&lt;/h2&gt;

&lt;p&gt;With RLS, every table has a policy that says: "You can only see rows where &lt;code&gt;org_id&lt;/code&gt; matches the current session variable." PostgreSQL enforces this at the engine level — not in your application code, not in your ORM, not in a middleware hack. At the database engine level.&lt;/p&gt;

&lt;p&gt;Here's what an actual RLS policy looks like for our &lt;code&gt;leads&lt;/code&gt; table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Enable RLS on the table&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;leads_lead&lt;/span&gt; &lt;span class="n"&gt;ENABLE&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="k"&gt;LEVEL&lt;/span&gt; &lt;span class="k"&gt;SECURITY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Force RLS even for table owners&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;leads_lead&lt;/span&gt; &lt;span class="k"&gt;FORCE&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="k"&gt;LEVEL&lt;/span&gt; &lt;span class="k"&gt;SECURITY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- The policy: users can only see rows belonging to their org&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="n"&gt;tenant_isolation_policy&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;leads_lead&lt;/span&gt;
    &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;org_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current_setting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'app.current_org'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Separate policy for INSERT (ensure users can only create rows for their org)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="n"&gt;tenant_insert_policy&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;leads_lead&lt;/span&gt;
    &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt;
    &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;org_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current_setting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'app.current_org'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Once this policy exists, PostgreSQL will &lt;strong&gt;never&lt;/strong&gt; return a row from another tenant, even if your application code has a bug. Even if someone writes a raw SQL query. Even if a junior developer forgets to add &lt;code&gt;.filter(org=request.org)&lt;/code&gt;. The database says no.&lt;/p&gt;

&lt;p&gt;This is defense in depth that actually works.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Django Talks to RLS
&lt;/h2&gt;

&lt;p&gt;The magic glue is a middleware that sets the PostgreSQL session variable on every request. Here's the actual pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# middleware/tenant.py
&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TenantMiddleware&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;get_response&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_response&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__call__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# Extract org from the authenticated user's JWT
&lt;/span&gt;        &lt;span class="n"&gt;org_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;org&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;org_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.db&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt;
            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SET app.current_org = %s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;org_id&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Reset to prevent connection pool leakage
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;org_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RESET app.current_org&lt;/span&gt;&lt;span class="sh"&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;response&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The beauty: &lt;strong&gt;your views and serializers don't need to know about tenancy at all.&lt;/strong&gt; The ORM just works. Querysets automatically return only the current tenant's data because PostgreSQL filters at the engine level.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# views.py — no tenant filtering needed!
&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LeadViewSet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;viewsets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ModelViewSet&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;serializer_class&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LeadSerializer&lt;/span&gt;
    &lt;span class="n"&gt;permission_classes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;IsAuthenticated&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_queryset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# This returns ALL leads — but RLS ensures
&lt;/span&gt;        &lt;span class="c1"&gt;# only the current org's leads come back
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Lead&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;order_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-created_at&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;perform_create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;serializer&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# org_id is set automatically, and RLS validates
&lt;/span&gt;        &lt;span class="c1"&gt;# that it matches the session variable
&lt;/span&gt;        &lt;span class="n"&gt;serializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;org_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;org&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;created_by&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;.filter(org=request.org)&lt;/code&gt; scattered across 200 views. No chance of forgetting it in one place and leaking data. The database is the single source of truth for tenant isolation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Full Stack: Why Django + SvelteKit
&lt;/h2&gt;

&lt;p&gt;A lot of people ask why not Next.js or React. Short answer: I've shipped production apps with all of them, and for a CRM, SvelteKit wins on three fronts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Bundle size.&lt;/strong&gt; CRMs have dozens of forms, tables, and modals. Svelte compiles away the framework — our JS bundle is ~40% smaller than the React equivalent we prototyped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reactivity without boilerplate.&lt;/strong&gt; Svelte 5's runes (&lt;code&gt;$state&lt;/code&gt;, &lt;code&gt;$derived&lt;/code&gt;) are cleaner than &lt;code&gt;useState&lt;/code&gt; + &lt;code&gt;useEffect&lt;/code&gt; + &lt;code&gt;useMemo&lt;/code&gt;. For a data-heavy app, this matters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SvelteKit's routing is file-based and simple.&lt;/strong&gt; No fighting with App Router vs Pages Router. No server components confusion. It just works.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The full stack:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Tech&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;Django 5 + DRF&lt;/td&gt;
&lt;td&gt;Battle-tested, ORM is excellent for complex data models&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frontend&lt;/td&gt;
&lt;td&gt;SvelteKit 2 + Svelte 5 + TailwindCSS 4&lt;/td&gt;
&lt;td&gt;Fast, small, great DX&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;PostgreSQL 14+&lt;/td&gt;
&lt;td&gt;RLS, JSONB, full-text search — all in one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth&lt;/td&gt;
&lt;td&gt;JWT (access + refresh tokens)&lt;/td&gt;
&lt;td&gt;Stateless, works with mobile apps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Background jobs&lt;/td&gt;
&lt;td&gt;Celery + Redis&lt;/td&gt;
&lt;td&gt;Email, notifications, data imports&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment&lt;/td&gt;
&lt;td&gt;Docker Compose&lt;/td&gt;
&lt;td&gt;One command: &lt;code&gt;docker compose up&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Performance: RLS vs Schema-Per-Tenant
&lt;/h2&gt;

&lt;p&gt;Let's talk numbers. These are from our benchmarks with realistic data (10K-100K rows per tenant):&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Query performance:&lt;/strong&gt; Virtually identical. PostgreSQL's query planner treats RLS policies like additional &lt;code&gt;WHERE&lt;/code&gt; clauses. With a proper index on &lt;code&gt;org_id&lt;/code&gt;, there's negligible overhead — we measured &amp;lt;1ms difference on 95th percentile queries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Migration speed:&lt;/strong&gt; This is where RLS dominates. One schema means one migration run, regardless of tenant count. 500 tenants? Same 3-second migration as 1 tenant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Connection pooling:&lt;/strong&gt; Standard PgBouncer works perfectly. No &lt;code&gt;search_path&lt;/code&gt; switching, no stale state. Each request just sets a session variable and resets it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disk usage:&lt;/strong&gt; Shared indexes across all tenants. Schema-per-tenant duplicates every index for every tenant. At 500 tenants with 15 tables, that's 7,500 indexes vs. 15.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The tradeoff?&lt;/strong&gt; Schema-per-tenant gives you harder isolation boundaries. If a tenant needs a custom column or a different schema, schema-per-tenant handles that natively. With RLS, every tenant shares the same schema. For a CRM where every tenant uses the same data model, this isn't a limitation — it's a feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why MIT and Not AGPL
&lt;/h2&gt;

&lt;p&gt;This was a deliberate choice. AGPL would have "protected" the project by forcing anyone who modifies it to open-source their changes. But in practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AGPL scares enterprises.&lt;/strong&gt; Legal teams see AGPL and close the tab. We wanted adoption.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MIT builds trust.&lt;/strong&gt; You can fork it, modify it, sell it, host it — we don't care. Use it however you want.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Community over control.&lt;/strong&gt; The projects with the healthiest ecosystems — React, Vue, Django itself — are permissively licensed. We wanted contributors, not compliance officers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our bet: if the product is good enough, people contribute because they want to, not because a license forces them. So far, that bet is paying off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons From Running This With Real Clients
&lt;/h2&gt;

&lt;p&gt;After years of running multi-tenant CRM deployments, here's what I wish someone had told me:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Always reset the session variable.&lt;/strong&gt; If you set &lt;code&gt;app.current_org&lt;/code&gt; and don't reset it, connection pools will serve the wrong tenant's data to the next request. We learned this the hard way. Always reset in a &lt;code&gt;finally&lt;/code&gt; block or middleware teardown.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Superuser bypasses RLS by default.&lt;/strong&gt; PostgreSQL RLS policies don't apply to superusers or table owners unless you use &lt;code&gt;FORCE ROW LEVEL SECURITY&lt;/code&gt;. We missed this initially and wondered why our admin panel showed all data. Add &lt;code&gt;FORCE&lt;/code&gt; to every table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Test with multiple tenants from day one.&lt;/strong&gt; Write a test that creates two orgs, inserts data for both, and asserts that each org can only see its own. Run it in CI. Never skip it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Audit logging needs the org context too.&lt;/strong&gt; Your audit trail should capture which org performed which action. Don't rely on application-level logging alone — use PostgreSQL triggers that read &lt;code&gt;current_setting('app.current_org')&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Data exports are trivially simple.&lt;/strong&gt; Need to export all of tenant 42's data? &lt;code&gt;SET app.current_org = '42'; COPY leads_lead TO '/tmp/leads.csv' CSV HEADER;&lt;/code&gt; — RLS does the filtering for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;BottleCRM runs with a single command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docker-compose.yml (simplified)&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./backend&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DATABASE_URL=postgres://crm:crm@db:5432/bottlecrm&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;REDIS_URL=redis://redis:6379&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;SECRET_KEY=${SECRET_KEY}&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;redis&lt;/span&gt;

  &lt;span class="na"&gt;frontend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./frontend&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;PUBLIC_API_URL=http://backend:8000&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3000:3000"&lt;/span&gt;

  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_DB=bottlecrm&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_USER=crm&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_PASSWORD=crm&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pgdata:/var/lib/postgresql/data&lt;/span&gt;

  &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis:7-alpine&lt;/span&gt;

  &lt;span class="na"&gt;celery&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./backend&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;celery -A crm worker -l info&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;redis&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pgdata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/MicroPyramid/Django-CRM.git
&lt;span class="nb"&gt;cd &lt;/span&gt;Django-CRM
docker compose up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Open &lt;code&gt;localhost:3000&lt;/code&gt;, create an account, and you've got a production-grade CRM running locally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ship It
&lt;/h2&gt;

&lt;p&gt;Building BottleCRM taught me that the best multi-tenancy strategy isn't the most clever one — it's the one that lets PostgreSQL do what it's good at. RLS pushes tenant isolation into the database engine where it belongs. Your application code stays clean, your deployments stay fast, and your data stays safe.&lt;/p&gt;

&lt;p&gt;If you're building a multi-tenant Django app, seriously consider RLS before reaching for &lt;code&gt;django-tenants&lt;/code&gt;. It's less magical, more explicit, and PostgreSQL has been battle-testing this feature since version 9.5.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;⭐ &lt;a href="https://github.com/MicroPyramid/Django-CRM" rel="noopener noreferrer"&gt;Star Django-CRM on GitHub&lt;/a&gt;&lt;/strong&gt; — 2,100+ stars and growing&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🚀 &lt;a href="https://bottlecrm.io" rel="noopener noreferrer"&gt;Try the live demo at bottlecrm.io&lt;/a&gt;&lt;/strong&gt; — free, no credit card&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🤝 Contribute&lt;/strong&gt; — we're looking for help with the mobile app (Flutter), analytics dashboard, and documentation. Check the issues tab.&lt;/p&gt;

&lt;p&gt;Built with 🐍 Django + ⚡ SvelteKit + 🐘 PostgreSQL. MIT Licensed. Free forever.&lt;/p&gt;

</description>
      <category>django</category>
      <category>opensource</category>
      <category>webdev</category>
      <category>crm</category>
    </item>
  </channel>
</rss>
