# Como manter vários SaaS em produção sendo dev solo sem virar refém do on-call

> Manter vários produtos sozinho depende mais de operação que de código: mesma stack, alertas por sintoma, runbooks e uma lista clara do que não construir.

Por Lucas Roberto · Carreira · publicado em 2026-10-09 · 10 min de leitura
Versão web: https://lucasroberto.com/blog/como-manter-varios-saas-em-producao-sendo-dev-solo-sem-virar-refem-do-on-call

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:

1. Toda decisão técnica tem um custo de manutenção recorrente, e você paga sozinho.
2. Conhecimento que só existe na sua cabeça é dívida. Você vai esquecer.
3. 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:

1. Health checks e alertas.
2. Recebimento e deduplicação de webhooks.
3. Clientes HTTP de provedores usados em mais de um produto, com retry e log padronizados.
4. 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:

```json
{
    "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](/blog/webhooks-de-pagamento-duplicados-e-fora-de-ordem-como-processar-cada-evento-uma-unica-vez-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:

1. Tempo de espera da fila acima do limite.
2. Jobs falhando acima do normal.
3. Ausência de eventos que deveriam acontecer (webhook que parou de chegar, agendamento que não foi enviado).
4. Erros 5xx acima de um patamar.
5. 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:

```php
'waits' => [
    'redis:default' => 60,
    'redis:webhooks' => 30,
],
```

E no `HorizonServiceProvider` aponta para onde a notificação vai:

```php
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:

```php
// 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:

```php
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:

1. **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](/blog/como-evitar-race-condition-no-saldo-da-conta-digital-com-laravel-e-postgresql).
2. **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.
3. **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](/blog/conciliacao-de-pagamentos-no-laravel-como-bater-o-extrato-do-provedor-com-o-seu-banco-todo-dia).

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:

```markdown
# 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:

1. **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.
2. **Ela exige operação manual recorrente?** Se precisa de alguém olhando toda semana, esse alguém sou eu.
3. **Dá para resolver com configuração em vez de código?** Uma flag por cliente custa menos do que um fluxo novo.
4. **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.
5. **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:

1. **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.
2. **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](/blog/jobs-disparados-dentro-de-transacao-o-bug-silencioso-do-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:

1. Liste os três incidentes que mais te custaram tempo no último trimestre e escreva um runbook para cada.
2. Configure os `waits` do Horizon e um alerta de ausência para o evento externo mais crítico de cada produto.
3. Mova para um pacote compartilhado só o código de operação que você já copiou entre projetos pelo menos duas vezes.
4. 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.

---

Lucas Roberto é desenvolvedor back-end e fundador da Granpag. Mais textos: https://lucasroberto.com/blog
