Manter vários produtos SaaS em produção sendo o único dev é viável, mas só se você tratar operação como parte do produto: uma stack só, código de infraestrutura compartilhado, alertas que disparam por sintoma e não por ruído, e runbooks escritos para o você cansado das três da manhã. O limite de um dev solo raramente é a capacidade de escrever código. É a quantidade de coisas que podem quebrar ao mesmo tempo sem ninguém olhando.
Eu mantenho o Horalis (agendamento com WhatsApp), o Vinea (gestão de paróquias) e a Granpag (fintech com maquininha e conta digital). São domínios bem diferentes, mas o que aprendi é que a forma de mantê-los vivos precisa ser a mesma. Este texto é sobre as decisões que tornam isso sustentável e os trade-offs de cada uma.
Por que o gargalo do dev solo é operação, e não código?
Quando você tem um produto, cada incidente tem sua atenção total. Com três, incidentes se sobrepõem, e o custo de trocar de contexto passa a dominar o seu dia. O problema não é escrever a feature nova. É lembrar como o deploy do produto B funciona, qual fila do produto C costuma travar e onde ficam os logs do produto A.
On-call, o regime em que alguém responde a incidentes fora do horário, numa empresa é uma escala. Para o dev solo é permanente. Então a pergunta de carreira deixa de ser "quanto eu consigo entregar?" e vira "quanto eu consigo entregar sem aumentar a superfície de coisas que me acordam?".
Três consequências práticas:
- Toda decisão técnica tem um custo de manutenção recorrente, e você paga sozinho.
- Conhecimento que só existe na sua cabeça é dívida. Você vai esquecer.
- Automatizar a detecção de problemas vale mais do que automatizar a correção.
Qual stack escolher quando você mantém mais de um produto sozinho?
A mesma para todos, sempre que possível. Eu trabalho com Laravel, Node.js, Python, React e Flutter, mas saber várias tecnologias é diferente de operar várias em produção. No back-end, o padrão é Laravel com PostgreSQL, Redis e Horizon (o painel e supervisor de filas do Laravel). Flutter entra onde existe app mobile, React onde o front pede. Node e Python só quando há um motivo concreto, como uma biblioteca que só existe naquele ecossistema.
| Decisão | Ganho | Custo |
|---|---|---|
| Mesma stack em todos os produtos | Deploy, monitoramento e debug idênticos; troca de contexto mínima | Às vezes a ferramenta não é a ideal para aquele problema |
| Monolito em vez de microsserviços | Um deploy, uma base, uma fila para observar | Escalar partes isoladas exige mais cuidado |
| PostgreSQL para quase tudo (inclusive filas leves e busca vetorial) | Menos serviços para manter e fazer backup | Limites de performance aparecem mais cedo em alguns casos |
| Serviços gerenciados (banco, Redis, storage) | Backup e atualização não são problema seu | Custo mensal maior e menos controle |
O critério que uso: uma tecnologia nova só entra se o problema que ela resolve for maior do que o custo de ter mais uma coisa para monitorar, atualizar e entender quando quebrar. Microsserviços para um dev solo quase sempre falham nesse teste. Você troca chamadas de método por chamadas de rede e ganha um sistema distribuído para depurar sozinho.
Como compartilhar código entre produtos sem criar um monstro?
A tentação é extrair tudo que parece comum para um pacote. Não faça isso. Regra de negócio quase nunca é realmente compartilhada: o agendamento do Horalis e uma cobrança da Granpag só parecem parecidos de longe.
O que vale compartilhar é a camada de operação, aquilo que é igual por natureza:
- Health checks e alertas.
- Recebimento e deduplicação de webhooks.
- Clientes HTTP de provedores usados em mais de um produto, com retry e log padronizados.
- Middlewares de logging com correlation ID (um identificador que acompanha a requisição por logs, jobs e chamadas externas).
Isso vira um pacote Composer privado, versionado com tags semânticas e instalado via repositório VCS:
{ "repositories": [ { "type": "vcs", "url": "git@github.com:seu-usuario/laravel-ops.git" } ], "require": { "seu-usuario/laravel-ops": "^1.4" } }
O trade-off é real: um bug no pacote afeta todos os produtos, e uma mudança quebrando compatibilidade obriga a atualizar todos. Por isso trato o pacote com mais rigor que os produtos: testes obrigatórios, changelog e nada de lógica de domínio lá dentro. Se um trecho precisa de um if ($produto === 'granpag'), ele não pertence ao pacote.
Um exemplo de algo que mora bem no pacote é a lógica de processar cada webhook uma única vez. Escrevi em detalhe sobre isso em como processar webhooks de pagamento duplicados e fora de ordem no Laravel, e é exatamente o tipo de código que eu não quero reimplementar três vezes.
Que alertas um dev solo realmente precisa?
Alerta demais é tão ruim quanto nenhum: depois da décima notificação irrelevante, você para de olhar. A regra que sigo é alertar por sintoma que o usuário sente, não por causa interna. CPU alta não é problema se as filas estão andando. Fila parada é problema mesmo com CPU baixa.
Minha lista mínima por produto:
- Tempo de espera da fila acima do limite.
- Jobs falhando acima do normal.
- Ausência de eventos que deveriam acontecer (webhook que parou de chegar, agendamento que não foi enviado).
- Erros 5xx acima de um patamar.
- Divergência na conciliação diária.
O primeiro o Horizon resolve nativamente. No config/horizon.php, você define o tempo máximo de espera por fila:
'waits' => [ 'redis:default' => 60, 'redis:webhooks' => 30, ],
E no HorizonServiceProvider aponta para onde a notificação vai:
Horizon::routeMailNotificationsTo(config('ops.alert_email'));
O terceiro item é o mais subestimado. Erro gera exceção e aparece no log. Silêncio não gera nada. Se o provedor de pagamento parou de mandar webhooks por uma mudança de configuração ou um certificado expirado, nenhum erro vai aparecer do seu lado. Por isso tenho checagens de ausência agendadas:
// routes/console.php use App\Models\WebhookEvent; use App\Support\Alert; use Illuminate\Support\Facades\Schedule; Schedule::call(function () { $recebeuAlgo = WebhookEvent::query() ->where('provider', 'pagarme') ->where('received_at', '>=', now()->subMinutes(30)) ->exists(); if (! $recebeuAlgo) { Alert::critical('Nenhum webhook da Pagar.me nos últimos 30 minutos'); } }) ->name('check-webhooks-pagarme') ->everyTenMinutes() ->between('08:00', '20:00') ->withoutOverlapping();
O between existe porque fora do horário comercial o volume cai e o silêncio é normal. Calibrar a janela é o que separa alerta útil de alerta que você vai silenciar em uma semana.
A classe Alert é propositalmente simples e mora no pacote compartilhado:
namespace App\Support; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; class Alert { public static function critical(string $message): void { Log::critical($message); Http::timeout(5)->post(config('ops.alert_webhook'), [ 'text' => sprintf('[%s] %s', config('app.name'), $message), ]); } }
O nome do app no prefixo parece detalhe, mas com três produtos mandando alerta para o mesmo canal, saber de onde veio sem abrir nada economiza minutos preciosos.
Como dormir tranquilo com dinheiro passando pelo sistema?
Em produtos como Horalis e Vinea, um bug costuma significar uma tela errada ou uma notificação atrasada. Na Granpag, um bug pode significar saldo errado. Para o dev solo isso muda a estratégia: você não consegue revisar tudo com a profundidade de um time, então precisa de defesas estruturais que funcionem mesmo quando você erra.
As que considero inegociáveis:
- Consistência garantida pelo banco, não pela aplicação. Locks, constraints e transações no PostgreSQL protegem contra o que você não previu. O caso mais clássico é o saldo, que detalhei em como evitar race condition no saldo da conta digital com Laravel e PostgreSQL.
- Idempotência em toda operação que mexe com dinheiro. Idempotência é a garantia de que executar a mesma operação duas vezes tem o mesmo efeito que executar uma. Retries, webhooks repetidos e cliques duplos acontecem, e você não estará acordado para corrigir à mão.
- Uma rede de segurança que roda todo dia. A conciliação compara o que o provedor diz que aconteceu com o que seu banco registrou. Ela é o alerta que pega tudo que os outros deixaram passar. Expliquei o processo em conciliação de pagamentos no Laravel.
A conciliação é especialmente valiosa para quem trabalha sozinho porque inverte o modelo: em vez de você precisar perceber o problema, o sistema te mostra a divergência com contexto suficiente para agir.
Como escrever runbooks que funcionam às três da manhã?
Runbook é um documento curto com o passo a passo para diagnosticar e resolver um incidente conhecido. Em time, ele serve para quem não conhece aquele sistema. Para o dev solo, serve para você mesmo daqui a seis meses, sem contexto e com sono.
O formato que funciona para mim é sempre o mesmo, um arquivo por alerta, dentro do repositório do produto:
# Alerta: nenhum webhook da Pagar.me em 30 min ## Impacto Pagamentos aprovados não atualizam o status do pedido. ## Checar primeiro 1. Status page do provedor. 2. Logs do endpoint /webhooks/pagarme nos últimos 60 min. 3. Configuração de webhook no dashboard do provedor. ## Mitigar Rodar `php artisan pagamentos:sincronizar --desde="2 hours ago"`. ## Não fazer Não reprocessar eventos manualmente sem checar a tabela de idempotência.
A seção "Não fazer" é a mais importante. Incidente é quando você toma decisões ruins com pressa, e documentar o erro óbvio que você quase cometeu da última vez evita repeti-lo.
Um efeito colateral bom: escrever o runbook frequentemente revela que falta um comando de mitigação. O pagamentos:sincronizar do exemplo é o tipo de ferramenta que nasce de um runbook, não de um planejamento de sprint.
Como decidir o que não construir?
Essa é a habilidade de carreira mais importante para quem trabalha sozinho, e a mais difícil. Todo produto tem uma fila infinita de pedidos razoáveis. Os critérios que aplico antes de aceitar uma feature:
- Ela cria um novo tipo de falha? Uma integração nova com um provedor é mais uma fonte de webhooks, de indisponibilidade e de mudança de API sem aviso.
- Ela exige operação manual recorrente? Se precisa de alguém olhando toda semana, esse alguém sou eu.
- Dá para resolver com configuração em vez de código? Uma flag por cliente custa menos do que um fluxo novo.
- Existe serviço pronto que resolve bem? KYC (verificação de identidade do cliente) é o exemplo clássico: integrar com a Sumsub é muito mais sensato do que construir, mesmo pagando por isso.
- Ela pode ser removida depois? Features em produção dificilmente morrem, então entram com mais critério.
Dizer não é desconfortável, principalmente quando o pedido vem de um cliente que paga. Mas uma feature mal operada é pior do que nenhuma: ela gera suporte, incidente e desconfiança.
Deploy e rollback precisam ser chatos
O último ponto é o mais simples e o mais negligenciado. Se o deploy de cada produto funciona de um jeito, você vai errar. O meu é idêntico nos três: push na branch principal, CI rodando testes, deploy com migrations e restart dos workers do Horizon via php artisan horizon:terminate, que deixa o supervisor subir os processos com o código novo.
Duas regras evitam a maioria dos problemas:
- Migrations sempre compatíveis com a versão anterior do código. Remover coluna acontece em dois deploys: primeiro o código para de usar, depois a coluna sai. Assim o rollback do código nunca depende de reverter o banco.
- Nada de deploy na sexta à noite em produto financeiro. Não é superstição, é disponibilidade: se der problema, quem resolve é você no fim de semana.
Outro cuidado que vale incluir no checklist de revisão: jobs disparados dentro de transações, que podem rodar antes do commit e ler dados que ainda não existem. Detalhei o bug em jobs disparados dentro de transação e o afterCommit no Laravel, e é o tipo de erro que só aparece em produção, sob carga.
O que fazer na segunda-feira
Se você mantém mais de um sistema sozinho, ou caminha para isso, não tente aplicar tudo de uma vez. Uma ordem que funciona:
- Liste os três incidentes que mais te custaram tempo no último trimestre e escreva um runbook para cada.
- Configure os
waitsdo Horizon e um alerta de ausência para o evento externo mais crítico de cada produto. - Mova para um pacote compartilhado só o código de operação que você já copiou entre projetos pelo menos duas vezes.
- Antes de aceitar a próxima feature, passe ela pelos cinco critérios acima e anote a resposta.
A carreira de dev solo com vários produtos não escala com mais horas. Escala quando cada produto exige menos de você para continuar de pé, e isso se constrói com decisões chatas, repetidas e documentadas.