SaaS de tatuagem · simulação de tattoo com IA
InkVision

Problema
Cliente de tatuagem decide no escuro: só vê como a arte vai ficar na pele durante a sessão, já com a agulha na mão.
Solução
Simulação da tatuagem na própria foto via IA, chat com o tatuador pra aprovar o desenho, agendamento e um marketplace multi-tenant conectando clientes a estúdios e artistas.
SaaS em desenvolvimento ativo.
Como foi construído
Monorepo pnpm + Turborepo com Clean Architecture explícita: apps/web (Next.js 15, App Router, React 19) cobre SSR/ISR das páginas públicas e Server Actions para mutações, apps/realtime isola o Socket.IO num processo dedicado (WebSocket de longa duração não convive bem com o modelo serverless do Next) e apps/worker roda os jobs assíncronos via BullMQ. A regra de negócio nunca mora nas rotas: fica em packages/core (use cases puros, sem I/O, DTOs validados com zod) que só conhece interfaces (ports) implementadas por packages/infra (repositórios Prisma). Essa decisão permitiria trocar a borda HTTP por outro framework sem tocar no domínio. Autenticação usa Better Auth em vez de Auth.js porque o plugin organization resolve membership multi-tenant nativamente (role ADMIN de plataforma e OWNER/MANAGER/ARTIST por estúdio; cliente é usuário sem membership).
A simulação de tatuagem é a feature central e foi desenhada como um pipeline, não uma chamada única de IA: packages/ai expõe portas (TattooSimulationProvider, ImageGenerationProvider, SkinSegmentationProvider) e um registry que troca de provider (Fal.ai, Replicate, OpenAI, Gemini, Stable Diffusion) só com uma env var, sem nenhum caso de uso importando um provider concreto. O fluxo é segmentar a pele/parte do corpo, estimar perspectiva e curvatura, aplicar o desenho com warp na escala e posição escolhidas pelo cliente no editor (arrastar, redimensionar, rotacionar) e fechar com um passo de img2img de baixa intensidade que harmoniza sombra, textura e iluminação preservando o traço original. Como a geração leva de 10 a 60 segundos, a API só enfileira no BullMQ; o worker chama o provider e o resultado chega ao cliente por WebSocket (evento simulation:done), sem nenhum request HTTP pendurado esperando a IA responder. Cada chamada grava um AiUsageLog (provider, operação, custo) que alimenta o dashboard admin e os limites de crédito por plano.
O isolamento multi-tenant é banco único com studioId, protegido em três camadas em vez de um filtro solto em cada query. Uma extensão do Prisma Client exige studioId em toda consulta de modelo tenant-scoped: lança erro em dev e bloqueia em prod. O RLS do Postgres (SET app.current_studio_id) segura mesmo se a camada de aplicação falhar. E uma suíte de testes tenta acesso cross-tenant em cada rota. Essa redundância pegou um bug real. O getActor() lia a tabela StudioMember, que é protegida por RLS, sem abrir o contexto de tenant nem de admin. A política nunca casava, a consulta voltava vazia mesmo com memberships reais, e o dono do estúdio acabava vendo um painel idêntico ao de um cliente comum. Em dev local o bug nunca apareceu, porque o role do Postgres usado ali ignora RLS por padrão. Só apareceu numa auditoria visual, comparando telas de verdade. O fix foi withAdmin(), seguro porque o filtro final continua sendo o próprio userId. A lição é direta: sem testar contra um role de produção, sem bypass de RLS, esse tipo de bug fica invisível por tempo indeterminado.
O primeiro deploy real (Vercel + Neon, ambiente de teste antes da VPS de produção) revelou três bugs que nenhum ambiente anterior exercitava. A migration da constraint anti-overbooking usava tstzrange() num índice. Essa função depende do fuso da sessão (é STABLE, não IMMUTABLE) e quebra num migrate deploy de verdade. A correção foi trocar por tsrange(), que bate com o tipo real da coluna. O ambiente local usa prisma db push, que ignora SQL bruto, e o CI não rodava migrations, então o bug ficou invisível até o primeiro migrate deploy real. Teria travado o deploy na VPS também. O bundle serverless da Vercel também 'perdia' o query engine do Prisma num monorepo pnpm, resolvido com o plugin oficial de workaround, ativo só quando VERCEL=1 pra não afetar o Docker da VPS. E o rate limiting do Better Auth usava um limitador em memória, que simplesmente não funciona com múltiplas instâncias serverless. Trocado por armazenamento em banco: uma tabela RateLimit, deliberadamente fora do RLS, no mesmo padrão de User e Session, que também não são multi-tenant.
Simulação de IA como pipeline
Segmentação de pele, estimativa de perspectiva e curvatura, warp do desenho e img2img de baixa intensidade pra harmonizar sombra e textura. Não é uma chamada única de IA.
Provider de IA plugável
packages/ai troca entre Fal.ai, Replicate, OpenAI, Gemini ou Stable Diffusion só com uma env var. Nenhum caso de uso importa um provider concreto.
Multi-tenant em 3 camadas
Extensão do Prisma que exige studioId, Postgres RLS como defesa em profundidade e uma suíte automatizada de testes de isolamento cross-tenant.
Bugs só visíveis em produção real
Migration com tstzrange() só quebrava num migrate deploy de verdade (dev usa db push); achada no primeiro deploy Vercel+Neon, antes de chegar na VPS.
Stack
- Next.js
- TypeScript
- Prisma
- PostgreSQL
- Turborepo
- Stripe