De CMS Moderno a Plataforma Escalável: A Trajetória Real do MKT
15+ anos desenvolvendo em PHP. Dois projetos em produção (CIM Base, Omniaflow). Agora, um desafio pessoal. Esse artigo é um registro transparente da construção do MKT – Marketing & Code: as decisões técnicas deliberadas, os padrões que já validei em produção, e o caminho até se tornar uma plataforma real.

Começamos em janeiro de 2026 — um desafio que eu mesmo me fiz, aplicando tudo o que aprendi construindo CIM Base (site + CRM integrado) e Omniaflow (ERP multi-tenant com processamento financeiro real).
Não é um case de "tudo perfeito". É uma trajetória informada por produção.
Contexto: Dois Projetos em Produção Que Ensinaram Tudo
Antes de montar o MKTCode, eu tinha dois projetos reais em produção — e foi aprendendo com eles que entendi como construir do zero certo.
CIM Base — Site + CRM Integrado
Uma plataforma que une um site institucional robusto com um CRM de leads integrado, sem atrito entre os dois. Tudo em um monolito moderno com Inertia.js v2.
Lições aprendidas:
DDD tático não é over-engineering — é proteção contra o crescimento
RBAC granular é fundamental quando a equipe passa de 2 para 5 pessoas
UI library unificada (+50 componentes) reduz tempo de feature em 40%
Site + CRM integrado no mesmo monolito melhora conversão drasticamente (não há "exporta/importa dados" manual)
Omniaflow — ERP Multi-Tenant com Processamento Financeiro
Um sistema educacional que processa cobranças reais (PIX/Boletos), conciliação automática e isolamento total entre 40+ franquias.
Lições aprendidas:
Gateway Pattern salva você quando precisa trocar de provedor externo
Multitenancy por design (não por filtros de banco de dados) é impossível de quebrar
WebSockets em tempo real (Laravel Reverb) transformam a experiência do usuário
Valores em centavos (
amount_cents) eliminam erros de ponto flutuanteEvent-driven architecture escala sem overhead operacional
O Problema Que Começou Tudo
Como desenvolvedor sênior, sempre tive um pé em dois mundos:
Mundo 1: Construir ERPs e plataformas SaaS complexas para clientes. Aí aprendi o custo de decisões arquiteturais ruins.
Mundo 2: Nunca tinha um site próprio que refletisse realmente o que eu faço. Sites "prontos" me frustravam.
Então em janeiro de 2026, decidi:
Construir o site da MC - Marketing & Code — aplicando exatamente o que já sei funcionar em produção.
Não era questão de ego. Era questão de controle e de validação prática.
Eu queria:
Performance extrema (não sei por que CMS tradicionais são tão lentos)
Arquitetura que não envelhecesse em 2 anos
Fundação para evoluir de "site institucional" para "plataforma de marketing + CRM"
Aplicar padrões (DDD tático, Gateway Pattern, RBAC) que já comprovei em produção
Então decidi: construir do zero, e certo.
Por Que o MKTCode é Diferente (E Por Que Tem Que Ser)
O MKTCode não é um terceiro projeto repetindo padrões. É um desafio pessoal de aplicar tudo o que aprendi em um projeto que é meu.
Diferente de CIM Base ou Omniaflow (que são soluções para clientes), o MKTCode é:
O site da minha agência
O lugar onde publico minha trajetória
A plataforma onde valido novas ideias
O que eu recomendo quando alguém pergunta: "Como você estruturaria um projeto do zero?"
Por isso cada decisão é deliberada. Cada padrão já foi testado em produção.
A Stack: Escolhas Deliberadas (Não Aleatórias)
Essa parte é importante porque toda escolha tem uma razão.
Backend: Laravel 12 + PHP 8.4
Por que Laravel?
Existem outras opções (Symfony, API platforms), mas Laravel oferece algo raro: opinativo sem ser engessado.
Ecossistema maduro (Filament, Fortify, Spatie packages)
Convenções que fazem sentido (não preciso inventar a roda)
Performance decente (PHP 8.4 é realmente rápido agora)
Comunidade que entende DDD e Clean Code
// app/Providers/AppServiceProvider.php
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
// Força HTTPS em produção automaticamente
if (app()->isProduction()) {
\URL::forceRootUrl(config('app.url'));
\URL::forceScheme('https');
}
// Super admin têm acesso irrestrito
Gate::before(function ($user, $ability) {
return $user->hasRole('super_admin') ? true : null;
});
// Observers disparam automações
Post::observe(PostObserver::class);
Project::observe(ProjectObserver::class);
User::observe(UserObserver::class);
}
}
Isso não parece muito, mas reflete uma decisão arquitetural: Observers como primeira classe para side effects, não como truques.
Admin: Filament v5
Inicialmente considerei Filament v4, mas v5 stable chegou:
Navegação mais rápida (menos renders desnecessários)
Ações contextuais integradas (WhatsApp, Email direto da tabela)
Componentes compostos (Grid, Section, Tabs) que não parecem gerados automaticamente
Table builders com Stack layout (dados empilhados, sem scroll horizontal)
// app/Providers/Filament/AdminPanelProvider.php
return $panel
->id('admin')
->login()
->multiFactorAuthentication([
AppAuthentication::make()->recoverable(),
EmailAuthentication::make(),
])
->passwordReset()
->emailVerification()
->path('admin')
->colors(['primary' => Color::Neutral])
->profile(EditProfile::class)
->discoverResources(in: app_path('Filament/Resources'))
->middleware([
EncryptCookies::class,
StartSession::class,
AuthenticateSession::class,
VerifyCsrfToken::class,
SubstituteBindings::class,
])
->authMiddleware([Authenticate::class]);
O importante: Filament é headless o suficiente pra coexistir com Inertia/Vue sem conflitos de rendering.
Frontend: Inertia.js v2 + Vue 3 + SSR
Esse é o diferencial real.
Inertia.js é a ponte perfeita entre Laravel e Vue:
Sem REST API boilerplate (Inertia cuida disso)
TypeScript completo (fewer bugs)
SSR habilitado (Google adora)
Deferred Props (lazy loading nativo)
// resources/js/composables/useSeo.ts
export function useSeo(props: SeoProps) {
const canonicalUrl = props.url
? props.url.startsWith('http')
? props.url
: ${SITE_URL}${props.url}
: undefined;
return {
siteName: SITE_NAME,
title: props.title,
description: props.description || SITE_DESCRIPTION,
image: props.image
? props.image.startsWith('http')
? props.image
: `${SITE_URL}${props.image}`
: `${SITE_URL}${SITE_OG_IMAGE}`,
url: canonicalUrl,
type: props.type || 'website',
keywords: props.keywords || SITE_KEYWORDS,
author: props.author || SITE_AUTHOR,
};
}
Um useSeo composable que abstrai toda a complexidade de metags, open graph, canonical URLs. Tudo tipado, reutilizável.
Styling: Tailwind CSS v4
Não há discussão aqui. Tailwind é produtividade pura.
Database: MySQL/MariaDB + Redis
MySQL/MariaDB: relacional, índices, transações. Padrão.
Redis: cache de queries frequentes, session store, fila de jobs.
Ambas em produção. Ambas essenciais para escala.
A Arquitetura Real
Aqui é onde a maioria dos artigos mente.
A documentação diz "Clean Architecture". Vou ser honesto: é o máximo possível sem virar overengineering.
Actions: Lógica Reutilizável
// app/Actions/Public/ProcessInquiry.php
declare(strict_types=1);
namespace App\Actions\Public;
use App\Models\Inquiry;
use Illuminate\Support\Facades\Log;
class ProcessInquiry
{
/**
* Process the inquiry from contact form.
*
* @param array<string, string> $data
*/
public function execute(array $data): void
{
Inquiry::create($data);
Log::info('Inquiry processed:', $data);
}
}
Isso é pequeno, intencionalmente.
Por quê? Porque essa action será reutilizada por:
Controller Web
API endpoint
Job assíncrono
Webhook de terceiro (futuramente)
Se tivesse lógica de notificação, como a do Telegram aqui, estaria acoplada. Em vez disso, deixo os Observers cuidarem.
Observers: Automações Desacopladas
// app/Observers/InquiryObserver.php
class InquiryObserver
{
public function created(Inquiry $inquiry): void
{
// Dispatch async job, não executar síncrono
SendInquiryNotificationJob::dispatch($inquiry)
->onQueue('notifications');
// Log
Log::info('New inquiry created', ['id' => $inquiry->id]);
}
}
A decisão crítica aqui: observers não fazem work pesado. Eles dispatcham jobs.
Isso foi aprendizado do Omniaflow — se Telegram API demora 500ms e temos 100 inquiries/dia, a última espera 50 segundos. Não aceitável.
Services: Integrações Limpas
// app/Services/Telegram/TelegramNotifier.php
class TelegramNotifier
{
public function send(TelegramBotTarget $target, Notification $notification): void
{
// Lógica de envio para Telegram
// Retry logic, erro handling
}
}
// app/Services/Telegram/TelegramBotTarget.php
class TelegramBotTarget
{
use Notifiable;
public function __construct(public readonly string $chatId) {}
public static function default(): self
{
return new self(config('telegram.bot.chat_id'));
}
}
Value Objects + Notifiable trait é um padrão elegante. Permite enviar notificações sem depender de models de banco de dados.
Decisões Críticas Que Fizeram Diferença
1. Segurança desde o Dia 1
// app/Providers/Filament/AdminPanelProvider.php
->multiFactorAuthentication([
AppAuthentication::make()->recoverable(),
EmailAuthentication::make(),
])
MFA não é "feature futura". É default. Desde o início.
// app/Providers/AppServiceProvider.php
Password::defaults(fn (): ?Password => app()->isProduction()
? Password::min(12)
->mixedCase()
->letters()
->numbers()
->symbols()
->uncompromised()
: null,
);
Senhas têm regras estritas em produção. Isso parece paranoia até você ver um hack.
2. Rate Limiting Em Camadas
// app/Providers/FortifyServiceProvider.php
RateLimiter::for('login', function (Request $request) {
$throttleKey = Str::transliterate(
Str::lower($request->input(Fortify::username())) . '|' . $request->ip()
);
return Limit::perMinute(5)->by($throttleKey);
});
5 tentativas por minuto por (username + IP). Brute force fisicamente impossível.
3. SSR Implementado Corretamente
Muitos projetos dizem "SSR", mas renderizam apenas metags. Aqui, renderiza HTML completo no servidor.
// resources/js/components/SeoHead.vue
<script setup lang="ts">
import { Head } from '@inertiajs/vue3';
import { useSeo } from '@/composables/useSeo';
const props = withDefaults(defineProps<SeoProps>(), {
type: 'website',
noIndex: false,
image: SITE_OG_IMAGE,
keywords: SITE_KEYWORDS,
});
const seo = useSeo(props);
</script>
<template>
<Head>
<title>{{ seo.title }}</title>
<meta name="description" :content="seo.description" />
<meta name="robots" :content="seo.noIndex ? 'noindex, nofollow' : 'index, follow'" />
<link v-if="seo.url" rel="canonical" :href="seo.url" />
<!-- Open Graph -->
<meta property="og:type" :content="seo.type === 'profile' ? 'profile' : seo.type" />
<meta property="og:title" :content="seo.title" />
<meta property="og:description" :content="seo.description" />
<meta property="og:image" :content="seo.image" />
<!-- Article only -->
<template v-if="seo.type === 'article' && seo.publishedAt">
<meta property="article:published_time" :content="seo.publishedAt" />
<meta property="article:author" :content="seo.author" />
</template>
</Head>
</template>
Composable reutilizável, tipado, sem bugs de metags esquecidas.
O Que o MKT Já Funciona Hoje
CMS com Performance Real
Posts com SEO nativo
Projetos como portfolio
Categorias e tags polimórficas
MediaLibrary para uploads
Sitemap dinâmico
Performance (Core Web Vitals):
LCP: ~1.2s (SSR)
FID: ~0.1s
CLS: 0 (sem layout shift)
Não é acidente. É resultado de decisões deliberadas.
Lead Capture Que Funciona
Formulário de contato com validação
Persistência em Inquiry model
Notificações Telegram automáticas
Logging de tudo
Isso não parece muito, mas é a base de um motor de leads.
Admin Que Não Dá Raiva
Filament com:
Dashboards dinâmicos
RBAC via Spatie Permission + Filament Shield
Ações rápidas (WhatsApp, Email)
Indicadores visuais (badges por status)
Administrar o site é satisfatório, não tedioso. Isso importa.
Os Desafios (Sendo Honesto)
Performance em Escala
Problema Real: SSR em Node.js é bom até ~500 visitas/hora. Depois disso, a fila de renderização cresce.
Solução em Roadmap: PM2 com cluster mode (multi-processo) e cache agressivo via Redis.
Queries Pesadas
MediaLibrary, Tags polimórficas, relacionamentos — tudo isso pode vira N+1 disaster se não otimizado.
Solução Atual: Índices bem planejados, eager loading com with(), e cache estratégico.
Solução Futura: Redis cache para queries frequentes de read (posts, projetos).
Filament v5 vs Inertia SSR
Ambos em produção, mas não documentei bem como sessão é compartilhada entre pipelines.
Atualmente:
Logout em
/admin→ destrói session → Inertia redireciona pra/loginIsso funciona, mas foi por acaso, não por design
Preciso documentar e testar mais cenários edge.
Decisões Futuras (O Roadmap Real)
Aqui é onde o MKTCode não é apenas um site — é uma evolução natural inspirada pelo que já comprovei em produção.
Curto Prazo
1. Cache Strategy com Redis
Aprendizado do Omniaflow: queries frequentes não escalam sem cache inteligente.
// Futuro
Route::get('/blog', function() {
$posts = Cache::remember('blog.posts.page.1', 3600, fn() =>
Post::with('category', 'tags')
->published()
->latest()
->paginate(10)
);
return Inertia::render('Blog/Index', [
'posts' => PostResource::collection($posts)
]);
});
// Invalidação smart quando post criado
class PostObserver {
public function created(Post $post): void {
Cache::tags('blog')->flush();
}
}
2. Security Headers Middleware
CSP, X-Frame-Options, HSTS — padrão já implementado em Omniaflow.
3. Structured Data (Schema.org)
JSON-LD para articles, organization, breadcrumbs. Google (e seus clientes) precisam disso.
Médio Prazo: Inquiry → Lead + CRM Leve
Esse é o ponto de inflexão que validei em CIM Base.
Quando você integra site + CRM no mesmo monolito, a conversão de leads melhora drasticamente porque não há atrito entre os sistemas.
Inquiry é uma mensagem. Lead é uma oportunidade.
// app/Models/Lead.php (novo)
class Lead extends Model
{
public $status; // 'novo', 'contatado', 'proposta', 'fechado'
public $source; // 'contato', 'ebook', 'landing-page'
public $potential_value; // Preenchido manualmente
public $assignedTo; // Responsável
}
// Pipeline de vendas
const STATUSES = ['novo', 'contatado', 'proposta', 'fechado'];
Isso abre porta para:
CRM leve integrado — como em CIM Base
Pipeline visual no Filament — drag-drop de leads entre estágios
Automações baseadas em status — notificações, webhooks, integrações
RBAC granular — cada vendedor vê apenas seus leads
4. Formulários Dinâmicos (Multi-Landing Page)
Aprendizado do Omniaflow: cada ponto de contato diferente captura dados diferentes.
// app/Models/Form.php (novo)
class Form extends Model
{
public $fields; // JSON: [{ name: 'name', type: 'text' }, ...]
public $redirectUrl; // URL pós-submit
public $successMessage; // Mensagem customizada
}
// Cada form gera leads segmentados
// /ebook-marketing?form=ebook-lead
// /consultoria-gratis?form=consultoria
// /proposta-customizada?form=proposta
Longo Prazo: Automações Event-Driven
Padrão que validei em Omniaflow: quando o sistema é event-driven, escala sem overhead operacional.
// Futuro
event(new LeadCreated($lead));
// Listeners automáticos:
// - SendWelcomeEmail
// - CreatePipelineCard
// - TriggerAutomation
// - LogActivity (Spatie Activity Log)
// - WebhookDispatch
Super Longo Prazo: SaaS Evolution
Com infraestrutura de leads + automações, viabiliza o que Omniaflow já comprovou:
Multi-tenant — agências usando MKTCode pra gerenciar leads de seus clientes
Cobrança via Gateways de pagamento (Stripe, Asaas, etc)
API pública — webhooks, integração com terceiros
Isolamento por design — não por filtros (garantia arquitetônica)
Isso não é fantasia. É o próximo passo natural, aplicando tudo o que Omniaflow já comprovou em produção com clientes reais.
Por Que Documentar Isso?
15+ anos desenvolvendo, e a lição mais importante é:
Código é passageiro. Decisões são permanentes.
Um controller ruim você refatora em um dia. Uma decisão arquitetural ruim você lida por anos.
Por isso documentei:
Estou usando Inertia SSR porque performance importa pra SEO
Estou usando Observers porque side effects precisam ser desacoplados
Estou usando Actions porque reutilização é obrigatória
Estou usando Redis porque cache é não-negociável em escala
Não é "best practices genérico". É decisão deliberada com razão, validada em dois projetos em produção.
Conclusão: Tecnologia Que Resolve
O MKTCode começou como um desafio pessoal. Evoluiu para um CMS. Vai virar uma plataforma.
Mas em todo passo:
Tecnologia em serviço do problema, não ao contrário.
Escolhemos Laravel não porque é "popular". Escolhemos porque oferece flexibilidade sem perder opinion.
Escolhemos Inertia não porque "SPA é hype". Escolhemos porque SSR é melhor para negócio.
Escolhemos Filament não porque "low-code é future". Escolhemos porque permite iterar rápido no admin sem sacrificar controle.
Escolhemos aplicar padrões (DDD, RBAC, Gateway Pattern) porque já os vimos funcionar em produção, não em blog posts.
Esse é o começo de uma trajetória.
Os próximos passos vão transformar o MKTCode de "site bonito com formulário" em "plataforma real de marketing + CRM".
E todas as decisões que fizemos até aqui foram pra garantir que quando chegar lá, o código não envelheça.
