Tecnologia e negócio··12 min de leitura

Como construímos em 2 semanas uma plataforma de live shopping que fez R$ 680 mil em 5 horas

Construímos do zero uma plataforma completa de live shopping em duas semanas, com três pessoas. Rodou R$ 680 mil em pedidos em cinco horas de transmissão, com mais de 200 usuários simultâneos e zero downtime. O que segurou não foi gente nem hype, foi decisão de arquitetura na primeira tentativa.

TL;DR: construímos do zero uma plataforma completa de live shopping em duas semanas. Ela rodou R$ 680 mil em pedidos durante cinco horas de transmissão ao vivo, com mais de 200 usuários simultâneos, capacidade para até 12 mil conexões WebSocket, mais de 120 pedidos e zero downtime. Éramos três pessoas.

O contexto: um prazo impossível

Na Alva eu brinco que existe o "tempo Alva". Ele corre num ritmo diferente do resto do mundo: enquanto lá fora passaram 8 horas, na Alva já se passaram 30 dias. É insano em vários momentos, e é também o que faz coisa incrível acontecer. Foi nesse contexto que chegou o briefing, e a mensagem era clara: precisávamos de uma plataforma de live shopping pronta em duas semanas.

Não era uma landing page. Era um sistema completo para uma live B2B, atacado mesmo, e tinha que ter transmissão ao vivo com produtos em tempo real, carrinho anônimo sem fricção de cadastro, checkout simplificado, painel de gestão para controlar a live e os pedidos, chat em tempo real entre cliente e gestor, notificações instantâneas, e tudo isso escalável para milhares de pessoas ao mesmo tempo.

A primeira reação foi "impossível, precisamos de um parceiro que resolva isso no prazo". Contatamos três plataformas. A primeira respondeu três dias antes do evento. Se a live dependesse disso, ela não teria acontecido. Então, depois do "impossível", veio o "vamos fazer".

O desafio não era só código

O maior desafio não era escrever código rápido. Era tomar as decisões de arquitetura certas logo no início, porque não haveria tempo para refatoração. Cada escolha precisava ser rápida de implementar, escalável, confiável a ponto de aguentar uma live com muito dinheiro em jogo, e segura.

Aqui uns anos de estrada ajudam. Minha líder direta, que não é da área de TI, brinca que eu sou "pragmático demais", e ela está certa. Comecei com um board que já nasceu com 190 tarefas. A primeira era definir a stack.

Arquitetura: a fundação

A stack, boring de propósito

Escolhemos uma arquitetura moderna e battle-tested. Nada de hype, nada da novidade do vídeo de ontem no YouTube. Foco no essencial.

No frontend, Next.js 15 com React 19 e TypeScript, TailwindCSS, WebSocket nativo e Context API para estado global. No backend, Django 5.2 com Django REST Framework, Django Channels 4 para WebSocket assíncrono, PostgreSQL 16 e Redis para cache e channel layer. Na infra, uma EC2 t3.2xlarge (8 cores, 32GB), RDS PostgreSQL Multi-AZ, S3 para imagens, Vercel para o frontend e Docker Compose para orquestrar. O streaming saiu pelo StreamYard fazendo o switching, com o YouTube como broadcaster.

Por que Django e Next.js

Django entrou pelo que ele resolve de graça: o admin pronto economizou uns 10 dias de desenvolvimento, o ORM permite modelar rápido, o ecossistema é maduro (DRF, Channels, SimpleJWT) e ele tem suporte nativo a WebSocket via Channels. Next.js entrou pelo SSR, pelo App Router e, principalmente, pelo deploy trivial na Vercel.

Por mais que eu não goste de desenvolver em JavaScript, e menos ainda em TypeScript, não dá pra negar que a Vercel faz um baita trabalho de resiliência e disponibilidade a um custo baixo, e com uma facilidade de deploy que, no meio de um prazo desses, vale muito.

O maior desafio técnico: tempo real em escala

Numa live shopping tudo precisa ser instantâneo. Produto aparece e some em segundos, estoque muda a cada compra, o gestor precisa ver o pedido na hora, o cliente manda mensagem e espera resposta, e não havia previsão de público: podiam ser 50 pessoas, podiam ser mil. HTTP com polling seria um pesadelo. Precisávamos de WebSocket.

Montamos três canais WebSocket independentes: um canal geral, que atualiza produtos e estoque para todos os conectados; um canal de chat, onde cada conversa cliente-gestor é isolada por UUID (nada de telefone ou id fácil de adivinhar, por LGPD); e um canal de notificações do gestor, autenticado por JWT.

