Building SaaS Products as a Solo Dev
Building a SaaS product on your own can feel like trying to juggle flaming torches while walking a tightrope. The good news? With the right mindset, tools, and a disciplined workflow, you can turn a solo vision into a revenue‑generating service. This guide walks you through the entire journey—from picking a stack to shipping features, handling payments, and scaling without a team.
Why Go Solo?
Solo development forces you to wear many hats, which accelerates learning. You make every architectural decision, so you own the product’s future. Plus, you keep equity 100 % and avoid the overhead of hiring.
However, the trade‑off is time. You’ll need to prioritize ruthlessly, automate repetitive tasks, and lean on community resources. The key is to focus on value‑creating work and outsource or eliminate the rest.
Choosing the Right Stack
Pick a stack that balances speed of development, ecosystem maturity, and scalability. For most solo SaaS founders, a Python‑centric stack works well: FastAPI for APIs, PostgreSQL for data, and Docker for reproducibility.
- FastAPI – async support, automatic OpenAPI docs, and minimal boilerplate.
- SQLModel – a thin wrapper around SQLAlchemy that feels like Pydantic.
- Stripe – industry‑standard for payments, with great SDKs.
- Docker Compose – spin up local dev environments with a single command.
If you prefer JavaScript, consider Next.js with Supabase for a serverless backend. The principles remain the same: choose tools that let you ship fast and iterate.
Minimal Project Layout
my_saas/
├── app/
│ ├── main.py # FastAPI entry point
│ ├── models.py # SQLModel definitions
│ ├── routes/
│ │ └── users.py
│ └── services/
│ └── billing.py
├── tests/
│ └── test_users.py
├── Dockerfile
└── docker-compose.yml
This layout keeps concerns separate and makes it easy to add new features without clutter.
Designing the MVP
The MVP should solve a single, well‑defined problem. Break it down into three layers:
- Core domain logic – the business rules that differentiate your product.
- API layer – thin wrappers that expose the domain to the outside world.
- UI – a simple React or HTMX front‑end that lets users interact.
Focus first on the domain logic; the UI can be a bare‑bones dashboard that you replace later.
Example: A URL Shortener SaaS
Suppose you want to build a service that lets teams create branded short links. The core model is a Link with an owner, target URL, and optional expiration.
# app/models.py
from sqlmodel import SQLModel, Field
from datetime import datetime
from typing import Optional
class Link(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
owner_id: int = Field(index=True)
slug: str = Field(index=True, unique=True)
target_url: str
expires_at: Optional[datetime] = None
created_at: datetime = Field(default_factory=datetime.utcnow)
With this model in place, you can write a single endpoint that validates input, creates a record, and returns the short URL.
# app/routes/links.py
from fastapi import APIRouter, HTTPException, Depends
from sqlmodel import Session, select
from app.models import Link
from app.dependencies import get_session, get_current_user
router = APIRouter(prefix="/links")
@router.post("/", response_model=Link)
def create_link(
payload: Link,
session: Session = Depends(get_session),
user_id: int = Depends(get_current_user)
):
# Ensure slug uniqueness
existing = session.exec(select(Link).where(Link.slug == payload.slug)).first()
if existing:
raise HTTPException(status_code=400, detail="Slug already taken")
payload.owner_id = user_id
session.add(payload)
session.commit()
session.refresh(payload)
return payload
That’s a complete, testable piece of functionality in under 30 lines of code.
Pro tip: Use pydantic schemas for request validation and response models. It keeps your API contracts explicit and reduces bugs.
Authentication & Multi‑Tenancy
Most SaaS products need to isolate data per customer. The simplest approach is “single‑tenant with a tenant_id column.” Every query filters on that column, guaranteeing data separation.
For authentication, consider OAuth2 with JWTs. FastAPI has built‑in support, and you can store the tenant_id inside the token payload.
# app/dependencies.py
from fastapi import Depends, HTTPException, Security
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
SECRET_KEY = "super-secret-key"
ALGORITHM = "HS256"
def get_current_user(token: str = Depends(oauth2_scheme)):
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
user_id: int = payload.get("sub")
tenant_id: int = payload.get("tenant")
if user_id is None or tenant_id is None:
raise HTTPException(status_code=401, detail="Invalid token")
return {"user_id": user_id, "tenant_id": tenant_id}
except JWTError:
raise HTTPException(status_code=401, detail="Invalid token")
Every route can now depend on get_current_user and automatically enforce tenant isolation.
Integrating Stripe for Billing
Stripe’s subscription APIs let you manage plans, trials, and webhooks without building a payment processor from scratch. The flow is:
- Create a Stripe Customer when a user signs up.
- Attach a payment method and subscribe to a plan.
- Listen to
invoice.paidandcustomer.subscription.deletedwebhooks to update your DB.
# app/services/billing.py
import stripe
from app.models import Tenant
from sqlmodel import Session
stripe.api_key = "sk_test_..."
def create_stripe_customer(email: str, tenant_id: int) -> str:
cust = stripe.Customer.create(email=email, metadata={"tenant_id": tenant_id})
return cust.id
def subscribe_customer(customer_id: str, price_id: str):
return stripe.Subscription.create(
customer=customer_id,
items=[{"price": price_id}],
expand=["latest_invoice.payment_intent"]
)
Store the returned customer_id and subscription_id on your Tenant model. Then, a simple webhook endpoint updates the subscription status.
Pro tip: Use Stripe’s test mode and the “stripe-cli listen” command to develop webhooks locally without exposing a public URL.
Automating Repetitive Tasks
As a solo dev, your time is your most valuable resource. Automate everything you can: migrations, CI/CD, and even code generation for common CRUD endpoints.
Docker Compose can spin up a full stack (API, DB, Redis) with one command. Pair it with Makefile shortcuts to run tests, lint, and start the dev server.
# Makefile
run:
docker-compose up --build
test:
docker-compose run api pytest
lint:
docker-compose run api flake8 .
Now you have a single source of truth for your workflow, and you never forget a step.
Deploying & Scaling
For a solo SaaS, start with a platform that abstracts infrastructure: Render, Fly.io, or Railway. They let you push a Docker image and handle TLS, autoscaling, and logs out of the box.
When traffic grows, you’ll need to think about:
- Horizontal scaling – add more containers behind a load balancer.
- Database read replicas – offload analytics queries.
- Background workers – handle email, PDF generation, or heavy data processing.
Background Jobs with Celery
Celery + Redis is a lightweight solution for async tasks. Below is a minimal worker that sends a welcome email after a new tenant signs up.
# tasks.py
from celery import Celery
import smtplib
app = Celery('tasks', broker='redis://redis:6379/0')
@app.task
def send_welcome_email(to_address: str):
with smtplib.SMTP('smtp.example.com') as server:
server.sendmail(
'no-reply@my-saas.com',
to_address,
'Subject: Welcome!\n\nThanks for joining our platform.'
)
Trigger the task right after creating the Stripe customer:
# app/services/billing.py (continued)
from tasks import send_welcome_email
def create_tenant(email: str, plan_id: str):
# ... create Stripe customer & subscription ...
tenant = Tenant(email=email, stripe_customer_id=customer_id, plan_id=plan_id)
session.add(tenant)
session.commit()
send_welcome_email.delay(email)
return tenant
Pro tip: Keep the worker code stateless and idempotent. If a task fails, Celery will retry automatically, preventing lost emails.
Marketing & Customer Feedback
Even the best product needs a pipeline for acquiring users. As a solo founder, focus on low‑cost channels:
- Content marketing – write blog posts, tutorials, or case studies that rank for niche keywords.
- Developer communities – share on Reddit, Hacker News, and relevant Discord servers.
- Free tier with usage caps – let prospects try before they buy, but enforce limits to control costs.
Collect feedback directly in the app with a simple “feedback” modal that posts to a Slack webhook or a Google Sheet.
# app/routes/feedback.py
import requests
from fastapi import APIRouter, Body
router = APIRouter(prefix="/feedback")
@router.post("/")
def submit_feedback(message: str = Body(...)):
payload = {"text": f"New feedback: {message}"}
requests.post("https://hooks.slack.com/services/XXX/YYY/ZZZ", json=payload)
return {"status": "sent"}
This gives you real‑time insight without building a full ticketing system.
Pro Tips for Solo SaaS Success
1. Prioritize revenue‑generating features. If a feature doesn’t directly move the needle on paying customers, put it on the backlog.
2. Use feature flags. Deploy code continuously, but hide unfinished work behind flags so you can ship safely.
3. Keep your codebase tiny. Aim for one function per file when possible. Smaller files are easier to test and refactor.
4. Automate backups. A nightly dump of your PostgreSQL database to an S3 bucket can save you from catastrophic data loss.
5. Track the right metrics. Focus on MRR, churn, CAC, and activation rate. Dashboards in Grafana or Metabase keep you data‑driven.
Conclusion
Building a SaaS product solo is a marathon, not a sprint. By choosing a fast, well‑supported stack, isolating core logic, automating repetitive chores, and leveraging third‑party services for payments and hosting, you can ship a polished product without a team.
Remember: the biggest advantage you have is agility. Iterate quickly, listen to early users, and let the market guide your roadmap. With discipline and the right tools, a solo developer can create a sustainable, growing SaaS business.