TL;DR
- J'ai construit un SaaS de gestion immobiliere en production avec de vrais utilisateurs
- Stack : SvelteKit (frontend), FastAPI (backend), Supabase (PostgreSQL + Auth + Storage)
- Row Level Security de Supabase est un game changer pour le multi-tenant
- Magic links plutot que mots de passe : moins de friction, moins de support
- Deploiement sur VPS avec Docker Compose, pas de serverless -- pour garder le controle et les couts bas
Le point de depart
J'ai construit un SaaS de gestion immobiliere from scratch. Pas un tutoriel, pas un side project abandonne au bout de deux semaines -- un vrai produit en production avec de vrais utilisateurs. Voici les decisions techniques que j'ai prises et pourquoi.
Le besoin est simple a formuler : permettre aux proprietaires de gerer leurs biens locatifs, suivre les loyers, generer des documents legaux (quittances, avis d'echeance), et avoir une vue claire de leur patrimoine. Les solutions existantes sont soit hors de prix, soit des usines a gaz pensees pour des gestionnaires professionnels, soit des fichiers Excel passes de main en main. Il y avait un trou dans le marche pour un outil simple, moderne, et abordable.
Ce qui est interessant dans ce projet, ce n'est pas le domaine metier. C'est les decisions d'architecture. Parce que quand tu construis un SaaS seul, chaque choix technique a un impact direct sur ta vitesse de livraison, tes couts, et ta capacite a maintenir le produit.
Le stack
Apres plusieurs iterations et quelques faux departs, voici le stack sur lequel j'ai converge.
Trois briques principales, chacune choisie pour des raisons precises.
SvelteKit pour le frontend
J'ai travaille avec React et Next.js pendant des annees. Je connais l'ecosysteme, les patterns, les pieges. Mais pour ce projet, j'ai choisi SvelteKit. Pourquoi ?
Moins de boilerplate. Svelte compile le code en JavaScript vanilla. Pas de virtual DOM, pas de useEffect a debugger pendant deux heures, pas de useMemo pour eviter des re-renders. Le code que tu ecris est plus proche de ce qui tourne dans le navigateur.
Performance native. Le bundle est plus petit, le rendu est plus rapide, et le Time to Interactive est excellent -- important pour un outil metier que les gens utilisent tous les jours.
DX agreable. Les fichiers .svelte regroupent template, style et logique. Le systeme de routing base sur le filesystem est propre. Les stores reactifs sont intuitifs. Quand tu travailles seul, la DX n'est pas un luxe -- c'est ce qui fait que tu livres ou que tu procrastines.
<script lang="ts">
import { onMount } from 'svelte';
import type { Property } from '$lib/types';
let properties: Property[] = [];
let loading = true;
onMount(async () => {
const res = await fetch('/api/properties', {
headers: { Authorization: `Bearer ${$session.token}` }
});
properties = await res.json();
loading = false;
});
</script>
{#if loading}
<p>Chargement...</p>
{:else}
{#each properties as property}
<PropertyCard {property} />
{/each}
{/if}
Pas de hooks, pas de composants wrapper, pas de providers. Le code fait ce qu'il dit.
FastAPI pour le backend
Python n'est pas mon premier reflexe. Je viens du monde Java/Spring Boot. Mais pour un SaaS ou je suis seul a developper, FastAPI a des arguments solides.
Prototypage rapide. Un endpoint CRUD complet en quelques lignes. La validation des donnees avec Pydantic est stricte et automatique. La documentation OpenAPI est generee sans effort.
Async natif. FastAPI est construit sur Starlette et supporte nativement async/await. Pour un SaaS qui fait beaucoup d'I/O (base de donnees, envoi d'emails, generation de PDF), c'est un avantage reel.
Schema validation avec Pydantic. C'est le point que je sous-estimais le plus. Avoir des modeles de donnees types et valides a chaque couche du backend, ca elimine une categorie entiere de bugs.
from pydantic import BaseModel, EmailStr
from datetime import date
from decimal import Decimal
class TenantCreate(BaseModel):
first_name: str
last_name: str
email: EmailStr
lease_start: date
rent_amount: Decimal
class PropertyResponse(BaseModel):
id: int
address: str
city: str
tenants: list[TenantCreate]
monthly_income: Decimal
class Config:
from_attributes = True
from fastapi import FastAPI, Depends, HTTPException
from .auth import get_current_user
from .models import TenantCreate, PropertyResponse
app = FastAPI()
@app.post("/api/properties/{property_id}/tenants")
async def add_tenant(
property_id: int,
tenant: TenantCreate,
user = Depends(get_current_user)
):
# Verification que la propriete appartient a l'utilisateur
prop = await get_property(property_id, user.id)
if not prop:
raise HTTPException(404, "Propriete non trouvee")
return await create_tenant(property_id, tenant)
Le typage strict de Python 3.12+ combine avec Pydantic donne un backend solide sans la verbosity de Java.
Supabase pour la couche donnees
Supabase, c'est trois services en un : PostgreSQL manage, authentification, et stockage de fichiers. Pour un developpeur solo qui construit un SaaS, c'est difficile a battre.
PostgreSQL. Pas de compromis sur la base de donnees. Relations, transactions, indexes, JSON -- tout ce dont un SaaS a besoin est la. Et c'est une vraie base relationnelle, pas un document store deguise.
Auth integree. Supabase Auth gere les comptes utilisateurs, les sessions, les JWT. Je n'ai pas eu a implementer la gestion de mots de passe, la verification d'email, ou la reinitialisation de mot de passe. On y revient plus bas.
Storage. Les fichiers (documents PDF generes, pieces jointes) sont stockes dans Supabase Storage avec des politiques d'acces granulaires. Upload direct depuis le frontend, securise par les memes regles RLS que la base de donnees.
La securite : Row Level Security
Si je devais retenir une seule decision technique de ce projet, ce serait celle-la.
Row Level Security (RLS) permet de definir des regles d'acces directement au niveau de la base de donnees. Chaque requete SQL est automatiquement filtree en fonction de l'utilisateur connecte. Ce n'est pas une couche applicative -- c'est PostgreSQL lui-meme qui refuse de renvoyer des donnees qui n'appartiennent pas a l'utilisateur.
Pour un SaaS multi-tenant, c'est fondamental. Meme si ton code applicatif a un bug, meme si tu oublies un WHERE user_id = ... dans une requete, la base de donnees ne laissera jamais passer des donnees d'un autre utilisateur.
-- Activer RLS sur la table properties
ALTER TABLE properties ENABLE ROW LEVEL SECURITY;
-- Politique : un utilisateur ne voit que ses propres biens
CREATE POLICY "Users can view own properties"
ON properties
FOR SELECT
USING (user_id = auth.uid());
-- Politique : un utilisateur ne peut inserer que pour lui-meme
CREATE POLICY "Users can insert own properties"
ON properties
FOR INSERT
WITH CHECK (user_id = auth.uid());
-- Politique : un utilisateur ne peut modifier que ses propres biens
CREATE POLICY "Users can update own properties"
ON properties
FOR UPDATE
USING (user_id = auth.uid())
WITH CHECK (user_id = auth.uid());
auth.uid() est une fonction Supabase qui renvoie l'ID de l'utilisateur authentifie via le JWT. C'est transparent : tu ecris tes requetes normalement, et RLS filtre automatiquement.
Le piege, c'est que RLS s'applique a toutes les operations, y compris celles de ton backend. Il faut bien gerer les roles (service_role pour les operations admin, anon ou authenticated pour les utilisateurs). Et il faut tester chaque politique soigneusement -- j'y reviens dans la section retrospective.
L'authentification : magic links
J'ai fait un choix qui peut sembler inhabituel : pas de mot de passe. L'authentification se fait uniquement par magic link.
L'utilisateur entre son email, recoit un lien, clique dessus, et il est connecte. Pas de mot de passe a retenir, pas de "mot de passe oublie", pas de politique de complexite a gerer.
Pourquoi ce choix ?
Moins de friction a l'inscription. Pas de formulaire avec confirmation de mot de passe, pas de regles de complexite. L'utilisateur entre son email et il est dedans en 30 secondes.
Moins de support. Zero tickets "j'ai oublie mon mot de passe". Zero probleme de mots de passe reutilises. Zero risque de fuite de mots de passe hasches.
Securite delegee. Supabase Auth gere tout : generation du lien, expiration, echange de token. Le JWT resultant est signe et verifiable cote backend sans appel reseau supplementaire.
Cote backend, la verification du JWT est directe :
from fastapi import Depends, HTTPException
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from jose import jwt, JWTError
security = HTTPBearer()
async def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(security)
) -> dict:
try:
payload = jwt.decode(
credentials.credentials,
SUPABASE_JWT_SECRET,
algorithms=["HS256"],
audience="authenticated"
)
return payload
except JWTError:
raise HTTPException(401, "Token invalide")
Le seul inconvenient : l'utilisateur doit avoir acces a sa boite mail pour se connecter. En pratique, sur ce type de produit, ca n'a jamais ete un probleme.
La generation de documents PDF
Un SaaS de gestion immobiliere qui ne genere pas de documents, ca ne sert a rien. Les proprietaires ont besoin de quittances de loyer, d'avis d'echeance, de recapitulatifs annuels.
La generation se fait cote backend. Le processus est simple : les donnees sont recuperees en base, injectees dans un template, et le PDF est genere puis stocke dans Supabase Storage. L'utilisateur peut ensuite le telecharger ou l'envoyer directement par email au locataire.
J'ai opte pour une approche template HTML vers PDF. Ca permet de garder le controle sur le rendu sans avoir a manipuler des librairies PDF bas niveau. Le template est du HTML/CSS classique, ce qui rend la maintenance et les modifications accessibles.
Les documents generes sont stockes avec une convention de nommage qui permet de les retrouver facilement : type de document, identifiant du bien, periode concernee. Les politiques RLS de Supabase Storage garantissent que chaque utilisateur n'accede qu'a ses propres documents.
Le deploiement : Docker + VPS
J'aurais pu deployer sur Vercel (frontend) + un service serverless (backend). Mais j'ai choisi un VPS classique avec Docker Compose. Voici pourquoi.
Controle des couts. Un VPS a cout fixe, c'est previsible. Pas de surprise de facturation si le trafic augmente. Pour un SaaS en phase de lancement, la previsibilite financiere est aussi importante que la scalabilite technique.
Controle total. Je peux SSH sur la machine, inspecter les logs, debugger en production si necessaire. Pas de couche d'abstraction entre moi et mon application.
Docker Compose. Un fichier docker-compose.yml qui decrit toute l'infrastructure. SvelteKit, FastAPI, Nginx, backups -- tout est la, versionne, reproductible.
# docker-compose.yml (simplifie)
services:
frontend:
build: ./frontend
ports:
- "3000:3000"
environment:
- PUBLIC_API_URL=http://backend:8000
restart: unless-stopped
backend:
build: ./backend
ports:
- "8000:8000"
environment:
- SUPABASE_URL=${SUPABASE_URL}
- SUPABASE_KEY=${SUPABASE_KEY}
- JWT_SECRET=${JWT_SECRET}
restart: unless-stopped
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d
- ./certbot/conf:/etc/letsencrypt
depends_on:
- frontend
- backend
restart: unless-stopped
Nginx sert de reverse proxy et gere le SSL via Let's Encrypt. Un cron job quotidien sauvegarde la base de donnees. Un autre gere le renouvellement des certificats.
Est-ce que ca scale a des millions d'utilisateurs ? Non. Est-ce que ca suffit pour un SaaS en croissance ? Largement. Et quand le moment viendra de scaler, Docker Compose se migre facilement vers Kubernetes ou un orchestrateur manage.
Ce que j'aurais fait differemment
Apres plusieurs mois en production, voici ce que j'ai appris a mes depens.
J'ai commence avec trop de features. La premiere version avait des tableaux de bord, des graphiques, des exports, des notifications... J'aurais du shipper avec la gestion de biens, le suivi des loyers, et la generation de quittances. Point. Le reste peut attendre que des utilisateurs le demandent.
Les politiques RLS, ca se teste. Ecrire une politique RLS, c'est facile. S'assurer qu'elle couvre tous les cas (INSERT, UPDATE, DELETE, edge cases avec des sous-requetes), c'est une autre histoire. J'ai eu un bug ou un utilisateur pouvait voir le nombre total de biens dans le systeme (pas les donnees, juste le count) a cause d'une politique mal configuree sur une vue. Depuis, chaque nouvelle politique RLS est accompagnee de tests automatises.
Le monitoring, c'est pas optionnel. Au debut, mon monitoring se limitait a "est-ce que le serveur repond ?". La premiere fois qu'un utilisateur m'a signale un probleme que je n'avais pas vu dans mes logs, j'ai compris qu'il fallait investir dans du monitoring applicatif. Alertes sur les erreurs 500, suivi des temps de reponse, metriques metier (nombre de documents generes, taux d'erreur).
Les migrations de base de donnees, ca se planifie. Avec RLS, chaque changement de schema peut casser des politiques existantes. J'ai appris a toujours verifier les politiques RLS apres une migration, et a tester les acces utilisateur dans un environnement de staging avant de deployer en production.
L'email transactionnel, c'est un metier. Envoyer un magic link ou une quittance par email, ca parait simple. En pratique, la delivrabilite, les filtres anti-spam, les templates responsive... c'est un sujet a part entiere. Utiliser un service specialise (dans mon cas Resend) plutot que d'envoyer depuis son propre serveur, c'est un des meilleurs choix que j'ai faits.
Ressources
Si tu veux creuser les technologies mentionnees dans cet article :
- Documentation SvelteKit -- Le guide officiel est excellent et couvre le routing, le SSR, et le deploiement
- Documentation FastAPI -- Tutoriel complet avec exemples de validation, auth, et async
- Guide Row Level Security Supabase -- Indispensable pour comprendre les politiques multi-tenant
- Best practices Docker Compose -- Pour structurer proprement une infra multi-services
Si tu t'interesses aussi a l'IA, j'ai ecrit une serie complete sur ce qui s'est passe depuis ChatGPT et comment rattraper son retard.