OSS + SaaS
Categories:
7 minute read
For software developers, one pattern that has emerged is building open-source projects that can be monetized as Software as a Service (SaaS). This allows you to share your work with the community while also creating a sustainable business model.
There are a few key pieces to this:
- Licensing: You need to choose the right open-source license that allows you to share your code while also protecting your ability to monetize it.
- Git Strategy: Decide how to structure your codebase on GitHub, whether using submodules or a monorepo.
Below is more detail on these topics.
Dual Licensing Strategy
To monetize an open-source project as a SaaS, a common approach is to use a dual licensing strategy. This allows you to offer your software under both an open-source license and a commercial license. Add LICENSE.md
(AGPL) and COMMERCIAL_LICENSE.md
to your repository root.
Open Source License
Use AGPL for public core. This ensures anyone who modifies + hosts must share back. This license is designed to protect the freedom of users while also allowing you to monetize your software.
The AGPL (Affero General Public License) has the following key features:
- Copyleft: Any derivative work must also be open-sourced under the same license
- Network Use: If the software is used over a network (like a SaaS), the source code must be made available to users
- Share-Alike: Any modifications must also be shared under the same AGPL license
- Commercial Use: Allows commercial use, but requires sharing source code if distributed or used over a network
- Compatibility: Compatible with many other open-source licenses, allowing for integration with other projects
- Community Contributions: Encourages contributions from the community, as they can use the software freely but must share improvements
- Legal Protection: Provides a strong legal framework to protect your rights as the original author
- Enforcement: The AGPL has been tested in courts, providing a level of legal certainty for both users and developers
Commercial License
The COMMERCIAL_LICENSE.md
mentioned above is a custom license agreement, typically created with the help of a legal professional. Unlike the AGPL, there isn’t a standard GNU commercial license template to use.
Here’s what a commercial license typically includes:
Custom Terms: Created specifically for your SaaS business, with help from a lawyer familiar with software licensing
Key Provisions:
- Permission to use the software without the AGPL’s “share-back” requirements
- Terms for hosting and modifying the software privately
- Usage restrictions (users, instances, API calls, etc.)
- Support and maintenance terms
- Warranty provisions (typically more extensive than AGPL)
- Pricing and payment terms
- Confidentiality clauses
- Termination conditions
Business Model Support: The license should be designed to support your specific pricing structure and service offerings
Most companies implementing a dual-licensing strategy:
- Work with specialized legal counsel who understand both open source and commercial software licensing
- Offer tiered commercial options with different terms based on customer size/needs
- Maintain clear boundaries between what’s covered by AGPL vs. commercial licenses
The commercial license essentially functions as a way for customers to “opt out” of the AGPL requirements (particularly the source code sharing requirements) in exchange for a fee, while still getting access to your software.
Git Strategy
The other key piece is how do you technically structure your codebase so that you can naturally separate the open-source core from the SaaS features? There are two common approaches:
Option 1: Git Submodules
This is scenario where you have two separate repositories:
awesomeapp-core
(public)awesomeapp-saas
(private, imports core as submodule, adds billing + premium features)
In the awesomeapp-saas
repository, you would include the awesomeapp-core
as a Git submodule (e.g. .gitmodules
). This allows you to keep the core code public while adding private SaaS features in the SaaS repo.
In other words, when you are working in the SaaS repository, it will look like the core code is part of the same project. However, the core code remains in its own repository, allowing you to maintain a clear separation between the open-source and SaaS components.
Option 2: Monorepo with Feature Flags
This is a scenario where you have a single repository that contains both the open-source core and the SaaS features, but you use feature flags to control what gets deployed.
/core
-> always published/pro
-> only deployed in SaaS version
This is more complex and not ideal because it can lead to confusion about what is open-source vs. SaaS-only. Also, your entire product is now public, which may not be what you want. For example, developers outside of U.S., who are not afraid of copyright lawsuits, may not care about the AGPL and will use your code to stand up a competing service. Again, probably not ideal.
Technical Implementation
There are a few ways to do this, and it also depends on the technologies that you are using. Below is a guide for how to implement this with Vite and React, but the concepts can be adapted to other frameworks.
Overlay OSS with SaaS Using Shadow Imports
If your open source project is a Vite-based React app for example, and you’re building a SaaS version on top of it, this guide covers a clean, scalable, and maintainable strategy to overlay new or changed files - without forking the whole thing.
We’re using a pattern we call shadow imports, which mirrors how Hugo and Jekyll, for example let you override theme files without touching the original theme source.
Why Shadow Imports?
We ultimately want something where the SaaS version is based on specific releases of the OSS project. Later on, we want to migrate features that have been in the SaaS version back into the OSS project, while keeping the SaaS version private.
This approach has several benefits:
- Clean separation of OSS and SaaS code
- Allows safe upstream updates to the OSS core
- Only override what you need - fall back to the rest
- Keeps the SaaS version private, while OSS stays public
Folder Layout
Your SaaS repo structure should look like this:
my-saas-app/
├── overrides/
│ ├── components/
│ │ └── Navbar.tsx # override just this file
│ └── pages/
│ └── Dashboard.tsx # pro-only page
├── oss-core/ # git submodule of your open-source project
│ ├── components/
│ └── pages/
├── main-app/
│ └── App.tsx # entry point
├── vite.config.ts
└── package.json
Step-by-Step Setup
1. Use Import Aliases in Your OSS Project
Refactor your OSS code to use alias-based imports instead of relative paths:
// Instead of this:
import Navbar from '../../components/Navbar'
// Do this:
import Navbar from '@components/Navbar'
Update the OSS project’s vite.config.ts
:
// vite.config.ts in oss-core
import { defineConfig } from 'vite'
import path from 'path'
export default defineConfig({
resolve: {
alias: {
'@components': path.resolve(__dirname, './components'),
'@pages': path.resolve(__dirname, './pages'),
},
},
})
Now all imports are traceable through aliases.
2. Add OSS Repo as a Git Submodule in SaaS
In the SaaS repo root:
git submodule add https://github.com/youruser/oss-core oss-core
This adds the OSS code as a submodule in its own folder.
3. Define Alias Override Logic in SaaS vite.config.ts
Here’s how you set up your SaaS vite.config.ts
to prioritize ./overrides/*
when files exist there, and fall back to oss-core/*
when they don’t:
// vite.config.ts in SaaS repo
import { defineConfig } from 'vite'
import path from 'path'
import fs from 'fs'
function smartResolve(dir: string) {
const overridePath = path.resolve(__dirname, 'overrides', dir)
const ossPath = path.resolve(__dirname, 'oss-core', dir)
return fs.existsSync(overridePath) ? overridePath : ossPath
}
export default defineConfig({
resolve: {
alias: {
'@components': smartResolve('components'),
'@pages': smartResolve('pages'),
},
},
})
If you want to always prioritize overrides when they exist per-file, you’ll need a more advanced plugin system or roll your own resolveId
plugin.
4. Drop in Overrides
Now, just copy any file you want to override into the overrides/
folder:
mkdir -p overrides/components
cp oss-core/components/Navbar.tsx overrides/components/Navbar.tsx
Edit it as needed for the SaaS version (e.g. pro menu items, logged-in state, new logo).
5. Use OSS Components from SaaS
Let’s say you override Navbar
, but still want the original Footer
:
import Footer from '@components/Footer' // still resolves to oss-core
But if you later add overrides/components/Footer.tsx
, that will automatically take precedence.
Best Practices
- Keep
App.tsx
andvite.config.ts
in your SaaS repo as the entry point. - Write new SaaS-only pages (e.g.
/dashboard
,/billing
) directly inoverrides/pages/
. - Use feature flags or role-based logic inside shared components.
- Keep the OSS repo pure and non-leaky - no SaaS logic should creep in.
- Pin your submodule to a stable commit and manually update when needed.
Summary
The shadow import pattern gives you the best of both worlds:
- A real, usable OSS project people can self-host
- A private SaaS version that builds on top with premium features
- The ability to override anything without duplicating everything
It’s like theming, but for code.