Back to Blog
SaaS & Side ProjectsAugust 11, 202610 min read

Building My SaaS from Scratch. The Foundation

DR

Doug Ribeiro

10 min read

I have been building software professionally for more than ten years. I have worked on large Angular codebases at BMO, spent two years on the front end of a consumer credit card platform with more than 50,000 users, and am building a tactical RPG as a solo hobby project. None of that prepared me for the specific combination of challenges that came with building my own SaaS from scratch, solo, while holding down a demanding contract role.

This is the first write-up on the build. The product is MailHive, but what I am documenting is the building experience, not a product pitch. What I will say is that it is AI-powered, it solves a problem I personally have had for years, and the core stack is Angular on the front end, Python and FastAPI on the back end, PostgreSQL for data, and Terraform on Scaleway for infrastructure.

This first post is about the foundation: the decisions I made early that everything else is built on, and what I would change if I were starting over today.

Frontend (Angular)Component library, routing, state management 1 API (FastAPI)Authentication, rate limiting, request routing 2 Backend ServicesBusiness logic, domain rules, provider-agnostic AI layer 3 Data LayerPostgreSQL: data, job queue, LISTEN/NOTIFY 4

Why I Decided to Build a SaaS

The honest answer is that I have been wanting to since I was a junior developer. The fantasy of building something useful, putting it online, and having it generate value while I sleep has never fully left me. But for years I rationalized not starting. I did not have a good enough idea. I did not have enough time. The market was too crowded. The idea was too niche.

What changed was the AI tooling wave of the last two years. A specific problem I had been aware of, one that I tried to solve manually and could never quite crack, became suddenly solvable with a combination of LLM capabilities and some custom data pipelines. I had the domain knowledge. I had the engineering skills. I had access to the AI APIs. The excuses ran out.

I also want to be honest about the financial motivation. Senior developer salaries in Canada are good. But passive income from a product you own is a different kind of financial security, and building one feels more aligned with where software is going than continuing to be purely a feature implementer, however senior.

The Tech Stack Decision

Choosing a tech stack for a new project is one of the easiest places to spend three weeks and make no progress. I gave myself a hard rule: choose technologies I already know well, except for one new thing per major area. The point of this project is to ship, not to learn a new framework.

Angular was non-negotiable for the front end. I have been building with it since 2017. I know its performance characteristics, its pitfalls, its best practices. The faster I can build the UI, the faster I can validate whether the product idea is worth continuing. This was not the moment to try React or Vue.

FastAPI for the backend. The short version: Python was necessary for the AI components, and FastAPI gave me the simplest possible path to a production API without the overhead of Django. I knew Python, and FastAPI's request validation and generated OpenAPI docs, both driven by type hints, meant less glue code in an early-stage, exploratory project.

PostgreSQL was chosen over every other database option because it is the right answer for structured business data when you do not have a specific reason to use something else. It handles JSON columns for the flexible parts, full-text search for the content parts, and relational integrity for everything else. No regrets.

Scaleway for hosting, with the infrastructure defined in Terraform and the services shipped as Docker containers. Same reasoning as the database migration workflow I describe below: infrastructure should be code I can read, review, and rebuild, not console clicks I have to remember. GitLab CI runs the quality gates on every merge request: lint rules, security scans, and Vitest with front-end coverage thresholds of 88-93%. Subscription billing runs on Paddle.

Architecture Choices Made Early

The most important early decision was to treat the Angular app and the FastAPI backend as completely separate deployments from day one. No server-side rendering of Angular, no serving the Angular build from FastAPI, no shared localhost server in development. Two separate processes. Two separate domains in production. Complete separation of concerns.

This adds development setup complexity. You need to configure CORS. You need to run two servers locally. But it gives you deployment flexibility, independent scaling, and a clean architectural boundary that pays dividends as the application grows. I have worked on projects where the frontend was served by the backend and the coupling it created was a source of constant pain.

The second early decision was to build the authentication layer first, completely, before writing any product features. Authentication is the hardest thing to retrofit, because permissions, session management, token refresh and every sign-in flow all have to agree with each other, and they all touch code you have already written. Completely meant something specific here: the FastAPI service owns identity, every protected route resolves the current user through one shared dependency rather than its own check, the client refreshes an expiring session in one place instead of per request, and the user and permission tables shipped in the first Alembic migration. No product endpoint was written until all of that worked end to end, which is why I have never had to retrofit an auth check onto an existing endpoint.

The third decision was to set up the database migration workflow on day one, SQLAlchemy with Alembic. Every schema change goes through a migration file. Never, ever, touch the database directly in production. Coming from enterprise Angular development, where we take branch-based workflows for granted, it surprised me how many SaaS tutorials suggest just modifying the schema directly. I could not bring myself to do it.

python
# FastAPI app factory: clean from day one
from contextlib import asynccontextmanager

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.ext.asyncio import create_async_engine

