MyImobee — Planejamento v2 (Hostinger)
Fase 1 · Planejamento · v2 — Hostinger (PHP + MySQL)

MyImobee: a vitrine digital do estoque da imobiliária.

Plataforma SaaS multi-tenant para imobiliárias e corretores regionais: portal público rápido e indexável, painel administrativo e cadastro assistido por IA com revisão humana obrigatória. Este documento cobre a arquitetura, o banco, os fluxos, o mapa de telas e as estratégias de autenticação, isolamento, IA e imagens. As telas estão no protótipo navegável.

Laravel · PHP 8.3MySQL / MariaDBBlade + Alpine.jsHostinger PremiumClaude API via servidor

1. Arquitetura proposta

Mudança da v2: saímos do Supabase por instabilidade e passamos para a hospedagem Hostinger Premium, que roda só PHP e MySQL. A v1 (Next.js + Supabase) continua em Planejamento MyImobee, para comparação.

Um único aplicativo Laravel serve o portal, o painel e o master, com separação por rota, por middleware e por permissão. As páginas são renderizadas no servidor (Blade), o que é ótimo para SEO e para celulares mais simples. O Alpine.js cuida das interações. Sem Node.js no servidor: CSS e JS são compilados no CI.

FRONTEND · BLADE
Portal público
SSR, SEO, busca, imóvel, favoritos
FRONTEND · BLADE
Painel da imobiliária
Dashboard, imóveis, cadastro IA, leads
FRONTEND · BLADE
Painel master
Imobiliárias, planos, consumo, métricas
MIDDLEWARE
ResolveTenant + autenticação + throttle
host → agency_domains → imobiliária da requisição · painel: imobiliária ativa do usuário (com checagem de vínculo)
APLICAÇÃO
Controllers + Policies
Form Requests, perfis, limites do plano
SERVIÇO
AiService
Claude via HTTP · chave no .env · ai_jobs
SERVIÇO
ImageService
GD/Intervention: WebP/AVIF, thumbs, hash
SERVIÇO
Eventos e filas
Driver database + cron a cada minuto
Auth do Laravel
Sessão em cookie seguro, 2FA para o master
MySQL / MariaDB
Fonte da verdade · agency_id em tudo
Disco da Hostinger
/storage/media/{agency}/{imóvel}/…
Cron do hPanel
schedule:run → filas, sitemap, limpeza
Por que Laravel: autenticação, migrations, filas no banco, agendador, validação, rate limit e policies prontos. Menos código próprio e deploy simples em hospedagem compartilhada.
Limites aceitos: sem processos persistentes (IA e fotos processadas na requisição), domínio próprio adicionado manualmente no hPanel, disco e inodes finitos.
Saída futura: o mesmo código sobe no Cloud ou em um VPS. As fotos migram para o Cloudflare R2 com uma troca de disco no .env.

2. Estrutura inicial do banco

Toda tabela de negócio carrega agency_id. IDs BIGINT internos, ULID público na imobiliária, created_at/updated_at em todas e exclusão lógica onde a URL precisa sobreviver. MySQL 8 / MariaDB com InnoDB, utf8mb4 e FULLTEXT na busca.

{{ t.name }}{{ t.tag }}
{{ t.desc }}
{{ c }}
ENUMs do MySQL: status (rascunho, publicado, reservado, vendido, alugado, inativo) · purpose (venda, aluguel) · role (admin, imobiliaria, corretor) · badge (destaque, novidade, oportunidade, exclusivo). Código do imóvel: contador agencies.next_code lido com SELECT … FOR UPDATE dentro de uma transação (IMB-1024), índice único (agency_id, code) e busca por código no portal.

3. Fluxos de usuário

{{ f.title }}
{{ s }}
{{ f.note }}

4. Mapa de telas e URLs

{{ g.title }}
{{ i.url }}{{ i.label }}

5. Componentes do design system

Fundação: tema escuro de baixa saturação, Inter peso 500 nos títulos, raio de 8px, acento usado como linha e brilho — não como preenchimento. Botão principal com contorno, foco visível no acento, fotos grandes.

{{ c.name }}
{{ c.desc }}
Estados em todos: loading (skeleton ou spinner no próprio botão), empty (mensagem + próxima ação), error (mensagem inline + tentar novamente), success (toast e confirmação), disabled (45% de opacidade, cursor bloqueado).

6. Autenticação e perfis

  • Autenticação do Laravel (Breeze/Fortify): e-mail e senha com hash argon2id, verificação de e-mail, recuperação de senha e login com Google (Socialite). Sessão em cookie httpOnly, Secure e SameSite=Lax, guardada na tabela sessions.
  • A imobiliária ativa fica em users.current_agency_id; o middleware confirma o vínculo em memberships a cada requisição do painel. Quem pertence a várias imobiliárias alterna entre elas.
  • Perfis: admin (is_platform_admin, MyImobee), imobiliaria (tudo da própria imobiliária), corretor (os próprios imóveis e leads). Regras em Policies, prontas para novos níveis.
  • Master com 2FA (TOTP) obrigatório. A opção “Acessar painel” de uma imobiliária usa Tenant::withoutScope() em modo suporte, registrado em audit_logs.
  • Proteções: CSRF em todos os formulários, throttle de login (5 tentativas/min), bloqueio progressivo e aviso por e-mail em novo acesso.
  • Favoritos começam em localStorage; com conta de visitante, passam para a tabela favorites (junção no primeiro login).

