Pular para o conteúdo
🎯 Módulo 2 - Aula 2

Inngest Background Jobs

Implemente jobs em background robustos com event-driven functions, scheduling e monitoramento

50 min
Nível:Intermediário

Por que isso é importante

Jobs em background são fundamentais para SaaS escaláveis.Inngest oferece event-driven functions com retries automáticos, scheduling avançado e monitoramento em tempo real. Substitui Redis/Bull/Sidekiq com uma solução serverless robusta.

📚 Aula Educacional

Esta aula apresenta conceitos e implementações do Inngest como exemplos educacionais. Você aprenderá arquitetura, padrões e best practices sem modificar o projeto atual. Use este conhecimento para implementar jobs em seus próprios projetos SaaS.

🎯 O que São Background Jobs?

Problemas que Resolvem:

  • • Tarefas longas bloqueiam UI
  • • Emails demoram para enviar
  • • Processamento de dados pesados
  • • Integrações externas lentas
  • • Timeouts em requisições

Casos de Uso SaaS:

  • • Envio de emails transacionais
  • • Processamento de webhooks
  • • Geração de relatórios
  • • Backup de dados
  • • Notificações push

🤔 Passo 1: Por que Inngest?

🎯 Comparação com Alternativas

Inngest elimina a complexidade de Redis/Bull, oferecendo jobs serverless com interface visual, retries inteligentes e observabilidade integrada.

❌ Soluções Tradicionais

Redis + Bull/BullMQ:

  • • Infraestrutura complexa
  • • Gerenciamento de filas manual
  • • Monitoramento limitado
  • • Scaling manual
  • • Debugging difícil

Cron Jobs:

  • • Sem retries automáticos
  • • Sem observabilidade
  • • Scheduling limitado
  • • Sem event-driven
✅ Inngest Advantages

Serverless + Event-Driven:

  • • Zero infraestrutura
  • • Auto-scaling nativo
  • • Event-driven architecture
  • • Type-safe com TypeScript
  • • Dashboard visual completo

Reliability:

  • • Retries exponenciais automáticos
  • • Dead letter queues
  • • Monitoring em tempo real
  • • Debugging visual

📦 Passo 2: Setup Inngest

🔧 Configuração Inicial

Inngest funciona com qualquer framework. Para Next.js, criamos functions que são servidas via API routes e registradas no Inngest Cloud.

terminal
# Instalar Inngest SDK
npm install inngest

# Instalar dependências opcionais para development
npm install --save-dev @inngest/cli

⚙️ Passo 3: Configuração Base

1. Cliente Inngest

src/inngest/client.ts
import { Inngest } from 'inngest';

// 🎯 Cliente Inngest - ponto central para todos os events
export const inngest = new Inngest({ 
  id: 'my-saas-app',
  name: 'My SaaS Application',
  
  // 🔒 Event key para segurança (production)
  eventKey: process.env.INNGEST_EVENT_KEY,
  
  // 🎨 Schema de events para type-safety
  schemas: {
    // Definiremos events específicos aqui
  }
});

2. Variáveis de Ambiente

.env.local
# Inngest Configuration
INNGEST_EVENT_KEY=your_event_key_here
INNGEST_SIGNING_KEY=your_signing_key_here

# Para development local
INNGEST_DEV=true

3. API Route Handler

src/app/api/inngest/route.ts
import { serve } from 'inngest/next';
import { inngest } from '@/inngest/client';

// 🎯 Import todas as functions aqui
import { 
  sendWelcomeEmail,
  processStripeWebhook,
  generateMonthlyReport 
} from '@/inngest/functions';

// 🚀 Serve Inngest functions via Next.js API route
export const { GET, POST, PUT } = serve({
  client: inngest,
  functions: [
    sendWelcomeEmail,
    processStripeWebhook,
    generateMonthlyReport,
    // Adicione novas functions aqui
  ],
  
  // 🔒 Configurações de segurança
  signingKey: process.env.INNGEST_SIGNING_KEY,
  
  // 🛠️ Development mode
  isDev: process.env.NODE_ENV === 'development',
});

🚀 Passo 4: Primeira Background Function

📧 Exemplo: Welcome Email

Function que envia email de boas-vindas quando usuário se registra. Demonstra event-driven architecture, type-safety e error handling.

src/inngest/functions/send-welcome-email.ts
import { inngest } from '../client';
import { sendEmail } from '@/lib/email';