from .config import Settings
from .auth import router as auth_router
from .api import router as api_router


def create_app(settings: Settings | None = None) -> FastAPI:
    settings = settings or Settings()
    engine = create_async_engine(settings.database_url)

    @asynccontextmanager
    async def lifespan(app: FastAPI):
        app.state.engine = engine
        yield
        await engine.dispose()

    app = FastAPI(lifespan=lifespan)
    app.add_middleware(
        CORSMiddleware,
        allow_origins=settings.allowed_origins,
        allow_methods=['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
        allow_headers=['Content-Type', 'Authorization'],
    )

    app.include_router(auth_router, prefix='/auth')
    app.include_router(api_router, prefix='/api/v1')

    # Schema changes never happen at startup: they live in
    # Alembic migration files and run with `alembic upgrade head`.
    return app

What the Angular App Structure Looks Like

I went with a feature-based folder structure from the start, with lazy-loaded standalone components. No NgModules. The app started on Angular 15, the release that made standalone APIs stable, and it is on Angular 21 today, with OnPush change detection and Tailwind CSS. Standalone was the one place I adopted a newer Angular pattern instead of falling back on what I already knew. The module system felt like unnecessary overhead for a project this size.

State management is Angular Signals with a thin service layer. No NgRx for now. The application is not complex enough to justify the ceremony. If I reach a complexity threshold where signal-based services become difficult to reason about, I will introduce a more structured solution. But I refuse to add architectural complexity speculatively.

typescript
// Feature-based structure with standalone components
// src/app/
//   core/
//     auth/          auth service, guards, interceptors
//     http/          base HTTP service, error handling
//     layout/        app shell, navigation
//   features/
//     dashboard/     lazy loaded
//     workspace/     lazy loaded
//     settings/      lazy loaded
//   shared/
//     components/    reusable UI components
//     pipes/
//     directives/

// app.routes.ts
export const routes: Routes = [
  {
    path: '',
    component: AppShellComponent,
    canActivate: [authGuard],
    children: [
      {
        path: 'dashboard',
        loadComponent: () =>
          import('./features/dashboard/dashboard.component')
            .then(m => m.DashboardComponent)
      },
      {
        path: 'workspace',
        loadChildren: () =>
          import('./features/workspace/workspace.routes')
            .then(m => m.WORKSPACE_ROUTES)
      }
    ]
  },
  {
    path: 'auth',
    loadChildren: () =>
      import('./features/auth/auth.routes').then(m => m.AUTH_ROUTES)
  }
];

The AI Integration Layer

The AI components of the product sit entirely on the backend. The Angular front end never calls an AI API directly. All AI calls go through FastAPI endpoints that handle authentication, rate limiting, prompt construction, and response processing, on top of a provider-agnostic AI layer so no feature depends on a single model vendor. This is intentional and important.

Calling AI APIs from the browser would expose my API keys, make rate limiting impossible, and give me no server-side processing layer to transform, cache, or validate AI responses before they reach the user. The backend layer is thin but essential. It is the only way to ship AI features responsibly without burning money on unmetered direct API access.

I have a queue for AI operations (PostgreSQL-backed, no separate broker), a response caching layer for deterministic queries, and per-user rate limiting on the backend. None of this is visible to the Angular app, which just sees a standard REST API that sometimes takes a few seconds to respond, plus a Server-Sent Events stream for real-time updates, fed by PostgreSQL LISTEN/NOTIFY.

What I Wish I Had Done Differently

I spent too long on the local development environment before building any features. Getting Docker Compose working cleanly for Postgres, FastAPI, and an Angular dev server, with hot reload for both app servers, ate far more setup time than it deserved. I could have shipped features faster by starting with simpler local tooling and only containerizing later.

I also over-designed the API response format early. I created a standardized envelope format for all API responses, with metadata, pagination, and error codes. It is a good pattern, but I spent time on it before I had any real data to shape responses around. Some of those early decisions had to be revisited once I understood what the actual data looked like.

The thing I got right and would do again: writing everything down. Every decision I made, I documented as a decision record, with the context and the reasoning. Reading back through those records, I can see where my thinking evolved and why certain architectural choices made sense in context. It is also the closest thing I have to a specification for the system.

Where I Am Now

The foundation is solid. Auth works. The database schema for the core domain is stable. The Angular app covers the main user flows. The AI integration is functional and producing useful output. The product is live in beta.

If you are building something similar, I hope this is useful. The indie dev journey is genuinely different from professional development, and the gaps in the advice available for that specific combination of experience levels and constraints are real.

The hardest part of a solo SaaS is not the code. It is making every architectural decision yourself without a team to pressure-test your reasoning, and shipping features fast enough to validate your idea before you run out of motivation.

References

SaaSAngularFastAPIPostgreSQLIndie Dev