7. Estratégia multi-tenant

  • Banco compartilhado, isolamento pela aplicação: o MySQL não tem Row Level Security. Toda tabela de negócio tem agency_id NOT NULL indexado, e todo model usa o trait BelongsToAgency: filtro global em toda consulta e agency_id preenchido em toda criação, sem poder ser alterado depois.
  • Resolução pelo domínio: o middleware ResolveTenant lê o host (caririprime.myimobee.com.br ou www.caririprime.com.br), consulta agency_domains (com cache de 5 min) e define o tenant. myimobee.com.br é o portal agregado, que só lê anúncios públicos.
  • Sem tenant, sem dados: uma consulta fora de contexto não devolve nada; uma criação sem tenant lança exceção. Contornar exige Tenant::withoutScope(), permitido só em jobs internos e no master.
  • Arquivos: media/{agency_uuid}/{property_id}/{ulid}-{largura}.webp. Uploads e exclusões passam sempre pela Policy do imóvel.
  • Testes de isolamento no CI bloqueiam o deploy: A não lista, não abre, não edita e não exclui nada de B; um agency_id forjado no request é ignorado; o domínio de A nunca mostra imóvel de B.
  • Limites do plano aplicados no servidor (imóveis, usuários, armazenamento, IA, destaques, domínio próprio).
trait BelongsToAgency {
  public static function bootBelongsToAgency(): void {
    static::addGlobalScope('agency', function (Builder $q) {
      if (Tenant::bypassed()) return;
      $id = Tenant::id();
      if ($id === null) { $q->publicVisible(); return; }   // portal agregado
      $q->where($q->getModel()->getTable().'.agency_id', $id);
    });
    static::creating(fn ($m) => $m->agency_id = Tenant::id()
      ?? throw new LogicException('Criação sem tenant.'));
    static::updating(fn ($m) => $m->isDirty('agency_id')
      && throw new LogicException('agency_id não pode mudar.'));
  }
}

8. Fluxo do cadastro com IA

{{ s.label }}
  • Entrada: texto livre, fotos (até 40) e, na próxima fase, PDF/Word/planilha (extração de texto no servidor antes da IA).
  • Extração estruturada: a IA devolve JSON validado por schema; cada campo vem com source (texto, foto ou nulo). Campo ausente = null → “Não informado”.
  • Inferência visual nunca vira fato: aparece como “Possível característica — confirmar antes de publicar” e só entra no anúncio se o usuário confirmar.
  • Descrição (Objetivo, Comercial, Premium, SEO) e SEO são gerados só a partir dos campos confirmados.
  • Revisão humana obrigatória: o resultado é salvo como rascunho; a publicação exige clique em “Publicar imóvel” e confirmação explícita.
  • Execução: AiService chama a API da Claude pelo PHP, no servidor (a chave fica no .env). São três requisições em sequência (extrair → descrever → SEO), e a tela marca cada etapa. Tempo limite de 45 s e uma nova tentativa.
  • Rastreio e custo: cada chamada grava ai_jobs (tenant, tipo, tokens, custo, duração, erro). Limite mensal por plano e rate limit por usuário.
  • Falha: sem resposta da IA, o sistema faz leitura básica local e marca tudo para revisão — o corretor nunca fica bloqueado.

9. Fluxo de upload de imagens

{{ s }}
  • Pré-processamento no navegador (redimensiona para 1600px e converte para WebP) reduz o upload em 80–95% em redes móveis — já funcionando no protótipo.
  • Upload de uma foto por requisição (POST multipart, com progresso na tela). O servidor valida o tipo real pelos magic bytes (finfo), o tamanho (15 MB), a quantidade por imóvel (40) e a cota de armazenamento do plano.
  • ImageService (Intervention Image + GD) gera WebP em 1600/960/480/320 e AVIF quando o PHP da hospedagem suporta. Também reescreve a imagem sem EXIF/GPS, cria um placeholder borrado inline, calcula um hash 8×8 para achar duplicadas e dá uma nota de qualidade pela resolução.
  • Uma cópia de 512px, privada, é enviada à IA para identificar ambientes. property_media guarda ordem, foto principal, legenda, alt e ambiente. As fotos são entregues pelo CDN da Hostinger com srcset, lazy loading e cache longo.

10. Integrações

IntegraçãoAgoraDepois
{{ i.n }}{{ i.now }}{{ i.later }}

11. SEO e performance

  • URLs amigáveis estáveis: /imovel/casa-3-quartos-salesianos-juazeiro-do-norte/1024. O número final é a chave; se o slug mudar, 301 para o novo.
  • Páginas indexáveis de cidade, bairro e tipo: /imoveis/juazeiro-do-norte-ce, /imoveis/juazeiro-do-norte-ce/casas, com texto local e links internos.
  • Gerados automaticamente: title, meta description, Open Graph (imagem principal 1200×630), canonical, breadcrumb (BreadcrumbList) e JSON-LD RealEstateListing + Offer.
  • Vendido/alugado mantém a URL no ar com o aviso e imóveis semelhantes — preserva o SEO e aproveita o tráfego.
  • sitemap.xml e robots.txt por imobiliária (comando agendado de hora em hora). Cache de páginas no banco (10 min), limpo ao publicar ou editar, e Cache-Control público nas páginas do portal.
  • Meta: LCP abaixo de 2,5 s em 4G, CLS abaixo de 0,1. Imagem hero com prioridade, restante lazy, fontes com display=swap, JS mínimo no portal.

12. Segurança

{{ s }}

13. Fases de implementação

FASE {{ p.n }}
{{ p.name }}
{{ p.items }}
{{ p.status }}