// 📧 Function para enviar email de boas-vindas
export const sendWelcomeEmail = inngest.createFunction(
  {
    id: 'send-welcome-email',
    name: 'Send Welcome Email',
    
    // 🎯 Configurações de retry
    retries: {
      attempts: 3,
      backoff: 'exponential', // 1s, 2s, 4s, 8s...
    },
    
    // ⏱️ Timeout para a function
    timeout: '30s',
  },
  
  // 🎧 Event trigger - escuta event específico
  { event: 'user/signup.completed' },
  
  // 🔧 Handler da function
  async ({ event, step, logger }) => {
    const { user, metadata } = event.data;
    
    // 📝 Log estruturado
    logger.info('Processing welcome email', { 
      userId: user.id, 
      email: user.email 
    });
    
    // 📧 Step 1: Enviar email de boas-vindas
    const emailResult = await step.run('send-welcome-email', async () => {
      return await sendEmail({
        to: user.email,
        subject: 'Welcome to Our SaaS! 🎉',
        template: 'welcome',
        data: {
          userName: user.firstName,
          loginUrl: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard`,
          supportEmail: 'support@mysaas.com'
        }
      });
    });
    
    // 📊 Step 2: Registrar analytics (opcional)
    await step.run('track-welcome-email-sent', async () => {
      // Analytics tracking
      return await fetch('/api/analytics/track', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          event: 'welcome_email_sent',
          userId: user.id,
          properties: {
            emailId: emailResult.id,
            timestamp: new Date().toISOString()
          }
        })
      });
    });
    
    // ✅ Retorno com dados do resultado
    return {
      success: true,
      emailId: emailResult.id,
      userId: user.id,
      sentAt: new Date().toISOString()
    };
  }
);

🎯 Passo 5: Disparando Events

📡 Como Enviar Events

Events podem ser enviados de qualquer lugar: API routes, Server Actions, webhooks, ou até outras functions. Sempre use type-safe events.

1. From API Route (User Signup)

src/app/api/auth/signup/route.ts
import { inngest } from '@/inngest/client';
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
  try {
    const { email, firstName, lastName } = await request.json();
    
    // 1️⃣ Criar usuário no banco
    const user = await prisma.user.create({
      data: { email, firstName, lastName }
    });
    
    // 2️⃣ Disparar event para background jobs
    await inngest.send({
      name: 'user/signup.completed',
      data: {
        user: {
          id: user.id,
          email: user.email,
          firstName: user.firstName,
          lastName: user.lastName
        },
        metadata: {
          source: 'web_signup',
          timestamp: new Date().toISOString(),
          userAgent: request.headers.get('user-agent')
        }
      }
    });
    
    return NextResponse.json({ 
      success: true, 
      userId: user.id 
    });
    
  } catch (error) {
    console.error('Signup error:', error);
    return NextResponse.json(
      { error: 'Signup failed' }, 
      { status: 500 }
    );
  }
}

2. From Server Action

src/app/actions/user-actions.ts
'use server';

import { inngest } from '@/inngest/client';
import { auth } from '@clerk/nextjs/server';

export async function updateUserProfile(formData: FormData) {
  const { userId } = await auth();
  
  if (!userId) {
    throw new Error('Unauthorized');
  }
  
  // Update user profile...
  const updatedUser = await updateUser(userId, formData);
  
  // 🎯 Trigger background job para sync com CRM
  await inngest.send({
    name: 'user/profile.updated',
    data: {
      userId,
      changes: updatedUser,
      updatedAt: new Date().toISOString()
    }
  });
  
  return { success: true };
}

3. From Webhook Handler

src/app/api/webhooks/stripe/route.ts
import { inngest } from '@/inngest/client';
import { headers } from 'next/headers';
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function POST(request: Request) {
  const body = await request.text();
  const signature = headers().get('stripe-signature')!;
  
  let event: Stripe.Event;
  
  try {
    event = stripe.webhooks.constructEvent(
      body, 
      signature, 
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch (err) {
    return new Response(`Webhook error: ${err}`, { status: 400 });
  }
  
  // 🎯 Enviar para Inngest processar de forma robusta
  await inngest.send({
    name: 'stripe/webhook.received',
    data: {
      type: event.type,
      stripeEventId: event.id,
      payload: event.data,
      receivedAt: new Date().toISOString()
    }
  });
  
  return new Response('OK');
}

🔥 Passo 6: Functions Avançadas

⏰ Scheduled Functions

Functions que executam em horários específicos, como relatórios mensais ou limpeza de dados. Cron jobs serverless com observabilidade.

src/inngest/functions/monthly-report.ts
import { inngest } from '../client';

export const generateMonthlyReport = inngest.createFunction(
  {
    id: 'generate-monthly-report',
    name: 'Generate Monthly Report',
    
    // ⏰ Executa no primeiro dia de cada mês às 9h
    cron: '0 9 1 * *',
    
    // 🚨 Function crítica - retry mais agressivo
    retries: {
      attempts: 5,
      backoff: 'exponential'
    },
    
    // ⏱️ Relatórios podem demorar
    timeout: '10m',
  },
  
  // 🎧 Trigger por schedule (cron)
  { cron: '0 9 1 * *' },
  
  async ({ step, logger }) => {
    const now = new Date();
    const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1);
    
    logger.info('Starting monthly report generation', { 
      month: lastMonth.toISOString() 
    });
    
    // 📊 Step 1: Coletar dados de usuários
    const userStats = await step.run('collect-user-stats', async () => {
      return await prisma.user.groupBy({
        by: ['createdAt'],
        where: {
          createdAt: {
            gte: lastMonth,
            lt: now
          }
        },
        _count: true
      });
    });
    
    // 💰 Step 2: Coletar dados financeiros
    const revenueStats = await step.run('collect-revenue-stats', async () => {
      return await prisma.subscription.findMany({
        where: {
          createdAt: {
            gte: lastMonth,
            lt: now
          }
        },
        include: {
          plan: true
        }
      });
    });
    
    // 📈 Step 3: Gerar relatório
    const report = await step.run('generate-report', async () => {
      return {
        period: {
          start: lastMonth.toISOString(),
          end: now.toISOString()
        },
        metrics: {
          newUsers: userStats.length,
          revenue: revenueStats.reduce((sum, sub) => sum + sub.plan.price, 0),
          churn: calculateChurn(revenueStats)
        }
      };
    });
    
    // 📧 Step 4: Enviar para stakeholders
    await step.run('send-report-email', async () => {
      return await sendEmail({
        to: ['ceo@company.com', 'cfo@company.com'],
        subject: `Monthly Report - ${lastMonth.toLocaleDateString()}`,
        template: 'monthly-report',
        data: { report }
      });
    });
    
    return { success: true, reportId: report.id };
  }
);

🔄 Multi-Step Workflows

Workflows complexos com múltiplos steps, delays e condicionais. Cada step é automaticamente retried e observável.

src/inngest/functions/user-onboarding.ts
import { inngest } from '../client';

export const userOnboardingFlow = inngest.createFunction(
  {
    id: 'user-onboarding-flow',
    name: 'User Onboarding Flow',
  },
  
  { event: 'user/signup.completed' },
  
  async ({ event, step, logger }) => {
    const { user } = event.data;
    
    // 📧 Step 1: Welcome email imediato
    await step.run('send-welcome-email', async () => {
      return await sendEmail({
        to: user.email,
        template: 'welcome',
        data: { user }
      });
    });
    
    // ⏰ Step 2: Aguardar 1 dia
    await step.sleep('wait-one-day', '1d');
    
    // 🔍 Step 3: Verificar se completou onboarding
    const onboardingStatus = await step.run('check-onboarding', async () => {
      return await prisma.user.findUnique({
        where: { id: user.id },
        select: { onboardingCompleted: true }
      });
    });
    
    // 🎯 Step 4: Conditional - só envia se não completou
    if (!onboardingStatus?.onboardingCompleted) {
      await step.run('send-onboarding-reminder', async () => {
        return await sendEmail({
          to: user.email,
          template: 'onboarding-reminder',
          data: { user }
        });
      });
      
      // ⏰ Step 5: Aguardar mais 3 dias
      await step.sleep('wait-three-days', '3d');
      
      // 🔍 Step 6: Check novamente
      const finalCheck = await step.run('final-onboarding-check', async () => {
        return await prisma.user.findUnique({
          where: { id: user.id },
          select: { onboardingCompleted: true }
        });
      });
      
      // 📞 Step 7: Se ainda não completou, trigger sales call
      if (!finalCheck?.onboardingCompleted) {
        await step.run('schedule-sales-call', async () => {
          return await inngest.send({
            name: 'sales/call.requested',
            data: {
              userId: user.id,
              reason: 'onboarding_incomplete',
              priority: 'medium'
            }
          });
        });
      }
    }
    
    return { success: true, userId: user.id };
  }
);

📊 Passo 7: Monitoring e Debugging

🎯 Inngest Dashboard

Dashboard visual completo para monitorar functions, debug failures, ver metrics em tempo real e gerenciar retries.

📈 Metrics Disponíveis
  • Executions: Total, success, failed
  • Duration: Avg, p95, p99
  • Throughput: Events/min, functions/min
  • Retries: Count, success rate
  • Queued: Pending jobs
  • Errors: Rate, types, frequency
🐛 Debugging Features
  • Visual Timeline: Cada step visualizado
  • Logs Estruturados: Por function/step
  • Replay Functions: Re-run com mesmo event
  • Pause/Resume: Pausar execução
  • Manual Retries: Force retry specific function
  • Event History: Todos events enviados

Logging Best Practices

src/inngest/functions/example-with-logging.ts
export const exampleFunction = inngest.createFunction(
  { id: 'example-function' },
  { event: 'example/event' },
  
  async ({ event, step, logger }) => {
    // 📝 Log estruturado no início
    logger.info('Function started', {
      eventId: event.id,
      userId: event.data.userId,
      timestamp: new Date().toISOString()
    });
    
    // 🔧 Log em cada step importante
    const result = await step.run('process-data', async () => {
      logger.debug('Processing data', { 
        dataSize: event.data.items.length 
      });
      
      try {
        const processed = await processData(event.data);
        
        // ✅ Log de sucesso
        logger.info('Data processed successfully', {
          itemsProcessed: processed.length,
          duration: Date.now() - startTime
        });
        
        return processed;
      } catch (error) {
        // ❌ Log de erro com contexto
        logger.error('Data processing failed', {
          error: error.message,
          stack: error.stack,
          input: event.data
        });
        
        throw error; // Re-throw para trigger retry
      }
    });
    
    // 🎯 Log final com métricas
    logger.info('Function completed', {
      success: true,
      resultCount: result.length,
      totalDuration: Date.now() - functionStartTime
    });
    
    return result;
  }
);

🔗 Passo 8: Integração com tRPC

🎯 tRPC + Inngest

Combine tRPC procedures com Inngest functions para APIs que respondem rápido mas processam de forma robusta em background.

1. tRPC Procedure que Dispara Job

src/modules/reports/server/procedures.ts
import { createTRPCRouter, protectedProcedure } from '@/trpc/init';
import { inngest } from '@/inngest/client';
import { z } from 'zod';

export const reportsRouter = createTRPCRouter({
  // 📊 Generate report - resposta imediata, processing em background
  generateReport: protectedProcedure
    .input(z.object({
      type: z.enum(['monthly', 'quarterly', 'annual']),
      filters: z.object({
        startDate: z.string(),
        endDate: z.string(),
        includeChurn: z.boolean().default(true)
      }).optional()
    }))
    .mutation(async ({ ctx, input }) => {
      const reportId = generateId();
      
      // 1️⃣ Criar record de report (status: pending)
      const report = await prisma.report.create({
        data: {
          id: reportId,
          type: input.type,
          status: 'pending',
          userId: ctx.auth.userId,
          filters: input.filters,
          createdAt: new Date()
        }
      });
      
      // 2️⃣ Disparar background job
      await inngest.send({
        name: 'report/generation.requested',
        data: {
          reportId,
          userId: ctx.auth.userId,
          type: input.type,
          filters: input.filters
        }
      });
      
      // 3️⃣ Resposta imediata ao cliente
      return {
        reportId,
        status: 'pending',
        message: 'Report generation started. You will be notified when ready.'
      };
    }),
    
  // 📋 Get report status
  getReportStatus: protectedProcedure
    .input(z.object({ reportId: z.string() }))
    .query(async ({ ctx, input }) => {
      const report = await prisma.report.findFirst({
        where: {
          id: input.reportId,
          userId: ctx.auth.userId
        }
      });
      
      if (!report) {
        throw new TRPCError({
          code: 'NOT_FOUND',
          message: 'Report not found'
        });
      }
      
      return {
        id: report.id,
        status: report.status,
        progress: report.progress,
        downloadUrl: report.downloadUrl,
        createdAt: report.createdAt,
        completedAt: report.completedAt
      };
    })
});

2. Inngest Function que Processa

src/inngest/functions/generate-report.ts
import { inngest } from '../client';

export const generateReport = inngest.createFunction(
  {
    id: 'generate-report',
    name: 'Generate Report',
    retries: { attempts: 3 },
    timeout: '15m' // Reports podem demorar
  },
  
  { event: 'report/generation.requested' },
  
  async ({ event, step, logger }) => {
    const { reportId, userId, type, filters } = event.data;
    
    // 📊 Step 1: Update status to processing
    await step.run('update-status-processing', async () => {
      return await prisma.report.update({
        where: { id: reportId },
        data: { 
          status: 'processing',
          startedAt: new Date()
        }
      });
    });
    
    // 📈 Step 2: Collect data (pode ser lento)
    const reportData = await step.run('collect-report-data', async () => {
      logger.info('Collecting data for report', { reportId, type });
      
      // Simulate complex data processing
      const data = await collectReportData(type, filters);
      
      // Update progress
      await prisma.report.update({
        where: { id: reportId },
        data: { progress: 50 }
      });
      
      return data;
    });
    
    // 📄 Step 3: Generate PDF/Excel (pode ser lento)
    const fileUrl = await step.run('generate-report-file', async () => {
      logger.info('Generating report file', { reportId });
      
      const file = await generateReportFile(reportData, type);
      
      // Update progress
      await prisma.report.update({
        where: { id: reportId },
        data: { progress: 80 }
      });
      
      return file.url;
    });
    
    // ✅ Step 4: Update status to completed
    await step.run('update-status-completed', async () => {
      return await prisma.report.update({
        where: { id: reportId },
        data: {
          status: 'completed',
          downloadUrl: fileUrl,
          completedAt: new Date(),
          progress: 100
        }
      });
    });
    
    // 📧 Step 5: Notify user via email
    await step.run('notify-user', async () => {
      const user = await prisma.user.findUnique({
        where: { id: userId }
      });
      
      return await sendEmail({
        to: user.email,
        subject: 'Your report is ready! 📊',
        template: 'report-ready',
        data: {
          reportType: type,
          downloadUrl: fileUrl,
          userName: user.firstName
        }
      });
    });
    
    return {
      success: true,
      reportId,
      downloadUrl: fileUrl
    };
  }
);

3. Frontend com Polling

components/ReportGenerator.tsx
'use client';

import { useTRPC } from '@/trpc/client';
import { useMutation, useQuery } from '@tanstack/react-query';
import { useState } from 'react';

export function ReportGenerator() {
  const [reportId, setReportId] = useState<string | null>(null);
  
  const trpc = useTRPC();
  
  // 🚀 Mutation para gerar report
  const generateMutation = useMutation({
    ...trpc.reports.generateReport.mutationOptions(),
    onSuccess: (data) => {
      setReportId(data.reportId);
    }
  });
  
  // 🔄 Polling para status do report
  const { data: reportStatus } = useQuery({
    ...trpc.reports.getReportStatus.queryOptions({
      reportId: reportId!
    }),
    enabled: !!reportId,
    refetchInterval: (data) => {
      // Stop polling quando completo
      return data?.status === 'completed' ? false : 2000;
    }
  });
  
  const handleGenerate = () => {
    generateMutation.mutate({
      type: 'monthly',
      filters: {
        startDate: '2025-01-01',
        endDate: '2025-01-31'
      }
    });
  };
  
  return (
    <div className="space-y-4">
      <button 
        onClick={handleGenerate}
        disabled={generateMutation.isPending}
        className="btn btn-primary"
      >
        {generateMutation.isPending ? 'Starting...' : 'Generate Report'}
      </button>
      
      {reportStatus && (
        <div className="bg-bg-2 p-4 rounded">
          <p>Status: {reportStatus.status}</p>
          {reportStatus.progress && (
            <div className="w-full bg-bg-3 rounded-full h-2">
              <div 
                className="bg-blue-600 h-2 rounded-full" 
                style={{ width: `${reportStatus.progress}%` }}
              />
            </div>
          )}
          
          {reportStatus.status === 'completed' && (
            <a 
              href={reportStatus.downloadUrl}
              className="btn btn-success mt-2"
              download
            >
              Download Report
            </a>
          )}
        </div>
      )}
    </div>
  );
}

✅ Inngest Mastery Completo

🎯 O que Você Dominou:

  • ✅ Event-driven architecture com Inngest
  • ✅ Background jobs robustos e confiáveis
  • ✅ Scheduled functions (cron serverless)
  • ✅ Multi-step workflows complexos
  • ✅ Retry automático e error handling
  • ✅ Monitoring e debugging visual
  • ✅ Integração seamless com tRPC
  • ✅ Type-safety completo end-to-end

🚀 Próximos Passos:

  • • Implementar em projeto real
  • • Configurar alertas e monitoring
  • • Adicionar mais event types
  • • Otimizar performance das functions
  • • Integrar com mais services externos
  • • Setup CI/CD para functions