He estado construyendo software profesionalmente por más de 10 años. He trabajado en grandes codebases de Angular en BMO, pasé dos años en el front end de una plataforma de tarjetas de crédito de consumo con más de 50.000 usuarios, y estoy construyendo un RPG táctico como proyecto hobby en solitario. Nada de eso me preparó para la combinación específica de desafíos que vino con construir mi propio SaaS desde cero, solo, mientras mantenía un rol de contrato exigente.
Este es el primer escrito sobre la construcción. El producto es MailHive, pero lo que estoy documentando es la experiencia de construirlo, no un pitch de producto. Lo que sí diré es que está potenciado por IA, resuelve un problema que he tenido personalmente durante años, y el stack principal es Angular en el front end, Python y FastAPI en el back end, PostgreSQL para datos, y Terraform sobre Scaleway para la infraestructura.
Este primer post es sobre los cimientos: las decisiones que tomé temprano sobre las que todo lo demás está construido, y lo que cambiaría si empezara de nuevo hoy.
Por qué decidí construir un SaaS
La respuesta honesta es que he querido hacerlo desde que era developer junior. La fantasía de construir algo útil, ponerlo en línea, y que genere valor mientras duermo nunca me abandonó del todo. Pero durante años racionalicé no empezar. No tenía una idea lo suficientemente buena. No tenía suficiente tiempo. El mercado estaba demasiado saturado. La idea era demasiado nicho.
Lo que cambió fue la ola de herramientas de IA de los últimos dos años. Un problema específico del que había sido consciente, uno que intenté resolver manualmente y nunca logré del todo, se volvió repentinamente solucionable con una combinación de capacidades de LLM y algunos data pipelines personalizados. Tenía el conocimiento del dominio. Tenía las habilidades de ingeniería. Tenía acceso a las APIs de IA. Las excusas se acabaron.
También quiero ser honesto sobre la motivación financiera. Los salarios de developer senior en Canadá son buenos. Pero el ingreso pasivo de un producto que posees es un tipo diferente de seguridad financiera, y construir uno se siente más alineado con hacia dónde va el software que seguir siendo puramente un implementador de features, por más senior que sea.
La decisión del tech stack
Elegir un tech stack para un proyecto nuevo es uno de los lugares más fáciles para pasar tres semanas sin avanzar. Me puse una regla estricta: elegir tecnologías que ya conozco bien, excepto una cosa nueva por área principal. El punto de este proyecto es lanzar, no aprender un framework nuevo.
Angular era innegociable para el front end. Construyo con él desde 2017. Conozco sus características de rendimiento, sus trampas, sus mejores prácticas. Cuanto más rápido pueda construir la UI, más rápido puedo validar si la idea del producto vale la pena continuar. Este no era el momento para probar React o Vue.
FastAPI para el backend. La versión corta: Python era necesario para los componentes de IA, y FastAPI me dio el camino más simple posible a una API de producción sin el overhead de Django. Conocía Python, y la validación de requests y la documentación OpenAPI generada de FastAPI, ambas basadas en type hints, significaron menos código de pegamento en un proyecto exploratorio de etapa temprana.
PostgreSQL fue elegido sobre toda otra opción de base de datos porque es la respuesta correcta para datos de negocio estructurados cuando no tienes una razón específica para usar algo más. Maneja columnas JSON para las partes flexibles, búsqueda de texto completo para las partes de contenido, e integridad relacional para todo lo demás. Sin arrepentimientos.
Scaleway para hosting, con la infraestructura definida en Terraform y los servicios entregados como contenedores Docker. El mismo razonamiento que el workflow de migraciones de base de datos que describo abajo: la infraestructura debería ser código que pueda leer, revisar y reconstruir, no clics de consola que tenga que recordar. GitLab CI corre los quality gates en cada merge request: reglas de lint, escaneos de seguridad, y Vitest con umbrales de cobertura de front end del 88 al 93%. La facturación de suscripciones corre sobre Paddle.
Decisiones de arquitectura tomadas temprano
La decisión temprana más importante fue tratar la app Angular y el backend FastAPI como deployments completamente separados desde el día uno. Sin server-side rendering de Angular, sin servir el build de Angular desde FastAPI, sin servidor localhost compartido en desarrollo. Dos procesos separados. Dos dominios separados en producción. Separación completa de responsabilidades.
Esto agrega complejidad de setup de desarrollo. Necesitas configurar CORS. Necesitas ejecutar dos servidores localmente. Pero te da flexibilidad de deployment, escalado independiente, y una frontera arquitectónica limpia que paga dividendos a medida que la aplicación crece. He trabajado en proyectos donde el frontend era servido por el backend y el acoplamiento que creaba era una fuente constante de dolor.
La segunda decisión temprana fue construir la capa de autenticación primero, completamente, antes de escribir cualquier feature de producto. La autenticación es lo más difícil de retrofittear, porque permisos, gestión de sesiones, refresh de tokens y cada flujo de inicio de sesión tienen que estar de acuerdo entre sí, y todos tocan código que ya escribiste. 'Completamente' significó algo concreto acá: el servicio FastAPI es dueño de la identidad, cada ruta protegida resuelve al usuario actual a través de una sola dependencia compartida en lugar de su propio check, el cliente renueva una sesión por expirar en un solo lugar en vez de por request, y las tablas de usuarios y permisos salieron en la primera migración de Alembic. No se escribió ningún endpoint de producto hasta que todo eso funcionaba de punta a punta, que es por lo que nunca tuve que retrofittear un check de auth en un endpoint existente.
La tercera decisión fue configurar el workflow de migraciones de base de datos el día uno, SQLAlchemy con Alembic. Cada cambio de schema pasa por un archivo de migración. Nunca, jamás, tocar la base de datos directamente en producción. Viniendo del desarrollo enterprise con Angular, donde damos los workflows basados en branches por sentado, me sorprendió cuántos tutoriales de SaaS sugieren simplemente modificar el schema directamente. No pude hacerlo.
# 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 appCómo luce la estructura de la app Angular
Fui con una estructura de carpetas basada en features desde el inicio, con standalone components lazy-loaded. Sin NgModules. La app arrancó en Angular 15, la versión que volvió estables las APIs standalone, y hoy está en Angular 21, con change detection OnPush y Tailwind CSS. Standalone fue el único lugar donde adopté un patrón más nuevo de Angular en lugar de recurrir a lo que ya conocía: el sistema de módulos se sentía como overhead innecesario para un proyecto de este tamaño.
El state management es Angular Signals con una capa delgada de servicios. Sin NgRx por ahora. La aplicación no es lo suficientemente compleja para justificar la ceremonia. Si llego a un umbral de complejidad donde los servicios basados en signals se vuelvan difíciles de razonar, introduciré una solución más estructurada. Pero me niego a agregar complejidad arquitectónica especulativamente.
// 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 capa de integración de IA
Los componentes de IA del producto viven completamente en el backend. El front end Angular nunca llama a una API de IA directamente: todas las llamadas de IA pasan por endpoints de FastAPI que manejan autenticación, rate limiting, construcción de prompts, y procesamiento de respuestas, sobre una capa de IA agnóstica del proveedor para que ninguna feature dependa de un solo vendor de modelos. Esto es intencional e importante.
Llamar a APIs de IA desde el navegador expondría mis API keys, haría imposible el rate limiting, y no me daría una capa de procesamiento del lado del servidor para transformar, cachear, o validar respuestas de IA antes de que lleguen al usuario. La capa de backend es delgada pero esencial: es la única forma de lanzar features de IA responsablemente sin quemar dinero en acceso directo a API sin medición.
Tengo una cola para operaciones de IA (respaldada por PostgreSQL, sin broker aparte), una capa de caching de respuestas para queries determinísticos, y rate limiting por usuario en el backend. Nada de esto es visible para la app Angular, que solo ve una API REST estándar que a veces tarda unos segundos en responder, más un stream de Server-Sent Events para actualizaciones en tiempo real, alimentado por LISTEN/NOTIFY de PostgreSQL.
Lo que desearía haber hecho diferente
Pasé demasiado tiempo en el entorno de desarrollo local antes de construir cualquier feature. Hacer que Docker Compose funcionara limpiamente para Postgres, FastAPI, y un servidor de desarrollo Angular, con hot reload para ambos servidores de aplicación, se comió mucho más tiempo de setup del que merecía. Podría haber lanzado features más rápido empezando con herramientas locales más simples y containerizando después.
También sobre-diseñé el formato de respuesta de API temprano. Creé un formato de envelope estandarizado para todas las respuestas de API, con metadata, paginación, y códigos de error. Es un buen patrón, pero dediqué tiempo a ello antes de tener datos reales para dar forma a las respuestas. Algunas de esas decisiones tempranas tuvieron que revisarse una vez que entendí cómo lucían los datos reales.
Lo que hice bien y volvería a hacer: escribir todo. Cada decisión que tomé la documenté como un registro de decisión, con el contexto y el razonamiento. Releyendo esos registros, puedo ver dónde evolucionó mi pensamiento y por qué ciertas elecciones arquitectónicas tenían sentido en contexto. También es lo más cercano que tengo a una especificación del sistema.
Dónde estoy ahora
Los cimientos son sólidos. La auth funciona. El schema de base de datos para el dominio core es estable. La app Angular cubre los flujos principales de usuario. La integración de IA es funcional y produce output útil. El producto está en vivo en beta.
Si estás construyendo algo similar, espero que esto sea útil. El viaje de indie dev es genuinamente diferente del desarrollo profesional, y las brechas en los consejos disponibles para esa combinación específica de niveles de experiencia y restricciones son reales.
Lo más difícil de un SaaS en solitario no es el código, es tomar cada decisión arquitectónica tú mismo sin un equipo para poner a prueba tu razonamiento, y lanzar features lo suficientemente rápido para validar tu idea antes de que se te acabe la motivación.
Referencias
- 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