⚡ AutomationsAI|Portal de Cursos →

Verificando acesso...

MÓDULO 1.2

🏗️ Arquitetura multi-plataforma

As peças que toda ferramenta de agendamento multi-rede precisa ter: adapters, filas, storage, banco, frontend e webhooks. O mapa antes do código.

6
Tópicos
45
Minutos
Básico
Nível
Teoria
Tipo
1

🔌 Padrão Adapter/Provider

Cada rede social tem uma API diferente: X usa OAuth 2.0 PKCE, LinkedIn pede UGC posts, Instagram exige container ID. Sem abstração, o código vira um switch gigante. O padrão Adapter resolve isso: uma classe por rede, todas implementando a mesma interface.

O resto da aplicação fala com a interface, não com a API específica. Adicionar TikTok amanhã = criar TikTokProvider e registrar no factory — zero mudança no agendador, na fila ou no frontend.

// Interface comum — contrato que TODA rede implementa
export interface SocialProvider {
  readonly platform: 'x' | 'linkedin' | 'instagram' | 'facebook' | 'tiktok';

  authenticate(code: string): Promise<AuthResult>;
  refreshToken(refreshToken: string): Promise<AuthResult>;
  post(account: SocialAccount, payload: PostPayload): Promise<PostResult>;
  delete(account: SocialAccount, externalId: string): Promise<void>;
  getMetrics(account: SocialAccount, externalId: string): Promise<Metrics>;
}

// Implementação concreta — uma por rede
export class XProvider implements SocialProvider {
  readonly platform = 'x' as const;

  async authenticate(code: string) { /* OAuth 2.0 PKCE */ }
  async post(account, payload) {
    // chama POST /2/tweets com bearer token
  }
  // ...
}

// Factory escolhe o provider certo em runtime
export function getProvider(platform: string): SocialProvider {
  return providers[platform]; // { x: new XProvider(), linkedin: ... }
}

💡 Dica prática

Mantenha a interface pequena. Se uma rede tiver feature exclusiva (ex: enquetes no X), não polua a interface comum — exponha via método específico no provider (XProvider.createPoll()) e cheque instanceof no caller. Interface inchada vira lixo de throw new Error("not supported").

Conceitos-chave

Interface

Contrato que define quais métodos toda rede deve ter.

Adapter

Traduz API específica para a interface comum.

Factory

Resolve qual provider instanciar em runtime.

Open/Closed

Aberto para extensão (nova rede), fechado para mudança.

2

⚙️ Fila com retry (BullMQ / Temporal)

Publicar post não é uma chamada síncrona. O usuário aperta "agendar para amanhã 9h" e o sistema precisa lembrar de publicar, tentar de novo se falhar, e não derrubar a app quando a API da rede estiver lenta. Filas resolvem os três.

BullMQ (Redis) cobre 95% dos casos: delay nativo, retry com backoff exponencial, jobs agendados via cron. Temporal entra quando você precisa de workflows duradouros (ex: thread no X com 10 tweets sequenciais com falha parcial).

// Agendar publicação para horário futuro
await postQueue.add(
  'publish-post',
  { postId: '42', target: 'linkedin' },
  {
    delay: scheduledAt.getTime() - Date.now(), // ms até disparar
    attempts: 5,
    backoff: { type: 'exponential', delay: 30_000 }, // 30s, 60s, 120s...
    removeOnComplete: { age: 86_400 }, // mantém logs 24h
  }
);

// Worker que processa
new Worker('post-queue', async (job) => {
  const provider = getProvider(job.data.target);
  return await provider.post(account, payload);
}, { concurrency: 4 });

✓ O que FAZER

  • Usar backoff: exponential — evita martelar API caída.
  • Limitar concurrency por worker para respeitar rate limits.
  • Tornar handlers idempotentes (mesmo job 2x = mesmo efeito).
  • Persistir status no DB depois de cada tentativa.

✗ O que NÃO fazer

  • Usar setTimeout em memória — morre no restart.
  • Retry infinito sem backoff — quebra o Redis e a API.
  • Tratar erro 4xx como retryable — refaz o mesmo erro 5x.
  • Misturar lógica de negócio no worker — dificulta teste.

Conceitos-chave

Job

Unidade de trabalho enfileirada para execução posterior.

Delay

Tempo antes do job ficar elegível para processar.

Backoff exponencial

Intervalo dobra a cada falha — protege APIs.

Idempotência

Mesmo job executado N vezes = mesmo resultado final.

3

