Je construis des logiciels professionnellement depuis plus de dix ans. J'ai travaillé sur de grands codebases Angular chez BMO, passé deux ans sur le front-end d'une plateforme grand public de cartes de crédit avec plus de 50 000 utilisateurs, et je construis un RPG tactique en tant que projet hobby solo. Rien de tout cela ne m'a préparé à la combinaison spécifique de défis qui est venue avec la construction de mon propre SaaS de zéro, en solo, tout en occupant un poste contractuel exigeant.
C'est le premier compte rendu sur cette construction. Le produit s'appelle MailHive, mais ce que je documente ici, c'est l'expérience de construction, pas un pitch produit. Ce que je dirai, c'est qu'il est propulsé par l'IA, qu'il résout un problème que j'ai personnellement eu pendant des années, et que la stack principale est Angular en front-end, Python et FastAPI en back-end, PostgreSQL pour les données et Terraform sur Scaleway pour l'infrastructure.
Ce premier article porte sur les fondations : les décisions que j'ai prises tôt sur lesquelles tout le reste est construit, et ce que je changerais si je recommençais aujourd'hui.
Pourquoi j'ai décidé de construire un SaaS
La réponse honnête est que j'en ai envie depuis que j'étais développeur junior. Le fantasme de construire quelque chose d'utile, de le mettre en ligne, et de le voir générer de la valeur pendant que je dors ne m'a jamais complètement quitté. Mais pendant des années j'ai rationalisé le fait de ne pas commencer. Je n'avais pas une assez bonne idée. Je n'avais pas assez de temps. Le marché était trop encombré. L'idée était trop niche.
Ce qui a changé, c'est la vague d'outils IA des deux dernières années. Un problème spécifique dont j'étais conscient, un que j'ai essayé de résoudre manuellement sans jamais tout à fait y arriver, est devenu soudainement résolvable avec une combinaison de capacités LLM et de pipelines de données personnalisés. J'avais la connaissance du domaine. J'avais les compétences d'ingénierie. J'avais accès aux API d'IA. Les excuses se sont épuisées.
Je veux aussi être honnête sur la motivation financière. Les salaires de développeur senior au Canada sont bons. Mais un revenu passif d'un produit qu'on possède est un type différent de sécurité financière, et en construire un semble plus aligné avec la direction que prend le logiciel que de continuer à être purement un implémenteur de fonctionnalités, aussi senior soit-il.
La décision de stack technique
Choisir une stack technique pour un nouveau projet est l'un des endroits les plus faciles pour passer trois semaines sans faire de progrès. Je me suis donné une règle stricte : choisir des technologies que je connais déjà bien, sauf pour une nouveauté par domaine majeur. Le but de ce projet est de livrer, pas d'apprendre un nouveau framework.
Angular était non négociable pour le front-end. Je construis avec depuis 2017. Je connais ses caractéristiques de performance, ses pièges, ses meilleures pratiques. Plus vite je peux construire l'interface, plus vite je peux valider si l'idée produit vaut la peine d'être poursuivie. Ce n'était pas le moment d'essayer React ou Vue.
FastAPI pour le back-end. La version courte : Python était nécessaire pour les composants IA, et FastAPI m'a donné le chemin le plus simple possible vers une API de production sans l'overhead de Django. Je connaissais Python, et la validation des requêtes et la documentation OpenAPI générée par FastAPI, toutes deux pilotées par les annotations de type, voulaient dire moins de code de plomberie dans un projet exploratoire en phase initiale.
PostgreSQL a été choisi par rapport à toute autre option de base de données parce que c'est la bonne réponse pour les données métier structurées quand vous n'avez pas de raison spécifique d'utiliser autre chose. Il gère les colonnes JSON pour les parties flexibles, la recherche plein texte pour les parties contenu, et l'intégrité relationnelle pour tout le reste. Aucun regret.
Scaleway pour l'hébergement, avec l'infrastructure définie en Terraform et les services livrés en conteneurs Docker. Même raisonnement que pour le workflow de migration de base de données décrit plus bas : l'infrastructure devrait être du code que je peux lire, réviser et reconstruire, pas des clics de console que je dois me rappeler. GitLab CI exécute les quality gates à chaque merge request : règles de lint, scans de sécurité, et Vitest avec des seuils de couverture front-end de 88 à 93 %. La facturation par abonnement tourne sur Paddle.
Choix d'architecture faits tôt
La décision initiale la plus importante a été de traiter l'application Angular et le back-end FastAPI comme des déploiements complètement séparés dès le premier jour. Pas de rendu côté serveur d'Angular, pas de service du build Angular depuis FastAPI, pas de serveur localhost partagé en développement. Deux processus séparés. Deux domaines séparés en production. Séparation complète des préoccupations.
Cela ajoute de la complexité de configuration en développement. Il faut configurer le CORS. Il faut faire tourner deux serveurs localement. Mais cela donne une flexibilité de déploiement, un scaling indépendant et une frontière architecturale propre qui rapporte des dividendes à mesure que l'application grandit. J'ai travaillé sur des projets où le front-end était servi par le back-end et le couplage que cela créait était une source constante de douleur.
La deuxième décision initiale a été de construire la couche d'authentification en premier, complètement, avant d'écrire une seule fonctionnalité produit. L'authentification est la chose la plus difficile à ajouter après coup, parce que les permissions, la gestion de session, le rafraîchissement de token et chaque flux de connexion doivent tous s'accorder entre eux, et qu'ils touchent tous du code déjà écrit. « Complètement » voulait dire quelque chose de précis ici : le service FastAPI est propriétaire de l'identité, chaque route protégée résout l'utilisateur courant via une seule dépendance partagée plutôt que par sa propre vérification, le client rafraîchit une session qui expire à un seul endroit au lieu de le faire par requête, et les tables d'utilisateurs et de permissions sont parties dans la première migration Alembic. Aucun endpoint produit n'a été écrit avant que tout cela ne fonctionne de bout en bout, et c'est pour ça que je n'ai jamais eu à ajouter une vérification d'authentification après coup sur un endpoint existant.
La troisième décision a été de mettre en place le workflow de migration de base de données dès le premier jour, SQLAlchemy avec Alembic. Chaque changement de schéma passe par un fichier de migration. Ne jamais, jamais, toucher la base de données directement en production. Venant du développement Angular en entreprise, où nous prenons les workflows basés sur les branches pour acquis, cela m'a surpris de voir combien de tutoriels SaaS suggèrent de modifier le schéma directement. Je ne pouvais pas m'y résoudre.
# 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 appCe à quoi ressemble la structure de l'application Angular
J'ai opté pour une structure de dossiers basée sur les fonctionnalités dès le départ, avec des composants standalone chargés en lazy. Pas de NgModules. L'application a démarré sur Angular 15, la version qui a rendu les API standalone stables, et elle est sur Angular 21 aujourd'hui, avec la détection de changement OnPush et Tailwind CSS. Standalone est le seul endroit où j'ai adopté un pattern Angular plus récent au lieu de me rabattre sur ce que je connaissais déjà. Le système de modules semblait être un overhead inutile pour un projet de cette taille.
La gestion d'état est Angular Signals avec une fine couche de services. Pas de NgRx pour l'instant. L'application n'est pas assez complexe pour justifier la cérémonie. Si j'atteins un seuil de complexité où les services basés sur les signals deviennent difficiles à raisonner, j'introduirai une solution plus structurée. Mais je refuse d'ajouter de la complexité architecturale de manière spéculative.
// 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)
}
];La couche d'intégration IA
Les composants IA du produit se trouvent entièrement côté back-end. Le front-end Angular n'appelle jamais une API IA directement. Tous les appels IA passent par des endpoints FastAPI qui gèrent l'authentification, le rate limiting, la construction de prompts et le traitement des réponses, par-dessus une couche IA agnostique du fournisseur pour qu'aucune fonctionnalité ne dépende d'un seul éditeur de modèle. C'est intentionnel et important.
Appeler les API IA depuis le navigateur exposerait mes clés d'API, rendrait le rate limiting impossible, et ne me donnerait aucune couche de traitement côté serveur pour transformer, mettre en cache ou valider les réponses IA avant qu'elles n'atteignent l'utilisateur. La couche back-end est fine mais essentielle. C'est le seul moyen de livrer des fonctionnalités IA de manière responsable sans brûler de l'argent sur un accès direct non mesuré aux API.
J'ai une file d'attente pour les opérations IA (adossée à PostgreSQL, sans broker séparé), une couche de cache de réponses pour les requêtes déterministes, et un rate limiting par utilisateur côté back-end. Rien de tout cela n'est visible pour l'application Angular, qui voit juste une API REST standard qui prend parfois quelques secondes à répondre, plus un flux Server-Sent Events pour les mises à jour en temps réel, alimenté par LISTEN/NOTIFY de PostgreSQL.
Ce que j'aurais aimé faire différemment
J'ai passé trop de temps sur l'environnement de développement local avant de construire des fonctionnalités. Faire fonctionner Docker Compose proprement pour Postgres, FastAPI et un serveur de dev Angular, avec hot reload pour les deux serveurs d'application, a mangé beaucoup plus de temps de configuration que ça ne le méritait. J'aurais pu livrer des fonctionnalités plus vite en commençant avec des outils locaux plus simples et en conteneurisant plus tard.
J'ai aussi sur-conçu le format de réponse API tôt. J'ai créé un format d'enveloppe standardisé pour toutes les réponses API, avec métadonnées, pagination et codes d'erreur. C'est un bon pattern, mais j'ai passé du temps dessus avant d'avoir de vraies données pour façonner les réponses. Certaines de ces décisions initiales ont dû être revisitées une fois que j'ai compris à quoi ressemblaient réellement les données.
La chose que j'ai bien faite et que je referais : tout écrire. Chaque décision que j'ai prise, je l'ai documentée sous forme de fiche de décision, avec le contexte et le raisonnement. En relisant ces fiches, je peux voir où ma pensée a évolué et pourquoi certains choix architecturaux faisaient sens dans le contexte. C'est aussi ce qui se rapproche le plus d'une spécification pour le système.
Où j'en suis maintenant
Les fondations sont solides. L'authentification fonctionne. Le schéma de base de données pour le domaine principal est stable. L'application Angular couvre les flux utilisateur principaux. L'intégration IA est fonctionnelle et produit des résultats utiles. Le produit est en ligne en bêta.
Si vous construisez quelque chose de similaire, j'espère que ceci est utile. Le parcours de développeur indépendant est véritablement différent du développement professionnel, et les lacunes dans les conseils disponibles pour cette combinaison spécifique de niveaux d'expérience et de contraintes sont réelles.
La partie la plus difficile d'un SaaS solo n'est pas le code. C'est prendre chaque décision d'architecture seul sans équipe pour tester votre raisonnement, et livrer des fonctionnalités assez vite pour valider votre idée avant de manquer de motivation.
Références
- FastAPI documentation. https://fastapi.tiangolo.com/
- SQLAlchemy. https://docs.sqlalchemy.org/en/20/
- Alembic. https://alembic.sqlalchemy.org/
- Terraform Scaleway provider. https://registry.terraform.io/providers/scaleway/scaleway/latest/docs
- PostgreSQL NOTIFY. https://www.postgresql.org/docs/current/sql-notify.html