Eu desenvolvo software profissionalmente há mais de 10 anos. Trabalhei em codebases Angular grandes no BMO, passei dois anos no front-end de uma plataforma de cartão de crédito para consumidor com mais de 50.000 usuários, e estou construindo um RPG tático como projeto solo de hobby. Nada disso me preparou para a combinação específica de desafios que veio com construir meu próprio SaaS do zero, sozinho, enquanto mantinha um contrato exigente.
Este é o primeiro relato sobre a construção. O produto é o MailHive, mas o que estou documentando é a experiência de construir, não uma apresentação de produto. O que posso dizer é que é alimentado por IA, resolve um problema que eu pessoalmente tive por anos, e a stack principal é Angular no front-end, Python e FastAPI no back-end, PostgreSQL para dados e Terraform na Scaleway para a infraestrutura.
Este primeiro post é sobre a fundação: as decisões que tomei cedo sobre as quais todo o resto é construído, e o que eu mudaria se estivesse começando de novo hoje.
Por Que Decidi Construir um SaaS
A resposta honesta é que tenho querido isso desde que era desenvolvedor junior. A fantasia de construir algo útil, colocar online e ter isso gerando valor enquanto eu durmo nunca me abandonou completamente. Mas por anos eu racionalizei não começar. Não tinha uma ideia boa o suficiente. Não tinha tempo suficiente. O mercado era saturado demais. A ideia era nicho demais.
O que mudou foi a onda de ferramentas de IA dos últimos dois anos. Um problema específico do qual eu estava ciente, um que tentei resolver manualmente e nunca consegui completamente, se tornou subitamente solucionável com uma combinação de capacidades de LLM e alguns pipelines de dados customizados. Eu tinha o conhecimento de domínio. Tinha as habilidades de engenharia. Tinha acesso às APIs de IA. As desculpas acabaram.
Também quero ser honesto sobre a motivação financeira. Salários de desenvolvedor sênior no Canadá são bons. Mas renda passiva de um produto que você possui é um tipo diferente de segurança financeira, e construir um parece mais alinhado com para onde o software está indo do que continuar sendo puramente um implementador de features, por mais sênior que seja.
A Decisão da Stack Técnica
Escolher uma stack técnica para um projeto novo é um dos lugares mais fáceis para gastar três semanas sem fazer progresso. Me dei uma regra rígida: escolher tecnologias que eu já conheço bem, exceto por uma coisa nova por área principal. O ponto deste projeto é entregar, não aprender um framework novo.
Angular era inegociável para o front-end. Construo com ele desde 2017. Conheço suas características de performance, suas armadilhas, suas melhores práticas. Quanto mais rápido eu conseguir construir a UI, mais rápido posso validar se a ideia do produto vale a pena continuar. Esse não era o momento de experimentar React ou Vue.
FastAPI para o backend. A versão curta: Python era necessário para os componentes de IA, e o FastAPI me deu o caminho mais simples possível para uma API de produção sem a overhead do Django. Eu conhecia Python, e a validação de requisições e a documentação OpenAPI gerada do FastAPI, ambas guiadas por type hints, significaram menos código de cola em um projeto exploratório de estágio inicial.
PostgreSQL foi escolhido em vez de toda outra opção de banco de dados porque é a resposta certa para dados de negócios estruturados quando você não tem uma razão específica para usar outra coisa. Ele lida com colunas JSON para as partes flexíveis, busca full-text para as partes de conteúdo, e integridade relacional para todo o resto. Sem arrependimentos.
Scaleway para hospedagem, com a infraestrutura definida em Terraform e os serviços entregues como contêineres Docker. Mesmo raciocínio do workflow de migração de banco que descrevo abaixo: infraestrutura deveria ser código que eu consigo ler, revisar e reconstruir, não cliques de console que preciso lembrar. O GitLab CI roda os quality gates a cada merge request: regras de lint, scans de segurança e Vitest com limites de cobertura de front-end de 88 a 93%. A cobrança de assinatura roda no Paddle.
Escolhas de Arquitetura Feitas Cedo
A decisão inicial mais importante foi tratar o app Angular e o backend FastAPI como deploys completamente separados desde o primeiro dia. Sem server-side rendering do Angular, sem servir o build do Angular pelo FastAPI, sem servidor localhost compartilhado em desenvolvimento. Dois processos separados. Dois domínios separados em produção. Separação completa de responsabilidades.
Isso adiciona complexidade no setup de desenvolvimento. Você precisa configurar CORS. Precisa rodar dois servidores localmente. Mas te dá flexibilidade de deploy, escalonamento independente e uma fronteira arquitetural limpa que paga dividendos conforme a aplicação cresce. Já trabalhei em projetos onde o frontend era servido pelo backend e o acoplamento que isso criou era fonte de dor constante.
A segunda decisão inicial foi construir a camada de autenticação primeiro, completamente, antes de escrever qualquer feature de produto. Autenticação é a coisa mais difícil de adicionar depois, porque permissões, gerenciamento de sessão, refresh de token e cada fluxo de login precisam concordar entre si, e todos tocam código que você já escreveu. "Completamente" significou algo bem específico aqui: o serviço FastAPI é dono da identidade, toda rota protegida resolve o usuário atual por uma única dependência compartilhada em vez de uma verificação própria, o cliente renova uma sessão que está expirando em um único lugar em vez de por requisição, e as tabelas de usuário e permissão saíram na primeira migração do Alembic. Nenhum endpoint de produto foi escrito antes de tudo isso funcionar de ponta a ponta, e é por isso que nunca precisei adicionar uma verificação de auth em um endpoint existente.
A terceira decisão foi configurar o workflow de migração de banco de dados no primeiro dia, SQLAlchemy com Alembic. Toda mudança de schema passa por um arquivo de migração. Nunca, jamais, toque no banco de dados diretamente em produção. Vindo de desenvolvimento Angular enterprise, onde consideramos workflows baseados em branches como garantidos, me surpreendeu quantos tutoriais de SaaS sugerem simplesmente modificar o schema diretamente. Não consegui me convencer a fazer isso.
# 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 appComo É a Estrutura do App Angular
Fui com uma estrutura de pastas baseada em features desde o início, com standalone components carregados sob demanda. Sem NgModules. O app começou no Angular 15, a versão que tornou as APIs standalone estáveis, e hoje está no Angular 21, com change detection OnPush e Tailwind CSS. Standalone foi o único lugar onde adotei um padrão mais novo do Angular em vez de recorrer ao que já conhecia. O sistema de módulos parecia overhead desnecessário para um projeto deste tamanho.
O gerenciamento de estado é Angular Signals com uma camada de serviços fina. Sem NgRx por enquanto. A aplicação não é complexa o suficiente para justificar a cerimônia. Se eu atingir um limite de complexidade onde serviços baseados em signals se tornem difíceis de raciocinar, vou introduzir uma solução mais estruturada. Mas me recuso a adicionar complexidade arquitetural 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)
}
];A Camada de Integração com IA
Os componentes de IA do produto ficam inteiramente no backend. O front-end Angular nunca chama uma API de IA diretamente. Todas as chamadas de IA passam por endpoints FastAPI que lidam com autenticação, rate limiting, construção de prompts e processamento de respostas, em cima de uma camada de IA agnóstica de provedor para que nenhuma feature dependa de um único fornecedor de modelo. Isso é intencional e importante.
Chamar APIs de IA pelo navegador exporia minhas API keys, tornaria rate limiting impossível e me deixaria sem uma camada de processamento server-side para transformar, cachear ou validar respostas de IA antes de chegarem ao usuário. A camada de backend é fina mas essencial. É a única forma de entregar features de IA de forma responsável sem queimar dinheiro em acesso direto à API sem medição.
Tenho uma fila para operações de IA (apoiada no PostgreSQL, sem broker separado), uma camada de cache de respostas para queries determinísticas e rate limiting por usuário no backend. Nada disso é visível para o app Angular, que apenas vê uma API REST padrão que às vezes demora alguns segundos para responder, mais um stream de Server-Sent Events para atualizações em tempo real, alimentado pelo LISTEN/NOTIFY do PostgreSQL.
O Que Eu Gostaria de Ter Feito Diferente
Gastei tempo demais no ambiente de desenvolvimento local antes de construir qualquer feature. Fazer o Docker Compose funcionar de forma limpa para Postgres, FastAPI e um servidor de desenvolvimento Angular, com hot reload para os dois servidores de aplicação, comeu muito mais tempo de setup do que merecia. Poderia ter entregado features mais rápido começando com ferramentas locais mais simples e containerizando depois.
Também projetei demais o formato de resposta da API cedo. Criei um formato de envelope padronizado para todas as respostas da API, com metadata, paginação e códigos de erro. É um bom padrão, mas gastei tempo nele antes de ter dados reais para moldar as respostas. Algumas dessas decisões iniciais tiveram que ser revisitadas quando entendi como os dados realmente eram.
O que acertei e faria de novo: documentar tudo. Cada decisão que tomei, documentei como um registro de decisão, com o contexto e o raciocínio. Relendo esses registros, consigo ver onde meu pensamento evoluiu e por que certas escolhas arquiteturais fizeram sentido no contexto. Também é a coisa mais próxima de uma especificação que tenho para o sistema.
Onde Estou Agora
A fundação está sólida. Auth funciona. O schema do banco de dados para o domínio principal está estável. O app Angular cobre os principais fluxos de usuário. A integração com IA é funcional e produzindo output útil. O produto está no ar em beta.
Se você está construindo algo similar, espero que isso seja útil. A jornada indie dev é genuinamente diferente do desenvolvimento profissional, e as lacunas nos conselhos disponíveis para essa combinação específica de níveis de experiência e restrições são reais.
A parte mais difícil de um SaaS solo não é o código. É tomar cada decisão arquitetural sozinho sem um time para testar seu raciocínio sob pressão, e entregar features rápido o suficiente para validar sua ideia antes de perder a motivação.
Referências
- 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