The longer version is that I've been building the infrastructure to ship both halves of this dual model for the past few weeks, and every week I discover another place where "two versions, one product" costs more than the simpler pitch suggests.
I should admit upfront that I have no idea if this model is going to work for SEO Dash.
I'm doing it anyway because I love open source, I enjoy building SaaS products, and I wanted to try both on the same product for the first time.
The strategic reasons came after the decision, not before it.
The Setup
SEO Dash is an SEO dashboard I'm building for my own properties, mostly to track what's actually happening to traffic as Google changes and AI engines reshape how people find content.
I wrote about why the measurement problem matters a couple of weeks ago, and the short version is that the old tools are built for a game that's splintering into five different practices, and none of them show you all of it.
The commercial SaaS version is the source of truth.
It has billing, user accounts, team features, hosted infrastructure, admin tooling, marketing site integration, and the managed providers that make everything work without the user touching an API key.
The OSS version is derived from the same codebase by deleting one main directory (/src/saas/) and a short list of SaaS-only siblings (the marketing route group, billing routes, admin routes, the Stripe dependency), then removing a single import "@/saas" line that pulls the SaaS layer into the app. It runs as a self-hosted app where developers bring their own credentials and manage their own infrastructure.
The theory is that feature work happens once in the main tree, the SaaS build ships immediately, and the OSS build picks up those changes on the next release cycle.
The reality is that every change to the core product surface requires thinking about how it flows to OSS, whether it depends on anything SaaS-only, and how the build will handle it when that one directory disappears.
None of that work is hard in isolation. It's the accumulation that gets you.
Why I'm Doing This Anyway
The honest reason is that I built Preflight as a pure open source project a while back and it was one of the most enjoyable things I've shipped. No commercial pressure, no onboarding metrics to chase, no refund emails to process.
Just writing useful code, putting it on GitHub, and watching other developers find it, star it, and sometimes fork it.
The SaaS products I've built since (Hyperfocal, AutoChangelog, and a few others) have been commercial from day one.
Those have their own joys, including revenue, direct customer conversations, and the operational challenges of running a live product that people use. Different satisfaction, but same general love of building.
SEO Dash felt like a chance to do both on the same product.
I wanted to know if I could have the community energy of open source and the sustainability of SaaS at the same time without the two halves canceling each other out.
The rationalizations came afterward, and they're legitimate enough to list.
Open source establishes credibility in technical communities that can smell marketing from a mile away. Self-hosters who would never pay for a SaaS version become contributors, evangelists, and sometimes eventually customers when they get tired of maintaining their own deployment.
The repo becomes a trust signal because nobody's locking their data inside a proprietary black box.
And OSS creates distribution in ways that are hard to buy, through GitHub stars, HackerNews threads, developer blogs, and word-of-mouth in dev communities.
All of that came after the decision. The decision itself was that I wanted to try both models at once because I like both, and I was willing to pay the cost of finding out whether the combined thing works better than either one alone.
How the Architecture Actually Works
I considered the fully separate two-repo approach where OSS is its own thing maintained in parallel, but the synchronization cost felt prohibitive for a solo builder.
Instead, I went with a single Next.js repo where the SaaS version is the source of truth and the OSS version gets generated from it by deleting /src/saas/ and a handful of SaaS-only siblings.
The top-level structure looks like this:
A few decisions shaped everything else, and each one solved a specific problem.
One repo, with /src/saas/ as the boundary. Keeping everything in a single tree means I work on both versions from the same place and generate the OSS release by excluding one directory.
The SaaS build pulls from /src/saas/* when it needs managed behaviours.
The OSS build doesn't have that folder at all, and the single line in src/lib/extensions.ts that imports @/saas gets removed as part of the OSS procedure. This approach traded the complexity of maintaining two parallel repos for the discipline of keeping one boundary clean, and for me that was the better trade.
The "managed provider" pattern. Inside /src/saas/providers/ there are five files: managed-email.ts, managed-llm.ts, managed-pagespeed.ts, managed-serp.ts, and managed-google-oauth.ts. Each one is a thin documentation stub that names the platform env vars for that capability. The actual logic lives in the corresponding OSS providers under /src/lib/providers/, which read the user's configured key first and fall back to those platform env vars when set.
So 'managed' doesn't mean a separate code path; it's the same provider with a platform-level fallback that only activates in SaaS deployments.
The SaaS version doesn't fully manage every external service.
It specifically handles the ones that are most painful to set up or that have real per-request costs.
- Transactional email comes out of my own provider so users don't need to configure Postmark or Resend.
- Google OAuth runs through my verified Cloud project so users don't have to create their own app and wait for Google's approval process for sensitive scopes.
- SERP queries (SerpAPI by default) go through my key with a per-tier monthly allocation, so the Pro plan ships with a quota included and BYOK kicks in when it's exhausted.
- LLM calls are metered with a limited number of "runs" per subscription tier.
- Everything else, including alternate SERP providers like DataForSEO or ValueSERP and any third-party SEO data providers (Ahrefs, Semrush, Moz, Majestic), stays BYOK even for SaaS users.
This middle-ground approach feels like the right shape for this product.
Fully managing every external service would push subscription prices higher than the target audience would accept. Making SaaS users bring their own keys for everything would eliminate most of the reason to pay for SaaS in the first place.
Handling the specific integrations that are either legitimately hard to configure (Google OAuth) or have meaningful per-call costs (SERP, LLMs) captures the convenience that justifies the subscription without requiring me to cover every possible external API a user might want to plug in.
OSS users bring their own keys for all of these. Self-hosters configure their own email provider, create their own Google OAuth app, plug in their own SERP API credentials, and point the LLM provider at whichever model they prefer.
Same interface, same business logic, different credential ownership.
Route groups for separation at the app layer. The (dashboard) and (marketing) route groups in Next.js let me organize the authenticated product and the marketing site without leaking that organization into the URL structure.
For OSS builds, (marketing) gets stripped out because self-hosters don't need pricing pages or signup flows. The setup/ route is the opposite. It exists primarily for OSS first-run flows, where someone just pulled the Docker image and needs to configure their admin account, initial database, and API credentials.
SaaS users never see that flow because I handle the setup on my end.
A single registration entry-point for the SaaS layer. src/lib/extensions.ts is where the app bootstraps itself, and it's the only file in core that imports @/saas. Deleting that one import line, plus the /src/saas/ directory itself, is what turns the SaaS tree into the OSS tree.
The actual SaaS/OSS switchover for credentials happens inside each provider category in src/lib/providers/ (email, LLM, OAuth, SERP, PageSpeed). Every external call reads from a user-configured key first and falls back to a platform env var when one is set. In OSS the platform env vars are simply unset, so every call is BYOK.
In SaaS they're set in production, and the same code path picks them up. The boundary is enforced by exactly one filename and one import statement, which is most of the reason the model is workable for a solo builder.
I also keep an internal OSS-SPLIT.md file that documents which directories get stripped for OSS builds, which features are explicitly SaaS-only, and what the build pipeline does differently for each release target.
I wrote it partly for myself and partly for future contributors.
Having it documented matters because the line between SaaS-only and core isn't always obvious, and without the doc I'd be making judgment calls inconsistently every few weeks. With the doc, I can at least be consistent with past decisions even when I later change my mind about where the line should sit.
The Sync Problem Becomes a Build Problem
The advantage of generating OSS from a single source tree is that I don't have to sync two repos. The disadvantage is that the sync problem becomes a build problem instead, which has its own failure modes.
The things I actively have to watch for:
| Surface | What Can Go Wrong | How I Handle It |
|---|---|---|
| Stripped imports | OSS build imports from /src/saas/* and fails because the folder is gone | Only one file in core imports @/saas (src/lib/extensions.ts); the OSS procedure deletes that single line |
| Environment variables | SaaS-only env var gets referenced in core code, OSS users hit cryptic errors | All env access goes through a single config loader that knows which vars are SaaS-only |
| Database migrations | Migration assumes SaaS infrastructure (team tables, billing schema) that OSS doesn't have | Migrations are split into core and SaaS-specific, OSS only runs core |
| Tests | Test suite references a managed provider that isn't in OSS | OSS-specific test pass is part of the manual release checklist; eventually a CI job builds both targets on every PR |
| Dependencies | SaaS-only package (Stripe) ends up imported in core | One known import line (import "@/saas" in src/lib/extensions.ts) is the only place core touches the SaaS layer; everything else is enforced by convention and by the OSS build's grep step |
| Route groups | (marketing) references SaaS-only components that don't exist in OSS | Entire route group excluded from OSS build; /setup handles first-run configuration instead, and the OSS README is the marketing site |
Right now the boundary gets enforced by hand. There's a written procedure in OSS-SPLIT.md that I follow at each major release. Clone the SaaS tree into a sibling directory, delete the SaaS-only paths, drop the import "@/saas" line, uninstall Stripe, run npm run build, and grep for any lingering SaaS references.
Eventually this becomes a script and then a CI job that builds both targets on every PR, but until the contribution traffic justifies the automation, the manual checklist is honest about what the project is.
When the build does break, it's almost always because a new file crept in that broke the boundary, and the fix is either lifting the SaaS-specific part into /src/saas/ or rewriting the dependency so it works from defaults.
Where the Line Keeps Moving
The architecture gives me a place to put everything, but the line between "goes in /src/saas/" and "goes in /src/lib/" still has to be drawn and redrawn constantly because new features often sit ambiguously on that boundary.
- Is the new report-scheduling feature a core capability that should exist in OSS, or does it require infrastructure (job queues, notification services) that only makes sense in the hosted version?
- Is the API rate limiter a core feature or a commercial protection?
- Is the export-to-CSV function something every self-hoster should get, or is it a hosted-only convenience that justifies some part of the subscription?
Those decisions accumulate, and every one of them reshapes what goes into /src/saas/ and what stays in /src/lib/.
The temptation when building fast is to default everything to the SaaS side because that's the version that generates revenue.
The temptation when courting the OSS community is to default everything to /src/lib/ because I want the open source version to feel substantial and not like a stripped-down demo.
Both instincts are wrong more often than they're right, and the actual answer usually requires thinking about what self-hosters actually need to run the product independently versus what only makes sense when someone else (me) is operating the infrastructure.
I don't have a clean rule for this yet.
- What I've been using is a rough heuristic. If removing this feature would make the OSS version feel useless to a self-hoster, it goes in
/src/lib/as core. - If keeping this feature in OSS would undercut the commercial version in a way that makes the SaaS hard to justify, it goes in
/src/saas/.
The gray zone between those two tests is still big enough that I make judgment calls constantly, and OSS-SPLIT.md is where I write those decisions down so at least I'm being consistent with my past self.
The Contribution Fantasy
The OSS-plus-SaaS model assumes contributions flow back from the open source community to the commercial codebase, which is one of the places the theory and reality pull apart most sharply.
Tidelift's 2024 State of the Open Source Maintainer Report found that 60% of maintainers are unpaid and nearly 60% have quit or considered quitting a project they maintain.
A study funded by Sentry and conducted by researcher Miranda Heath at the University of Edinburgh identified six interconnected factors driving maintainers to the edge, including the difficulty of getting paid, the crushing workload of managing requests and PRs, the fact that maintenance work itself feels unrewarding compared to creating new things, and the loneliness of being the single point of responsibility.
What's less discussed in the maintainer burnout research is how few high-quality contributions most OSS projects actually receive.
Popular packages like Kubernetes and React attract real communities, but most projects don't. They receive occasional issues, occasional drive-by PRs that need significant rework before they can be merged, and a lot of feature requests from people who are not going to help build the feature themselves.
The mental model of "the community builds it with me" is a fantasy for most small-to-mid open source projects, and the reality is that you maintain it while occasionally merging improvements from outside.
Nadia Eghbal's Ford Foundation report from years ago framed open source infrastructure as "roads and bridges," and the metaphor has held up because most critical open source is maintained by one or two people doing work that the rest of the world benefits from without contributing back.
This changes how you have to think about the OSS side of a dual model.
If you're doing 95% of the work on both repos anyway, the OSS version isn't saving you labor. It's adding labor in exchange for a set of benefits that are real but indirect.
What You Actually Get
Despite the mess, the reasons the approach might still be worth it for SEO Dash are specific enough to name.
Self-hosters become beta testers at scale, running the software in configurations and environments I'd never think to test. Bugs surface that would never appear in the hosted version. Feature requests arrive with higher signal because the person making them is technical enough to articulate the problem clearly and sometimes attach a PR.
The distribution effect is real even when contributions are thin, because GitHub stars, HackerNews posts, and developer blog mentions create awareness that's harder to buy through paid channels.
And there's a long conversion tail that most OSS-plus-SaaS writers underestimate.
The person who self-hosts today might run it for six months, get tired of maintaining their own deploy, and sign up for the hosted version next year. The developer who saw the repo on HackerNews tells their team about the commercial version two years later when their company has a budget.
None of that shows up in your analytics attributed cleanly to OSS, but it's there, and it accumulates.
The Sustainability Question
The dual model only works if the commercial version pays for enough of the work that maintaining both doesn't burn you out.
Open source has a brutal track record of maintainer burnout specifically because the incentive structure is misaligned.
You write code for free, then get pressured to support it, fix it, and add features to it, also for free. The 2024 Tidelift survey found that 44% of maintainers who left their projects cited burnout specifically as the reason they quit, which is the kind where the joy leaves and the resentment arrives rather than the kind where someone just lost interest and moved on.
The SaaS version has to be the economic engine that lets the OSS version exist.
If the commercial side doesn't generate enough revenue to justify the maintenance overhead on both, you end up with three options.
You can cut OSS support to a trickle and watch the community evaporate, close the OSS version entirely and deal with the reputational hit, or hit the burnout wall and abandon the whole thing.
The people who've made this model work long-term (GitLab, Sentry, Plausible, Supabase, and others) have all reached a scale where the commercial side supports a team that can maintain both.
For a solo builder, the sustainability math is harder, and the honest answer is that I won't know whether it's going to work for SEO Dash until I'm a year or two into operating it, not just building it.
Would I Do It Again
I don't know yet, which is the answer I'd rather give than a confident one that's probably wrong.
I'm about a month into building the infrastructure for this model and zero months into operating it. Ask me in a year.
What I can say is that shipping something I'm excited about matters more than optimizing for the cleanest operational model.
Preflight was pure OSS and I loved building it. The SaaS-only products have been their own kind of satisfying. SEO Dash is the attempt to do both, and even if the dual model turns out to be a mistake, I'll have learned something from it that I couldn't have learned by picking one.
Most of what building actually is comes down to committing to a strategy before you know whether it's going to work.
You don't get to test-drive your business model, you pick one, try to make it work, and adjust when you learn something. The OSS-plus-SaaS approach might be right for SEO Dash, or it might turn out to be a tax I'm paying for aesthetic reasons more than strategic ones.
I'm willing to find out, and the finding out is part of the reason I'm building it this way.