🖼️ Storage de imagem (S3 / Cloudinary)

Quase nenhuma API aceita imagem em base64. X, LinkedIn e Instagram exigem URL pública ou upload prévio com multipart. Resultado: você precisa de um object storage antes de chamar o provider. Sem isso, qualquer post com foto falha.

O fluxo padrão é: usuário sobe arquivo → backend gera signed URL → frontend faz PUT direto no S3/R2/Cloudinary → backend salva a URL final no Post. Nada de proxy pela aplicação — desperdiça banda e timeout.

📊 Provedores e quando usar

  • AWS S3: padrão da indústria, barato, integra com CloudFront. Ideal para volume alto e equipe que já está na AWS.
  • Cloudflare R2: compatível com API S3, sem egress fee. Vence em custo se você serve muita imagem via CDN.
  • Cloudinary: além de armazenar, transforma (resize, crop, format) via URL. Útil para gerar mil tamanhos sem pipeline próprio.
  • Local (dev): /uploads em volume Docker. Zero infra para começar, mas não serve em produção multi-réplica.
// Backend gera signed URL — válida por 5 min, só PUT
const command = new PutObjectCommand({
  Bucket: 'mkblogs-media',
  Key: `${userId}/${nanoid()}.jpg`,
  ContentType: 'image/jpeg',
});
const uploadUrl = await getSignedUrl(s3, command, { expiresIn: 300 });

// Frontend faz upload direto, sem passar pelo backend
await fetch(uploadUrl, {
  method: 'PUT',
  headers: { 'Content-Type': 'image/jpeg' },
  body: file,
});

// Depois, ao postar:
// - X: media_id obtido via /1.1/media/upload.json
// - LinkedIn: registerUpload + PUT no asset URL
// - Instagram: image_url DEVE ser HTTPS público acessível por Meta

Dica prática

Instagram baixa a imagem do seu storage no momento da publicação. Se você usa bucket privado com signed URL curta, a URL pode expirar antes do Meta puxar — gere com expiresIn de pelo menos 1h, ou marque o objeto como público com ACL fixa.

Conceitos-chave

Object storage

Armazena blobs (imagens, vídeos) acessíveis via HTTP.

Signed URL

URL temporária com permissão embutida na query string.

Direct upload

Cliente sobe direto no storage, sem proxy pelo backend.

CDN

Cache de borda que entrega a mídia rápido global.

4

🗄️ Banco de dados (Prisma + Postgres)

A modelagem certa torna features futuras (cross-post, analytics, rascunhos) triviais. A errada te força a refatorar tudo no segundo mês. Cinco tabelas formam o núcleo: User, SocialAccount, Post, PostTarget e Media.

A separação Post (conteúdo lógico) × PostTarget (publicação por rede) é o que permite um post → várias redes com status independente em cada uma.

// schema.prisma — núcleo da modelagem
model User {
  id        String   @id @default(cuid())
  email     String   @unique
  accounts  SocialAccount[]
  posts     Post[]
}

model SocialAccount {
  id           String   @id @default(cuid())
  userId       String
  platform     String   // 'x', 'linkedin', 'instagram'...
  externalId   String   // ID da conta na rede
  accessToken  String   // criptografado em rest
  refreshToken String?
  expiresAt    DateTime?
  user         User     @relation(fields: [userId], references: [id])
  targets      PostTarget[]
  @@unique([userId, platform, externalId])
}

model Post {
  id          String   @id @default(cuid())
  userId      String
  content     String   // markdown / texto base
  scheduledAt DateTime
  status      String   // 'draft' | 'scheduled' | 'partial' | 'done' | 'failed'
  user        User     @relation(fields: [userId], references: [id])
  targets     PostTarget[]
  media       Media[]
}

model PostTarget {
  id              String   @id @default(cuid())
  postId          String
  socialAccountId String
  status          String   // 'pending' | 'publishing' | 'published' | 'failed'
  externalPostId  String?  // ID retornado pela rede após publicar
  error           String?
  publishedAt     DateTime?
  post            Post @relation(fields: [postId], references: [id])
  account         SocialAccount @relation(fields: [socialAccountId], references: [id])
}

model Media {
  id     String @id @default(cuid())
  postId String
  url    String // S3/R2/Cloudinary
  type   String // 'image' | 'video'
  post   Post   @relation(fields: [postId], references: [id])
}

Conceitos-chave

Relacional

Joins fortes entre User, Post e Target com FK.

Fan-out

Um Post → N PostTarget, cada um com status próprio.

