🔌 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
Contrato que define quais métodos toda rede deve ter.
Traduz API específica para a interface comum.
Resolve qual provider instanciar em runtime.
Aberto para extensão (nova rede), fechado para mudança.
⚙️ 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
concurrencypor 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
setTimeoutem 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
Unidade de trabalho enfileirada para execução posterior.
Tempo antes do job ficar elegível para processar.
Intervalo dobra a cada falha — protege APIs.
Mesmo job executado N vezes = mesmo resultado final.
🖼️ 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):
/uploadsem 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
Armazena blobs (imagens, vídeos) acessíveis via HTTP.
URL temporária com permissão embutida na query string.
Cliente sobe direto no storage, sem proxy pelo backend.
Cache de borda que entrega a mídia rápido global.
🗄️ 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
Joins fortes entre User, Post e Target com FK.
Um Post → N PostTarget, cada um com status próprio.
Prisma versiona schema, replay garante consistência.
Tokens OAuth nunca em texto plano no banco.
🎨 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.
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.
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".
Calendário semanal/mensal
Cada slot mostra um chip colorido por rede. Bibliotecas: FullCalendar, react-big-calendar ou um grid custom com CSS Grid.
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
Next.js renderiza server, hidrata cliente — SEO + UX.
Atualiza tela antes do servidor confirmar — sensação fluida.
Editor, preview, calendário cada um isolado e reutilizável.
Drag-drop deve ter alternativa via teclado.
🔔 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
HTTP callback que a rede faz no seu endpoint.
Assinatura com chave secreta — prova autenticidade.
Webhook pode chegar duplicado — handler aguenta repetição.
SSE/WebSocket empurra status novo pro browser.
🎯 Resumo do Módulo
Próximo Módulo:
1.3 — Trade-offs: self-hosted vs SaaS, build vs buy