A decisão de que eu mais gosto aqui foi usar os signals do Django como fonte única de verdade do broadcast. Em vez de espalhar group_send por todo canto, um signal centraliza: quando uma mensagem é salva, ele decide para quais canais mandar e ainda invalida o cache dos contadores.

# backend/signals.py
@receiver(post_save, sender=Message)
def message_saved(sender, instance, created, **kwargs):
    if not created:
        return
    channel_layer = get_channel_layer()

    # manda para o canal do chat
    async_to_sync(channel_layer.group_send)(chat_group(instance), {
        "type": "chat_message",
        "message": MessageSerializer(instance).data,
    })

    # se não veio do gestor, notifica os gestores
    if not instance.is_from_gestor:
        async_to_sync(channel_layer.group_send)(notif_group(instance), {
            "type": "new_message_notification",
            "conversation_id": str(instance.conversation.conversation_id),
        })

    cache.delete(counts_key(instance))

Resultado: zero mensagem duplicada, broadcast consistente, lógica num lugar só.

Escalabilidade: uma máquina, dois modos

Django tradicionalmente roda em WSGI (Gunicorn), que é síncrono e não fala WebSocket. Para WebSocket, você precisa de ASGI. Mas rodar tudo em ASGI seria desperdiçar recurso nas requisições HTTP simples. E, pra quem já roeu osso, aqui bate também o velho medo do "pode dar m*rda". Como a AWS deixa você ligar qualquer tamanho de máquina pelo tempo que quiser, preferimos gastar um pouco mais a morrer economizando trocado.

A solução foi separar as responsabilidades na mesma máquina. Gunicorn na porta 8000 servindo a API REST síncrona (8 workers, ~120 req/s). Daphne na porta 8001 servindo os WebSockets, com seis réplicas configuradas no Docker Compose, cada uma aguentando ~2 mil conexões, num total teórico de 12 mil conexões simultâneas. O Nginx faz o roteamento: /api/ vai pro Gunicorn, /ws/ vai pro Daphne com balanceamento entre as réplicas.

