ANTESDE LA AGUJA.

- ESTUDIO
- MILWEB
- TIPO
- PRODUCTO DIGITAL
- ESTADO
- EN DESARROLLO
02 / LA IDEA
SaaS de tatuajes · simulación de tattoo con IA
El cliente de tatuajes decide a ciegas: solo ve cómo va a quedar el arte en la piel durante la sesión, ya con la aguja en la mano.
01 / 01
Segmentación de piel, estimación de perspectiva y curvatura, warp del diseño e img2img de baja intensidad para armonizar sombra y textura. No es una llamada única de IA.

02 / 02
packages/ai cambia entre Fal.ai, Replicate, OpenAI, Gemini o Stable Diffusion con solo una env var. Ningún caso de uso importa un provider concreto.

03 / 03
Extensión de Prisma que exige studioId, Postgres RLS como defensa en profundidad y una suite automatizada de tests de aislamiento cross-tenant.

04 / 04
Una migration con tstzrange() solo se rompía en un migrate deploy de verdad (dev usa db push); se encontró en el primer deploy Vercel+Neon, antes de llegar al VPS.

01
02
03
04
04 / BAJO EL CAPÓ
01Monorepo pnpm + Turborepo con Clean Architecture explícita: apps/web (Next.js 15, App Router, React 19) cubre SSR/ISR de las páginas pública…+
Monorepo pnpm + Turborepo con Clean Architecture explícita: apps/web (Next.js 15, App Router, React 19) cubre SSR/ISR de las páginas públicas y Server Actions para mutaciones, apps/realtime aísla Socket.IO en un proceso dedicado (un WebSocket de larga duración no convive bien con el modelo serverless de Next) y apps/worker corre los jobs asíncronos vía BullMQ. La regla de negocio nunca vive en las rutas: está en packages/core (use cases puros, sin I/O, DTOs validados con zod), que solo conoce interfaces (ports) implementadas por packages/infra (repositorios Prisma). Esa decisión permitiría cambiar el borde HTTP por otro framework sin tocar el dominio. La autenticación usa Better Auth en vez de Auth.js porque el plugin organization resuelve la membresía multi-tenant de forma nativa (rol ADMIN de plataforma y OWNER/MANAGER/ARTIST por estudio; el cliente es un usuario sin membresía).
02La simulación de tatuaje es la feature central y se diseñó como un pipeline, no como una llamada única de IA: packages/ai expone puertos (Ta…+
La simulación de tatuaje es la feature central y se diseñó como un pipeline, no como una llamada única de IA: packages/ai expone puertos (TattooSimulationProvider, ImageGenerationProvider, SkinSegmentationProvider) y un registry que cambia de provider (Fal.ai, Replicate, OpenAI, Gemini, Stable Diffusion) con solo una env var, sin que ningún caso de uso importe un provider concreto. El flujo es segmentar la piel/parte del cuerpo, estimar perspectiva y curvatura, aplicar el diseño con warp en la escala y posición que el cliente eligió en el editor (arrastrar, redimensionar, rotar) y cerrar con un paso de img2img de baja intensidad que armoniza sombra, textura e iluminación preservando el trazo original. Como la generación tarda de 10 a 60 segundos, la API solo encola en BullMQ; el worker llama al provider y el resultado le llega al cliente por WebSocket (evento simulation:done), sin ningún request HTTP colgado esperando que la IA responda. Cada llamada registra un AiUsageLog (provider, operación, costo) que alimenta el dashboard admin y los límites de crédito por plan.
03El aislamiento multi-tenant es una base única con studioId, protegida en tres capas en vez de un filtro suelto en cada query. Una extensión …+
El aislamiento multi-tenant es una base única con studioId, protegida en tres capas en vez de un filtro suelto en cada query. Una extensión del Prisma Client exige studioId en toda consulta de modelo tenant-scoped: lanza error en dev y bloquea en prod. El RLS de Postgres (SET app.current_studio_id) aguanta incluso si la capa de aplicación falla. Y una suite de tests intenta acceso cross-tenant en cada ruta. Esa redundancia atrapó un bug real. getActor() leía la tabla StudioMember, protegida por RLS, sin abrir el contexto de tenant ni de admin. La política nunca coincidía, la consulta volvía vacía incluso con membresías reales, y el dueño del estudio terminaba viendo un panel idéntico al de un cliente común. En dev local el bug nunca apareció, porque el rol de Postgres que se usa ahí ignora RLS por defecto. Solo apareció en una auditoría visual, comparando pantallas de verdad. El fix fue withAdmin(), seguro porque el filtro final sigue siendo el propio userId. La lección es directa: sin probar contra un rol de producción, sin bypass de RLS, ese tipo de bug queda invisible por tiempo indefinido.
04El primer deploy real (Vercel + Neon, ambiente de prueba antes del VPS de producción) reveló tres bugs que ningún ambiente anterior ejercita…+
El primer deploy real (Vercel + Neon, ambiente de prueba antes del VPS de producción) reveló tres bugs que ningún ambiente anterior ejercitaba. La migration de la constraint anti-overbooking usaba tstzrange() en un índice. Esa función depende de la zona horaria de la sesión (es STABLE, no IMMUTABLE) y se rompe en un migrate deploy de verdad. La corrección fue cambiarla por tsrange(), que coincide con el tipo real de la columna. El ambiente local usa prisma db push, que ignora SQL crudo, y el CI no corría migrations, así que el bug quedó invisible hasta el primer migrate deploy real. Habría trabado el deploy en el VPS también. El bundle serverless de Vercel además 'perdía' el query engine de Prisma en un monorepo pnpm, resuelto con el plugin oficial de workaround, activo solo cuando VERCEL=1 para no afectar el Docker del VPS. Y el rate limiting de Better Auth usaba un limitador en memoria, que simplemente no funciona con múltiples instancias serverless. Se cambió por almacenamiento en base de datos: una tabla RateLimit, deliberadamente fuera del RLS, con el mismo patrón de User y Session, que tampoco son multi-tenant.
CONSTRUIDO CON — Next.js · TypeScript · Prisma · PostgreSQL · Turborepo · StripeCÓDIGO ↗
06 / RESULTADO
CADA MILLABAJO CONTROL.