Migrations

Prisma versiona schema, replay garante consistência.

Encryption at rest

Tokens OAuth nunca em texto plano no banco.

5

🎨 Frontend de agendamento

O frontend é o lugar onde o usuário sente se a ferramenta é boa. Quatro componentes definem a experiência: editor com contagem de chars por rede, preview fiel ao look de cada plataforma, calendário visual e drag-drop para reagendar sem clicar em menu.

1

Editor multi-rede com contador

X tem 280 chars, LinkedIn 3000, Instagram 2200. Mostre o limite da rede ativa em tempo real e destaque excesso em vermelho.

2

Preview fiel por plataforma

Card que imita o feed: avatar, nome, tempo, conteúdo e imagem renderizados com o CSS da rede. Reduz ansiedade de "como vai sair".

3

Calendário semanal/mensal

Cada slot mostra um chip colorido por rede. Bibliotecas: FullCalendar, react-big-calendar ou um grid custom com CSS Grid.

4

Drag-and-drop para reagendar

Arrastar o card no calendário dispara PATCH no Post, que reagenda o job na fila. Optimistic UI: muda na tela antes de confirmar.

💡 Dica prática

Sempre ofereça "texto base + override por rede". O usuário escreve uma versão geral, e pode customizar (mais hashtags no Instagram, formato thread no X) sem perder o resto. UX que diferencia ferramenta amadora de profissional.

Conceitos-chave

SSR/CSR

Next.js renderiza server, hidrata cliente — SEO + UX.

Optimistic UI

Atualiza tela antes do servidor confirmar — sensação fluida.

Componentização

Editor, preview, calendário cada um isolado e reutilizável.

Acessibilidade

Drag-drop deve ter alternativa via teclado.

6

🔔 Webhooks de callback

Algumas redes (Meta principalmente) confirmam publicação de forma assíncrona: você manda o post, recebe um ID temporário, e só algum tempo depois um webhook avisa "publicou OK" ou "rejeitou por política de conteúdo". Sem expor um endpoint de webhook, você fica adivinhando status.

O fluxo é: rede social → POST no seu /webhooks/instagram com payload assinado → você valida assinatura HMAC → atualiza PostTarget.status e notifica o frontend via WebSocket/SSE.

// Endpoint público para receber callbacks
app.post('/webhooks/instagram', async (req, res) => {
  // 1. Validar assinatura HMAC (NÃO confiar no payload sem isso)
  const sig = req.headers['x-hub-signature-256'];
  const expected = 'sha256=' + crypto
    .createHmac('sha256', process.env.META_APP_SECRET)
    .update(req.rawBody)
    .digest('hex');
  if (sig !== expected) return res.status(401).end();

  // 2. Processar evento
  for (const entry of req.body.entry) {
    await handleEvent(entry); // atualiza PostTarget
  }

  // 3. Responder 200 RÁPIDO — Meta retenta se demorar >5s
  res.status(200).end();
});

✓ O que FAZER

  • Sempre validar assinatura HMAC antes de processar.
  • Responder 200 em <2s — enfileirar processamento pesado.
  • Tornar handler idempotente — webhooks chegam duplicados.
  • Logar payload bruto por 7 dias para debug.

✗ O que NÃO fazer

  • Confiar em IP de origem como autenticação.
  • Fazer chamada síncrona ao DB lento no handler.
  • Retornar 500 quando o evento é desconhecido — vira retry storm.
  • Esquecer de expor o endpoint via HTTPS público.

Conceitos-chave

Webhook

HTTP callback que a rede faz no seu endpoint.

HMAC

Assinatura com chave secreta — prova autenticidade.

At-least-once

Webhook pode chegar duplicado — handler aguenta repetição.

Real-time UI

SSE/WebSocket empurra status novo pro browser.

🎯 Resumo do Módulo

Adapters por rede — uma classe por plataforma, interface comum, factory para resolver em runtime.
Fila com retry — BullMQ/Temporal com delay, backoff exponencial e jobs idempotentes.
Object storage — S3/R2/Cloudinary com signed URL e upload direto do cliente.
Modelagem Prisma — User, SocialAccount, Post, PostTarget, Media com fan-out por rede.
Frontend de agendamento — editor multi-rede, preview, calendário e drag-drop com optimistic UI.
Webhooks de callback — HMAC validado, 200 rápido, handler idempotente para confirmar status.

Próximo Módulo:

1.3 — Trade-offs: self-hosted vs SaaS, build vs buy