location /api/ { proxy_pass http://backend:8000; }

upstream daphne_backend {
    least_conn;
    server daphne_1:8001;
    # ... daphne_2 a daphne_6
}

location /ws/ {
    proxy_pass http://daphne_backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
}

O Redis entra como Pub/Sub entre os processos, que é o que faz o broadcast funcionar quando você tem várias réplicas de Daphne. Sem isso, uma mensagem publicada numa réplica não chegaria a quem está conectado na outra.

Segurança: checkout sem cadastro, mas sem brecha

O pedido do comercial e do marketing foi zero fricção: sem login, sem senha, sem confirmar e-mail. A pergunta que isso levanta é óbvia. Com checkout aberto e API pública, como garantir que um rato não vá pro queijo do vizinho? De novo, o básico resolve: UUID e tokens.

Montamos três tokens. Um session token para o carrinho, gerado com secrets.token_urlsafe(32) e exigido no header de toda operação seguinte, então ninguém mexe num carrinho que não é o seu. Um read token para o cliente acompanhar o pedido depois do checkout, sem precisar de conta. E JWT com refresh token para o painel do gestor. Em cima disso, rate limiting agressivo por endpoint (5 tentativas de login por minuto contra brute-force, teto de checkouts por hora, throttle no chat contra spam) e os headers de segurança de sempre (HSTS, cookies seguros, nosniff).

Nada disso é sofisticado. É o feijão com arroz bem feito, que é o que segura uma operação real.

RDS Multi-AZ: onde eu não quis economizar

Cheguei a considerar rodar o PostgreSQL no próprio EC2. Mas com muito dinheiro em jogo e sem tempo nem equipe para construir uma solução elástica na mão, não havia margem pra erro. O RDS Multi-AZ da AWS caiu como uma luva: backup automático, failover em menos de dois minutos, replicação síncrona para uma zona secundária e snapshot manual antes da live.

Um detalhe que rendeu muito: um índice composto certo nas queries de conversa derrubou o tempo de 200ms para 15ms. Índice é barato e a diferença numa tela de gestor durante a live é sentida no dedo.

A dívida técnica que achamos 4 dias antes

Nem tudo foram flores. Em alguns testes operacionais percebemos que o WebSocket estava recebendo requisição demais. Sem tempo pra montar uma bateria de testes automatizados, foi o bom e velho console do navegador, cruzado com o log do Django Channels, que mostrou o problema: várias telas faziam conexões desnecessárias à API e ao WebSocket.

No painel do gestor, cada troca de filtro de pedidos (Novos, Em Atendimento) carregava todos os pedidos, uns 50KB, e filtrava no frontend. Isso tinha nascido de um contador que a gente montou no começo e que, na correria, ficou ali como dívida técnica. Somado a um auto-refresh a cada 10 segundos, dava 36MB de tráfego por hora. Vimos isso quatro dias antes da live.

A correção foi mover o filtro pro backend e criar um endpoint de contadores que faz uma query só, com agregação, e responde uns 100 bytes:

@api_view(["GET"])
@gestor_required
def gestor_orders_counts(request, slug):
    live = LiveShopping.objects.get(slug=slug)
    counts = Order.objects.filter(live_shopping=live).aggregate(
        novo=Count("id", filter=Q(status="novo")),
        em_atendimento=Count("id", filter=Q(status="em_atendimento")),
        concluido=Count("id", filter=Q(status="concluido")),
        todos=Count("id"),
    )
    return Response(counts)  # ~100 bytes

De 50KB por troca de tela para 100 bytes. O auto-refresh de 36MB/hora saiu do caminho. Foi uma dívida boba, do tipo que só se paga quando alguém para pra olhar o console.

Vercel: o deploy que eu não tinha tempo de configurar

Com o prazo apertado, não dava pra gastar tempo configurando Nginx, SSL e CDN pro frontend. A Vercel resolveu tudo: deploy automático via git push, CDN global, SSL automático, preview por branch para QA e rollback em um clique. Frontend em produção em menos de um minuto. É o tipo de coisa que, num prazo desses, compra tempo que você não tem.

O dia da live

Nas 24 horas antes: snapshot do RDS, teste de carga com 500 usuários simultâneos no k6, CloudWatch configurado e plano de contingência escrito. Uma hora antes, tudo verde e a máquina ociosa a 12% de CPU.

Durante as cinco horas, o pico foi de 212 usuários simultâneos, com cerca de 2 mil conexões WebSocket ativas. A CPU chegou a 87-99%, a memória ficou estável em 6GB de 32GB, a latência média do WebSocket ficou em 45ms e a da API em 120ms. Zero downtime.

O que ficou de tudo isso

O que mais me marcou não foi o número. Foi ter feito isso com três pessoas: eu, um dev júnior de frontend, e um segundo júnior que ajudou parcialmente no QA. Sem DevOps dedicado, sem QA formal, sem designer full-time. Três coisas explicam isso, e nenhuma é sorte.

A primeira é que experiência em arquitetura vale mais que quantidade de gente. Quando você já bateu a cabeça em problemas parecidos, não precisa testar cinco abordagens pra achar a que funciona. Você já sabe que WebSocket pede ASGI e HTTP comum roda melhor em WSGI, que Redis resolve o broadcast entre processos, que rate limiting se configura antes da live e não depois. Isso me deixou acertar a arquitetura na primeira tentativa, e refatorar sob pressão é muito mais caro do que decidir bem no começo. É a mesma conta de sempre: o barato de pular o planejamento sai caro na virada.

A segunda é que ferramenta comprovada ganha do hype. Django, PostgreSQL, Redis, RDS. Coisas boring, com muitos anos de produção validando que funcionam. Meu trabalho não foi provar que elas aguentam, foi combiná-las. Isso libera todo o tempo que você gastaria caçando bug obscuro de tecnologia nova pra gastar resolvendo o problema do cliente.

A terceira é foco no essencial e coragem de cortar o resto. Com três pessoas e duas semanas, cortamos autenticação complexa (checkout anônimo), painel customizado (Django Admin adaptado), animação sofisticada e teste unitário completo (focamos em teste de carga). Ficamos com o que a live não perdoava: WebSocket robusto, segurança, capacidade e plano de contingência. Experiência é, em boa parte, saber separar o crítico do cosmético.

Não vou fingir que não teve as 16 horas por dia, teve. Mas o que segurou os R$ 680 mil não foi a jornada, nem uma equipe enorme, nem recurso ilimitado. Foi decidir a arquitetura certa cedo, usar ferramenta chata que já funciona, e cortar tudo que não era essencial. O resto, honestamente, foi o time certo resolvendo o problema certo.