[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"portal-settings:stajic:es":3,"public-menus:all":38,"post:a-practical-monorepo-architecture-next-js-platform-admin-fastify-api-prisma-and-nginx:es":205,"related:post:a-practical-monorepo-architecture-next-js-platform-admin-fastify-api-prisma-and-nginx:es:1":598},{"statusCode":4,"data":5,"message":37},200,{"tenantId":6,"lang":7,"defaultLang":8,"siteUrl":9,"contactEmail":10,"brandName":11,"logoUrl":12,"siteName":11,"siteDescription":13,"ogImage":10,"robotsIndex":14,"socialLinks":10,"reservedSlugs":10,"seoPolicy":15},"stajic","es","de","https:\u002F\u002Fstajic.de",null,"Stajic Platform","\u002FLogo_Planet.svg","Stajic Portal",true,{"branding":16,"relatedContent":17,"crossDomainLinks":18},{"logoUrl":12},{"enabled":14},[19,22,25,28,31,34],{"url":20,"label":21,"isActive":14,"showInFooter":14,"includeInSameAs":14},"https:\u002F\u002Ffigure.rocks","figure.rocks",{"url":23,"label":24,"isActive":14,"showInFooter":14,"includeInSameAs":14},"https:\u002F\u002Floving.rocks","loving.rocks",{"url":26,"label":27,"isActive":14,"showInFooter":14,"includeInSameAs":14},"https:\u002F\u002Fbazify.com","bazify.com",{"url":29,"label":30,"isActive":14,"showInFooter":14,"includeInSameAs":14},"https:\u002F\u002Fbazify.de","bazify.de",{"url":32,"label":33,"isActive":14,"showInFooter":14,"includeInSameAs":14},"https:\u002F\u002Fbazify.at","bazify.at",{"url":35,"label":36,"isActive":14,"showInFooter":14,"includeInSameAs":14},"https:\u002F\u002Fbazify.ba","bazify.ba","Portal settings resolved",[39,45],{"id":40,"name":41,"location":42,"isActive":14,"isDefault":43,"items":44},1,"main-navigation","header",false,[],{"id":46,"name":47,"location":48,"isActive":14,"isDefault":14,"items":49},4,"main-menu","sidebar",[50,66,79,93,103,118,133],{"id":51,"title":52,"url":60,"target":61,"icon":62,"isActive":14,"type":63,"productId":10,"categoryId":10,"shopCategoryId":10,"articleId":10,"pageId":64,"portfolioId":10,"children":65},"item-18",{"de":53,"en":54,"es":55,"fr":56,"it":54,"ru":57,"sr":58,"zh":59},"Startseite","Home","Inicio","Accueil","Главная","Почетна","首页","\u002Ffull-stack-web-developer-munich-performance-seo-and-maintainable-builds","_self","i-lucide-home","page",111,[],{"id":67,"title":68,"url":75,"target":61,"icon":76,"isActive":14,"type":63,"productId":10,"categoryId":10,"shopCategoryId":10,"articleId":10,"pageId":77,"portfolioId":10,"children":78},"item-22",{"de":69,"en":69,"es":70,"fr":69,"it":71,"ru":72,"sr":73,"zh":74},"Vision","Visión","Visione","Видение","Визија","想象","\u002Fueber-uns-webdesign-muenchen-webaplikation","i-lucide-eye",113,[],{"id":80,"title":81,"url":89,"target":61,"icon":90,"isActive":14,"type":63,"productId":10,"categoryId":10,"shopCategoryId":10,"articleId":10,"pageId":91,"portfolioId":10,"children":92},"item-19",{"de":82,"en":83,"es":84,"fr":83,"it":85,"ru":86,"sr":87,"zh":88},"Leistungen","Services","Servicios","Servizi","Услуги","Услуге","服务","\u002Fservices-dienstleistungen-muenchen","i-lucide-wrench",116,[],{"id":94,"title":95,"url":99,"target":61,"icon":100,"isActive":14,"type":63,"productId":10,"categoryId":10,"shopCategoryId":10,"articleId":10,"pageId":101,"portfolioId":10,"children":102},"item-23",{"de":96,"en":96,"es":96,"fr":96,"it":96,"ru":97,"sr":97,"zh":98},"Blog","Блог","博客","\u002Fblog","i-lucide-book-open",112,[],{"id":104,"title":105,"url":114,"target":61,"icon":115,"isActive":14,"type":63,"productId":10,"categoryId":10,"shopCategoryId":10,"articleId":10,"pageId":116,"portfolioId":10,"children":117},"item-32",{"de":106,"en":107,"es":108,"fr":109,"it":110,"ru":111,"sr":112,"zh":113},"Neue Technologien","New Technologies","Nuevas tecnologías","Nouvelles technologies","Nuove tecnologie","Новые технологии","Нове технологије","新技术！","\u002Fneue-webtechnologien","i-lucide-sparkles",122,[],{"id":119,"title":120,"url":129,"target":61,"icon":130,"isActive":14,"type":63,"productId":10,"categoryId":10,"shopCategoryId":10,"articleId":10,"pageId":131,"portfolioId":10,"children":132},"item-20",{"de":121,"en":122,"es":123,"fr":124,"it":125,"ru":126,"sr":127,"zh":128},"Kontakt","Contact us!","Contacto","Contact","Contatto","Контакт","Контактирајте нас","联系我们！","\u002Fcontact","i-lucide-mail",115,[],{"id":134,"title":135,"url":144,"target":61,"icon":145,"isActive":14,"type":63,"productId":10,"categoryId":10,"shopCategoryId":10,"articleId":10,"pageId":146,"portfolioId":10,"children":147},"item-21",{"de":136,"en":137,"es":138,"fr":139,"it":140,"ru":141,"sr":142,"zh":143},"Unsere Arbeit","Our Work","Nuestro trabajo","Nos réalisations","I nostri lavori","Наши работы","Наши радови","文件夹","\u002Fportfolio","i-lucide-briefcase",114,[148,161,175,181,193],{"id":149,"title":150,"url":144,"target":61,"icon":159,"isActive":14,"type":63,"productId":10,"categoryId":10,"shopCategoryId":10,"articleId":10,"pageId":146,"portfolioId":10,"children":160},"item-24",{"de":151,"en":152,"es":153,"fr":154,"it":155,"ru":156,"sr":157,"zh":158},"Alle Projekte","All Projects","Todos los proyectos","Tous les projets","Tutti i progetti","Все проекты","Сви пројекти","所有项目","i-lucide-grid-3x3",[],{"id":162,"title":163,"url":171,"target":61,"icon":172,"isActive":14,"type":173,"productId":10,"categoryId":10,"shopCategoryId":10,"articleId":10,"pageId":10,"portfolioId":10,"children":174},"item-29",{"de":164,"en":165,"es":166,"fr":167,"it":168,"ru":169,"sr":170,"zh":143},"Local Roots, Global Reach","Local Roots - Global Reach","Empresa local ","Entreprise locale","Azienda locale","Местная компания","Локално предузеће глобално тржиште","\u002Fportfolio\u002Flocal-roots-global-reach-communication-media-systems-for-modern-business","i-lucide-folder","custom",[],{"id":176,"title":177,"url":179,"target":61,"icon":172,"isActive":14,"type":173,"productId":10,"categoryId":10,"shopCategoryId":10,"articleId":10,"pageId":10,"portfolioId":10,"children":180},"item-28",{"de":178,"en":178,"es":178,"fr":178,"it":178,"ru":178,"sr":178,"zh":178},"Solr Suggester","\u002Fportfolio\u002Fsolr-fuzzy-suggester-und-solr-infix-suggester-abfrage-ueber-ajax-und-filterung",[],{"id":182,"title":183,"url":191,"target":61,"icon":172,"isActive":14,"type":173,"productId":10,"categoryId":10,"shopCategoryId":10,"articleId":10,"pageId":10,"portfolioId":10,"children":192},"item-27",{"de":184,"en":185,"es":186,"fr":187,"it":188,"ru":189,"sr":190,"zh":185},"Firmenwebseite SEO","Company Website SEO","Sitio web corporativo SEO","Site web d’entreprise SEO","Sito web aziendale SEO","Корпоративный сайт SEO","Пословна веб-страница SEO","\u002Fportfolio\u002Fseo-sem-branding-mobile-webseite-muenchen",[],{"id":194,"title":195,"url":203,"target":61,"icon":172,"isActive":14,"type":173,"productId":10,"categoryId":10,"shopCategoryId":10,"articleId":10,"pageId":10,"portfolioId":10,"children":204},"item-31",{"de":196,"en":197,"es":198,"fr":199,"it":200,"ru":201,"sr":202,"zh":197},"Digitalisierungsportal","Digitalization Portal","Portal de digitalización","Portail de numérisation","Portale di digitalizzazione","Портал цифровизации","Портал за дигитализацију","\u002Fportfolio\u002Fdigitalisierungsportal-archiv-museum-bibliothek-ead-lido-mets-mods",[],{"statusCode":4,"data":206,"message":597},{"id":207,"title":208,"slug":209,"content":210,"contentJson":211,"excerpt":374,"featuredImage":375,"featuredImageAlt":376,"featuredImageCaption":10,"featuredImageTitle":10,"featuredImageCopyright":10,"featuredImageAuthor":10,"featuredImageSourceUrl":10,"featuredImageLicense":10,"featuredImageIsAiGenerated":43,"status":377,"publishedAt":378,"createdAt":379,"updatedAt":380,"seoLocalePaths":381,"categories":390,"author":398,"translations":403},"382","Una Arquitectura Monorepo Práctica con Next.js, Fastify, Prisma y NGINX","a-practical-monorepo-architecture-next-js-platform-admin-fastify-api-prisma-and-nginx","\u003Ch1>Monorepo de Plataforma: Next.js (Público + Admin) + API Fastify + DB Prisma + NGINX\u003C\u002Fh1>\n\u003Cp>Este documento describe la arquitectura objetivo actual para un monorepo con un sitio público Next.js, una aplicación de administración Next.js con sesiones de cookies, una API Fastify y un paquete de base de datos basado en Prisma. Está escrito para compañeros de equipo que trabajarán con múltiples paquetes y necesitan límites predecibles.\u003C\u002Fp>\n\u003Ch2>Restricciones que no estamos negociando (por ahora)\u003C\u002Fh2>\n\u003Cul>\u003Cli>El sitio público debe ser SSR (Next.js App Router). Sin atajos solo para SPA.\u003C\u002Fli>\u003Cli>El sitio público se comunica con el backend solo a través de rutas de API públicas bajo \u002Fapi (por ejemplo, \u002Fapi\u002Fcontent, \u002Fapi\u002Fmedia).\u003C\u002Fli>\u003Cli>El administrador utiliza autenticación de sesión basada en cookies. Cookie solo HTTP. Sin tokens de localStorage.\u003C\u002Fli>\u003Cli>La API es Fastify. Sesiones a través de @fastify\u002Fsession. Almacén “fakeRedis” para desarrollo; Redis en producción más adelante.\u003C\u002Fli>\u003Cli>El acceso a la base de datos es solo a través de @platform\u002Fdb. Sin clientes Prisma directos dispersos en las aplicaciones.\u003C\u002Fli>\u003Cli>NGINX termina TLS y enruta \u002F, \u002Fadmin y \u002Fapi. Un dominio está bien; el subdominio es opcional más adelante.\u003C\u002Fli>\u003Cli>El monitoreo existe desde el primer día (recopilación de Prometheus + marcadores de posición del panel de Grafana).\u003C\u002Fli>\u003C\u002Ful>\n\u003Ch2>Diseño del repositorio\u003C\u002Fh2>\n\u003Cpre class=\"code-block\">\u003Ccode>repo\u002F\n  apps\u002F\n    platform\u002F        # public site (SSR)\n    admin\u002F           # admin UI (cookie session)\n  packages\u002F\n    api\u002F             # Fastify backend (auth\u002Fusers\u002Fcontent\u002Fmedia)\n    db\u002F              # Prisma client + migrations\n    shared\u002F          # shared types, UI bits, utilities\n  INFRASTRUCTURE\u002F\n    nginx\u002F\n      nginx.conf\n      sites\u002F\n        app.conf\n    monitoring\u002F\n      prometheus.yml\n      grafana\u002F\n  docker-compose.yml\n  turbo.json\n  package.json\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2>Compensaciones (las reales)\u003C\u002Fh2>\n\u003Cul>\u003Cli>Las sesiones de cookies son aburridas. Eso es una característica. Funcionan detrás de proxies y mantienen la lógica de autenticación fuera del frontend. La desventaja: debes configurar correctamente SameSite\u002FSecure\u002Fpath, o perseguirás errores de inicio de sesión “aleatorios”.\u003C\u002Fli>\u003Cli>Una superficie \u002Fapi es limpia, pero también significa que debes ser estricto sobre lo que es público frente a lo que es solo para administradores. No filtres endpoints de administración solo porque sea conveniente.\u003C\u002Fli>\u003Cli>Prisma puede estar “listo para múltiples bases de datos” en el sentido de intercambiar proveedores. No es un puente mágico entre SQL y Mongo con un solo esquema. Si alguien dice que lo es, no lo ha implementado.\u003C\u002Fli>\u003Cli>Next.js SSR es bueno para el SEO y la primera carga. También puede sobrecargar la API si no configuras el almacenamiento en caché y la revalidación intencionalmente.\u003C\u002Fli>\u003C\u002Ful>\n\u003Ch2>Contrato de enrutamiento (NGINX como puerta principal)\u003C\u002Fh2>\n\u003Cp>Por ahora, enrutamos todo a través de un solo dominio. Es más sencillo de depurar. Si más adelante movemos la administración a un subdominio, revisaremos el alcance de las cookies y las suposiciones de CSRF.\u003C\u002Fp>\n\u003Cpre class=\"code-block\">\u003Ccode># INFRASTRUCTURE\u002Fnginx\u002Fsites\u002Fapp.conf\nserver {\n  listen 80;\n  server_name yourdomain.com;\n\n  location \u002Fapi\u002F {\n    proxy_pass http:\u002F\u002Fapi:4000\u002F;\n    proxy_set_header Host $host;\n    proxy_set_header X-Forwarded-Proto $scheme;\n    proxy_set_header X-Real-IP $remote_addr;\n  }\n\n  location \u002Fadmin\u002F {\n    proxy_pass http:\u002F\u002Fadmin:3001\u002F;\n    proxy_set_header Host $host;\n  }\n\n  location \u002F {\n    proxy_pass http:\u002F\u002Fplatform:3000\u002F;\n    proxy_set_header Host $host;\n  }\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2>Paquete API (Fastify) — sesiones y autenticación\u003C\u002Fh2>\n\u003Cp>La API es propietaria de la autenticación y el estado de la sesión. Las aplicaciones nunca emiten tokens. Simplemente devuelven cookies. Mantén la carga útil de la sesión pequeña. ID de usuario + rol. Eso es todo.\u003C\u002Fp>\n\u003Cpre class=\"code-block\">\u003Ccode>\u002F\u002F packages\u002Fapi\u002Fsrc\u002Fserver.ts\nimport Fastify from &quot;fastify&quot;;\nimport cookie from &quot;@fastify\u002Fcookie&quot;;\nimport session from &quot;@fastify\u002Fsession&quot;;\nimport { buildSessionStore } from &quot;.\u002FsessionStore&quot;;\nimport { authRoutes } from &quot;.\u002Froutes\u002Fauth&quot;;\nimport { meRoutes } from &quot;.\u002Froutes\u002Fme&quot;;\n\nexport async function buildServer() {\n  const app = Fastify({ logger: true });\n\n  await app.register(cookie);\n  await app.register(session, {\n    secret: process.env.SESSION_SECRET || &quot;dev-secret-change-me&quot;,\n    cookieName: &quot;sid&quot;,\n    cookie: {\n      httpOnly: true,\n      secure: process.env.NODE_ENV === &quot;production&quot;,\n      sameSite: &quot;lax&quot;,\n      path: &quot;\u002F&quot;\n    },\n    store: buildSessionStore(),\n    saveUninitialized: false\n  });\n\n  app.register(authRoutes, { prefix: &quot;\u002Fapi&quot; });\n  app.register(meRoutes, { prefix: &quot;\u002Fapi&quot; });\n\n  return app;\n}\n\n\u002F\u002F packages\u002Fapi\u002Fsrc\u002Findex.ts\nimport { buildServer } from &quot;.\u002Fserver&quot;;\n\n(async () =&gt; {\n  const app = await buildServer();\n  const port = Number(process.env.PORT || 4000);\n  await app.listen({ port, host: &quot;0.0.0.0&quot; });\n})();\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cpre class=\"code-block\">\u003Ccode>\u002F\u002F packages\u002Fapi\u002Fsrc\u002FsessionStore.ts\nimport type { SessionStore } from &quot;@fastify\u002Fsession&quot;;\n\nexport function buildSessionStore(): SessionStore {\n  const mem = new Map&lt;string, { value: any; expiresAt: number }&gt;();\n\n  return {\n    get: (sid, cb) =&gt; {\n      const hit = mem.get(sid);\n      if (!hit) return cb(null, null);\n      if (Date.now() &gt; hit.expiresAt) {\n        mem.delete(sid);\n        return cb(null, null);\n      }\n      cb(null, hit.value);\n    },\n    set: (sid, session, cb) =&gt; {\n      const ttlMs = 1000 * 60 * 60 * 8;\n      mem.set(sid, { value: session, expiresAt: Date.now() + ttlMs });\n      cb(null);\n    },\n    destroy: (sid, cb) =&gt; {\n      mem.delete(sid);\n      cb(null);\n    }\n  };\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2>Paquete DB (Prisma) — cliente único, tipos compartidos\u003C\u002Fh2>\n\u003Cp>Exponemos un único PrismaClient desde @platform\u002Fdb. La API importa eso y solo eso. Si creas un segundo cliente Prisma en una aplicación porque es “más rápido de prototipar”, solo estás creando un incidente futuro.\u003C\u002Fp>\n\u003Cpre class=\"code-block\">\u003Ccode>\u002F\u002F packages\u002Fdb\u002Fsrc\u002Findex.ts\nimport { PrismaClient } from &quot;@prisma\u002Fclient&quot;;\n\nexport const db = new PrismaClient();\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2>Aplicación de administración — sesión de cookies y guardia \u002Fme\u003C\u002Fh2>\n\u003Cp>La aplicación de administración es una aplicación normal de Next.js App Router. La única parte especial es que debe tratar la API como la fuente de verdad y siempre incluir cookies al llamar a \u002Fapi\u002Fme.\u003C\u002Fp>\n\u003Cpre class=\"code-block\">\u003Ccode>\u002F\u002F apps\u002Fadmin\u002Fapp\u002F(admin)\u002Flayout.tsx\nimport { cookies } from &quot;next\u002Fheaders&quot;;\n\nasync function getMe() {\n  const cookieHeader = cookies().toString();\n  const res = await fetch(`${process.env.ADMIN_BASE_URL}\u002Fapi\u002Fme`, {\n    headers: { cookie: cookieHeader },\n    cache: &quot;no-store&quot;\n  });\n  return res.json();\n}\n\nexport default async function AdminLayout({ children }: { children: React.ReactNode }) {\n  const me = await getMe();\n  if (!me?.ok) {\n    return (\n      &lt;html&gt;\n        &lt;body&gt;\n          &lt;p&gt;No autorizado. Ve a \u002Fadmin\u002Flogin.&lt;\u002Fp&gt;\n        &lt;\u002Fbody&gt;\n      &lt;\u002Fhtml&gt;\n    );\n  }\n\n  return (\n    &lt;html&gt;\n      &lt;body&gt;{children}&lt;\u002Fbody&gt;\n    &lt;\u002Fhtml&gt;\n  );\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2>Paso a paso: flujo de inicio de sesión del administrador\u003C\u002Fh2>\n\u003Col>\u003Cli>El navegador envía credenciales: POST \u002Fapi\u002Flogin\u003C\u002Fli>\u003Cli>NGINX reenvía \u002Fapi\u002F* a Fastify\u003C\u002Fli>\u003Cli>Fastify valida al usuario contra la DB (Prisma)\u003C\u002Fli>\u003Cli>Fastify escribe la sesión en el almacén fakeRedis (desarrollo) y devuelve Set-Cookie: sid=…\u003C\u002Fli>\u003Cli>El navegador almacena la cookie (solo HTTP)\u003C\u002Fli>\u003Cli>Los componentes del servidor de administración llaman a GET \u002Fapi\u002Fme con la cookie adjunta\u003C\u002Fli>\u003Cli>El administrador renderiza el panel solo si \u002Fme devuelve ok=true\u003C\u002Fli>\u003C\u002Fol>\n\u003Ch2>Una cosa que salió mal (para no repetirla)\u003C\u002Fh2>\n\u003Cp>Tuvimos un bucle de inicio de sesión a pesar de que \u002Fapi\u002Flogin devolvió 200. La cookie nunca se envió de vuelta en \u002Fapi\u002Fme. La causa fue aburrida: ruta de la cookie + desajuste de enrutamiento del proxy. Configuramos la cookie para \u002Fadmin por accidente, pero \u002Fapi\u002Fme reside bajo \u002Fapi. El navegador hizo exactamente lo que debía. No envió la cookie. La solución fue establecer la ruta de la cookie en \"\u002F\" y confirmar que NGINX no estaba reescribiendo las rutas de una manera que lo rompiera.\u003C\u002Fp>\n\u003Ch2>Plan del proyecto (lanzar primero, refinar después)\u003C\u002Fh2>\n\u003Col>\u003Cli>Fase 0: cableado del monorepo (Turbo, configuración de TS, importaciones de paquetes compartidos). Mantenerlo ligero.\u003C\u002Fli>\u003Cli>Fase 1: Endpoints de autenticación\u002Fsesión de la API: \u002Flogin, \u002Flogout, \u002Fme. Añadir limitación de tasa básica en \u002Flogin.\u003C\u002Fli>\u003Cli>Fase 2: MVP de administración: pantalla de inicio de sesión + guardia del lado del servidor + shell de panel mínimo.\u003C\u002Fli>\u003Cli>Fase 3: Endpoints de lectura de contenido para la plataforma pública: \u002Fapi\u002Fcontent\u002F* (solo lectura primero).\u003C\u002Fli>\u003Cli>Fase 4: CRUD de contenido en administración + carga de medios (empezar simple, luego cambiar el almacenamiento más tarde).\u003C\u002Fli>\u003Cli>Fase 5: Monitoreo y endurecimiento: endpoint de métricas de Prometheus, paneles de Grafana, encabezados, copias de seguridad.\u003C\u002Fli>\u003C\u002Fol>\n\u003Ch2>Definición de terminado (para no discutir después)\u003C\u002Fh2>\n\u003Cul>\u003Cli>Un navegador nuevo puede iniciar sesión en \u002Fadmin y permanecer conectado a través de las actualizaciones.\u003C\u002Fli>\u003Cli>La plataforma pública renderiza contenido SSR desde \u002Fapi\u002Fcontent sin exponer endpoints solo para administradores.\u003C\u002Fli>\u003Cli>NGINX enruta \u002F, \u002Fadmin, \u002Fapi correctamente en docker-compose.\u003C\u002Fli>\u003Cli>La API puede reiniciarse sin corromper el comportamiento de la sesión (los reinicios del almacén de desarrollo están bien, pero debe fallar elegantemente).\u003C\u002Fli>\u003Cli>Existe al menos un objetivo de raspado de Prometheus (incluso si los paneles de Grafana son marcadores de posición).\u003C\u002Fli>\u003C\u002Ful>\n\u003Chr class=\"my-8\">\n\u003Cp>Si estás implementando un nuevo endpoint, decide primero: ¿es público, solo para administradores o interno? No adivines. Luego, ponlo detrás del prefijo correcto y protégelo. Ese es todo el juego.\u003C\u002Fp>",{"time":212,"blocks":213,"version":373},1769827200000,[214,218,223,228,241,245,250,254,262,266,270,274,278,282,286,290,294,298,302,306,310,314,318,330,334,338,342,352,356,365,369],{"id":215,"data":216,"type":42},"h1_arch",{"text":217,"level":40},"Monorepo de Plataforma: Next.js (Público + Admin) + API Fastify + DB Prisma + NGINX",{"id":219,"data":220,"type":222},"p_scope",{"text":221},"Este documento describe la arquitectura objetivo actual para un monorepo con un sitio público Next.js, una aplicación de administración Next.js con sesiones de cookies, una API Fastify y un paquete de base de datos basado en Prisma. Está escrito para compañeros de equipo que trabajarán con múltiples paquetes y necesitan límites predecibles.","paragraph",{"id":224,"data":225,"type":42},"h2_constraints",{"text":226,"level":227},"Restricciones que no estamos negociando (por ahora)",2,{"id":229,"data":230,"type":240},"list_constraints",{"items":231,"style":239},[232,233,234,235,236,237,238],"El sitio público debe ser SSR (Next.js App Router). Sin atajos solo para SPA.","El sitio público se comunica con el backend solo a través de rutas de API públicas bajo \u002Fapi (por ejemplo, \u002Fapi\u002Fcontent, \u002Fapi\u002Fmedia).","El administrador utiliza autenticación de sesión basada en cookies. Cookie solo HTTP. Sin tokens de localStorage.","La API es Fastify. Sesiones a través de @fastify\u002Fsession. Almacén “fakeRedis” para desarrollo; Redis en producción más adelante.","El acceso a la base de datos es solo a través de @platform\u002Fdb. Sin clientes Prisma directos dispersos en las aplicaciones.","NGINX termina TLS y enruta \u002F, \u002Fadmin y \u002Fapi. Un dominio está bien; el subdominio es opcional más adelante.","El monitoreo existe desde el primer día (recopilación de Prometheus + marcadores de posición del panel de Grafana).","unordered","list",{"id":242,"data":243,"type":42},"h2_layout",{"text":244,"level":227},"Diseño del repositorio",{"id":246,"data":247,"type":249},"code_tree",{"code":248},"repo\u002F\n  apps\u002F\n    platform\u002F        # public site (SSR)\n    admin\u002F           # admin UI (cookie session)\n  packages\u002F\n    api\u002F             # Fastify backend (auth\u002Fusers\u002Fcontent\u002Fmedia)\n    db\u002F              # Prisma client + migrations\n    shared\u002F          # shared types, UI bits, utilities\n  INFRASTRUCTURE\u002F\n    nginx\u002F\n      nginx.conf\n      sites\u002F\n        app.conf\n    monitoring\u002F\n      prometheus.yml\n      grafana\u002F\n  docker-compose.yml\n  turbo.json\n  package.json","code",{"id":251,"data":252,"type":42},"h2_tradeoffs",{"text":253,"level":227},"Compensaciones (las reales)",{"id":255,"data":256,"type":240},"list_tradeoffs",{"items":257,"style":239},[258,259,260,261],"Las sesiones de cookies son aburridas. Eso es una característica. Funcionan detrás de proxies y mantienen la lógica de autenticación fuera del frontend. La desventaja: debes configurar correctamente SameSite\u002FSecure\u002Fpath, o perseguirás errores de inicio de sesión “aleatorios”.","Una superficie \u002Fapi es limpia, pero también significa que debes ser estricto sobre lo que es público frente a lo que es solo para administradores. No filtres endpoints de administración solo porque sea conveniente.","Prisma puede estar “listo para múltiples bases de datos” en el sentido de intercambiar proveedores. No es un puente mágico entre SQL y Mongo con un solo esquema. Si alguien dice que lo es, no lo ha implementado.","Next.js SSR es bueno para el SEO y la primera carga. También puede sobrecargar la API si no configuras el almacenamiento en caché y la revalidación intencionalmente.",{"id":263,"data":264,"type":42},"h2_routing",{"text":265,"level":227},"Contrato de enrutamiento (NGINX como puerta principal)",{"id":267,"data":268,"type":222},"p_routing",{"text":269},"Por ahora, enrutamos todo a través de un solo dominio. Es más sencillo de depurar. Si más adelante movemos la administración a un subdominio, revisaremos el alcance de las cookies y las suposiciones de CSRF.",{"id":271,"data":272,"type":249},"code_nginx",{"code":273},"# INFRASTRUCTURE\u002Fnginx\u002Fsites\u002Fapp.conf\nserver {\n  listen 80;\n  server_name yourdomain.com;\n\n  location \u002Fapi\u002F {\n    proxy_pass http:\u002F\u002Fapi:4000\u002F;\n    proxy_set_header Host $host;\n    proxy_set_header X-Forwarded-Proto $scheme;\n    proxy_set_header X-Real-IP $remote_addr;\n  }\n\n  location \u002Fadmin\u002F {\n    proxy_pass http:\u002F\u002Fadmin:3001\u002F;\n    proxy_set_header Host $host;\n  }\n\n  location \u002F {\n    proxy_pass http:\u002F\u002Fplatform:3000\u002F;\n    proxy_set_header Host $host;\n  }\n}",{"id":275,"data":276,"type":42},"h2_api",{"text":277,"level":227},"Paquete API (Fastify) — sesiones y autenticación",{"id":279,"data":280,"type":222},"p_api",{"text":281},"La API es propietaria de la autenticación y el estado de la sesión. Las aplicaciones nunca emiten tokens. Simplemente devuelven cookies. Mantén la carga útil de la sesión pequeña. ID de usuario + rol. Eso es todo.",{"id":283,"data":284,"type":249},"code_server",{"code":285},"\u002F\u002F packages\u002Fapi\u002Fsrc\u002Fserver.ts\nimport Fastify from \"fastify\";\nimport cookie from \"@fastify\u002Fcookie\";\nimport session from \"@fastify\u002Fsession\";\nimport { buildSessionStore } from \".\u002FsessionStore\";\nimport { authRoutes } from \".\u002Froutes\u002Fauth\";\nimport { meRoutes } from \".\u002Froutes\u002Fme\";\n\nexport async function buildServer() {\n  const app = Fastify({ logger: true });\n\n  await app.register(cookie);\n  await app.register(session, {\n    secret: process.env.SESSION_SECRET || \"dev-secret-change-me\",\n    cookieName: \"sid\",\n    cookie: {\n      httpOnly: true,\n      secure: process.env.NODE_ENV === \"production\",\n      sameSite: \"lax\",\n      path: \"\u002F\"\n    },\n    store: buildSessionStore(),\n    saveUninitialized: false\n  });\n\n  app.register(authRoutes, { prefix: \"\u002Fapi\" });\n  app.register(meRoutes, { prefix: \"\u002Fapi\" });\n\n  return app;\n}\n\n\u002F\u002F packages\u002Fapi\u002Fsrc\u002Findex.ts\nimport { buildServer } from \".\u002Fserver\";\n\n(async () => {\n  const app = await buildServer();\n  const port = Number(process.env.PORT || 4000);\n  await app.listen({ port, host: \"0.0.0.0\" });\n})();",{"id":287,"data":288,"type":249},"code_session_store",{"code":289},"\u002F\u002F packages\u002Fapi\u002Fsrc\u002FsessionStore.ts\nimport type { SessionStore } from \"@fastify\u002Fsession\";\n\nexport function buildSessionStore(): SessionStore {\n  const mem = new Map\u003Cstring, { value: any; expiresAt: number }>();\n\n  return {\n    get: (sid, cb) => {\n      const hit = mem.get(sid);\n      if (!hit) return cb(null, null);\n      if (Date.now() > hit.expiresAt) {\n        mem.delete(sid);\n        return cb(null, null);\n      }\n      cb(null, hit.value);\n    },\n    set: (sid, session, cb) => {\n      const ttlMs = 1000 * 60 * 60 * 8;\n      mem.set(sid, { value: session, expiresAt: Date.now() + ttlMs });\n      cb(null);\n    },\n    destroy: (sid, cb) => {\n      mem.delete(sid);\n      cb(null);\n    }\n  };\n}",{"id":291,"data":292,"type":42},"h2_db",{"text":293,"level":227},"Paquete DB (Prisma) — cliente único, tipos compartidos",{"id":295,"data":296,"type":222},"p_db",{"text":297},"Exponemos un único PrismaClient desde @platform\u002Fdb. La API importa eso y solo eso. Si creas un segundo cliente Prisma en una aplicación porque es “más rápido de prototipar”, solo estás creando un incidente futuro.",{"id":299,"data":300,"type":249},"code_db",{"code":301},"\u002F\u002F packages\u002Fdb\u002Fsrc\u002Findex.ts\nimport { PrismaClient } from \"@prisma\u002Fclient\";\n\nexport const db = new PrismaClient();",{"id":303,"data":304,"type":42},"h2_admin",{"text":305,"level":227},"Aplicación de administración — sesión de cookies y guardia \u002Fme",{"id":307,"data":308,"type":222},"p_admin",{"text":309},"La aplicación de administración es una aplicación normal de Next.js App Router. La única parte especial es que debe tratar la API como la fuente de verdad y siempre incluir cookies al llamar a \u002Fapi\u002Fme.",{"id":311,"data":312,"type":249},"code_admin_guard",{"code":313},"\u002F\u002F apps\u002Fadmin\u002Fapp\u002F(admin)\u002Flayout.tsx\nimport { cookies } from \"next\u002Fheaders\";\n\nasync function getMe() {\n  const cookieHeader = cookies().toString();\n  const res = await fetch(`${process.env.ADMIN_BASE_URL}\u002Fapi\u002Fme`, {\n    headers: { cookie: cookieHeader },\n    cache: \"no-store\"\n  });\n  return res.json();\n}\n\nexport default async function AdminLayout({ children }: { children: React.ReactNode }) {\n  const me = await getMe();\n  if (!me?.ok) {\n    return (\n      \u003Chtml>\n        \u003Cbody>\n          \u003Cp>No autorizado. Ve a \u002Fadmin\u002Flogin.\u003C\u002Fp>\n        \u003C\u002Fbody>\n      \u003C\u002Fhtml>\n    );\n  }\n\n  return (\n    \u003Chtml>\n      \u003Cbody>{children}\u003C\u002Fbody>\n    \u003C\u002Fhtml>\n  );\n}",{"id":315,"data":316,"type":42},"h2_flow",{"text":317,"level":227},"Paso a paso: flujo de inicio de sesión del administrador",{"id":319,"data":320,"type":240},"list_flow",{"items":321,"style":329},[322,323,324,325,326,327,328],"El navegador envía credenciales: POST \u002Fapi\u002Flogin","NGINX reenvía \u002Fapi\u002F* a Fastify","Fastify valida al usuario contra la DB (Prisma)","Fastify escribe la sesión en el almacén fakeRedis (desarrollo) y devuelve Set-Cookie: sid=…","El navegador almacena la cookie (solo HTTP)","Los componentes del servidor de administración llaman a GET \u002Fapi\u002Fme con la cookie adjunta","El administrador renderiza el panel solo si \u002Fme devuelve ok=true","ordered",{"id":331,"data":332,"type":42},"h2_went_wrong",{"text":333,"level":227},"Una cosa que salió mal (para no repetirla)",{"id":335,"data":336,"type":222},"p_went_wrong",{"text":337},"Tuvimos un bucle de inicio de sesión a pesar de que \u002Fapi\u002Flogin devolvió 200. La cookie nunca se envió de vuelta en \u002Fapi\u002Fme. La causa fue aburrida: ruta de la cookie + desajuste de enrutamiento del proxy. Configuramos la cookie para \u002Fadmin por accidente, pero \u002Fapi\u002Fme reside bajo \u002Fapi. El navegador hizo exactamente lo que debía. No envió la cookie. La solución fue establecer la ruta de la cookie en \"\u002F\" y confirmar que NGINX no estaba reescribiendo las rutas de una manera que lo rompiera.",{"id":339,"data":340,"type":42},"h2_project_plan",{"text":341,"level":227},"Plan del proyecto (lanzar primero, refinar después)",{"id":343,"data":344,"type":240},"list_plan",{"items":345,"style":329},[346,347,348,349,350,351],"Fase 0: cableado del monorepo (Turbo, configuración de TS, importaciones de paquetes compartidos). Mantenerlo ligero.","Fase 1: Endpoints de autenticación\u002Fsesión de la API: \u002Flogin, \u002Flogout, \u002Fme. Añadir limitación de tasa básica en \u002Flogin.","Fase 2: MVP de administración: pantalla de inicio de sesión + guardia del lado del servidor + shell de panel mínimo.","Fase 3: Endpoints de lectura de contenido para la plataforma pública: \u002Fapi\u002Fcontent\u002F* (solo lectura primero).","Fase 4: CRUD de contenido en administración + carga de medios (empezar simple, luego cambiar el almacenamiento más tarde).","Fase 5: Monitoreo y endurecimiento: endpoint de métricas de Prometheus, paneles de Grafana, encabezados, copias de seguridad.",{"id":353,"data":354,"type":42},"h2_definition_done",{"text":355,"level":227},"Definición de terminado (para no discutir después)",{"id":357,"data":358,"type":240},"list_dod",{"items":359,"style":239},[360,361,362,363,364],"Un navegador nuevo puede iniciar sesión en \u002Fadmin y permanecer conectado a través de las actualizaciones.","La plataforma pública renderiza contenido SSR desde \u002Fapi\u002Fcontent sin exponer endpoints solo para administradores.","NGINX enruta \u002F, \u002Fadmin, \u002Fapi correctamente en docker-compose.","La API puede reiniciarse sin corromper el comportamiento de la sesión (los reinicios del almacén de desarrollo están bien, pero debe fallar elegantemente).","Existe al menos un objetivo de raspado de Prometheus (incluso si los paneles de Grafana son marcadores de posición).",{"id":366,"data":367,"type":368},"hr_end",{},"delimiter",{"id":370,"data":371,"type":222},"p_next",{"text":372},"Si estás implementando un nuevo endpoint, decide primero: ¿es público, solo para administradores o interno? No adivines. Luego, ponlo detrás del prefijo correcto y protégelo. Ese es todo el juego.","2.29.1","Explora una arquitectura monorepo práctica utilizando Next.js, Fastify, Prisma y NGINX, destacando la integración y el flujo de trabajo en el mundo real.","\u002Fuploads\u002F2026\u002F01\u002Fa-practical-monorepo-architecture-next-js-platform-admin-fastify-api-prisma-and-nginx-1769885526116-q1100n.webp","a-practical-monorepo-architecture-next-js-platform-admin-fastify-api-prisma-and-nginx-1769885526116-q1100n","PUBLISHED","2026-01-31T08:18:00.000Z","2026-01-31T17:18:53.110Z","2026-02-20T20:41:28.972Z",{"en":382,"de":383,"sr":384,"es":385,"fr":386,"it":387,"ru":388,"zh":389},"\u002Fblog\u002Fa-practical-monorepo-architecture-next-js-platform-admin-fastify-api-prisma-and-nginx","\u002Fde\u002Fblog\u002Fa-practical-monorepo-architecture-next-js-platform-admin-fastify-api-prisma-and-nginx","\u002Fsr\u002Fblog\u002Fa-practical-monorepo-architecture-next-js-platform-admin-fastify-api-prisma-and-nginx","\u002Fes\u002Fblog\u002Fa-practical-monorepo-architecture-next-js-platform-admin-fastify-api-prisma-and-nginx","\u002Ffr\u002Fblog\u002Fa-practical-monorepo-architecture-next-js-platform-admin-fastify-api-prisma-and-nginx","\u002Fit\u002Fblog\u002Fa-practical-monorepo-architecture-next-js-platform-admin-fastify-api-prisma-and-nginx","\u002Fru\u002Fblog\u002Fa-practical-monorepo-architecture-next-js-platform-admin-fastify-api-prisma-and-nginx","\u002Fzh\u002Fblog\u002Fa-practical-monorepo-architecture-next-js-platform-admin-fastify-api-prisma-and-nginx",[391,394],{"id":46,"name":392,"slug":393},"Aaasaasa","aaasaasa",{"id":395,"name":396,"slug":397},48,"Capacidades","capabilities",{"id":399,"login":400,"email":401,"displayName":402},"20","rooth8233","aleksandar@stajic.de","Aleksandar Stajić",[404,527],{"lang":405,"title":406,"content":407,"contentJson":408,"excerpt":526},"en","A Practical Monorepo Architecture with Next.js, Fastify, Prisma, and NGINX","{\"time\":1769827200000,\"blocks\":[{\"id\":\"h1_arch\",\"data\":{\"text\":\"Platform Monorepo: Next.js (Public + Admin) + Fastify API + Prisma DB + NGINX\",\"level\":1},\"type\":\"header\"},{\"id\":\"p_scope\",\"data\":{\"text\":\"This document describes the current target architecture for a monorepo with a public Next.js site, an admin Next.js app with cookie sessions, a Fastify API, and a Prisma-based DB package. It’s written for teammates who will touch multiple packages and need predictable boundaries.\"},\"type\":\"paragraph\"},{\"id\":\"h2_constraints\",\"data\":{\"text\":\"Constraints we’re not negotiating (for now)\",\"level\":2},\"type\":\"header\"},{\"id\":\"list_constraints\",\"data\":{\"items\":[\"Public site must be SSR (Next.js App Router). No SPA-only shortcuts.\",\"Public site talks to the backend only via public API routes under \u002Fapi (e.g., \u002Fapi\u002Fcontent, \u002Fapi\u002Fmedia).\",\"Admin uses cookie-based session auth. HTTP-only cookie. No localStorage tokens.\",\"API is Fastify. Sessions via @fastify\u002Fsession. “fakeRedis” store for development; Redis in production later.\",\"DB access is via @platform\u002Fdb only. No direct Prisma clients scattered in apps.\",\"NGINX terminates TLS and routes \u002F, \u002Fadmin, and \u002Fapi. One domain is fine; subdomain is optional later.\",\"Monitoring exists from day one (Prometheus scrape + Grafana dashboard placeholders).\"],\"style\":\"unordered\"},\"type\":\"list\"},{\"id\":\"h2_layout\",\"data\":{\"text\":\"Repository layout\",\"level\":2},\"type\":\"header\"},{\"id\":\"code_tree\",\"data\":{\"code\":\"repo\u002F\\n  apps\u002F\\n    platform\u002F        # public site (SSR)\\n    admin\u002F           # admin UI (cookie session)\\n  packages\u002F\\n    api\u002F             # Fastify backend (auth\u002Fusers\u002Fcontent\u002Fmedia)\\n    db\u002F              # Prisma client + migrations\\n    shared\u002F          # shared types, UI bits, utilities\\n  INFRASTRUCTURE\u002F\\n    nginx\u002F\\n      nginx.conf\\n      sites\u002F\\n        app.conf\\n    monitoring\u002F\\n      prometheus.yml\\n      grafana\u002F\\n  docker-compose.yml\\n  turbo.json\\n  package.json\"},\"type\":\"code\"},{\"id\":\"h2_tradeoffs\",\"data\":{\"text\":\"Trade-offs (the real ones)\",\"level\":2},\"type\":\"header\"},{\"id\":\"list_tradeoffs\",\"data\":{\"items\":[\"Cookie sessions are boring. That’s a feature. They work behind proxies and keep auth logic out of the frontend. The downside: you must get SameSite\u002FSecure\u002Fpath right, or you’ll chase “random” login bugs.\",\"One \u002Fapi surface is clean, but it also means you must be strict about what is public vs admin-only. Don’t leak admin endpoints just because it’s convenient.\",\"Prisma can be “multi-db ready” in the sense of swapping providers. It is not a magic bridge between SQL and Mongo with a single schema. If someone says it is, they haven’t shipped it.\",\"Next.js SSR is good for SEO and first load. It can also slam the API if you don’t set caching and revalidation intentionally.\"],\"style\":\"unordered\"},\"type\":\"list\"},{\"id\":\"h2_routing\",\"data\":{\"text\":\"Routing contract (NGINX as the front door)\",\"level\":2},\"type\":\"header\"},{\"id\":\"p_routing\",\"data\":{\"text\":\"We route everything through one domain for now. It’s simpler to debug. If we move admin to a subdomain later, we’ll revisit cookie scope and CSRF assumptions.\"},\"type\":\"paragraph\"},{\"id\":\"code_nginx\",\"data\":{\"code\":\"# INFRASTRUCTURE\u002Fnginx\u002Fsites\u002Fapp.conf\\nserver {\\n  listen 80;\\n  server_name yourdomain.com;\\n\\n  location \u002Fapi\u002F {\\n    proxy_pass http:\u002F\u002Fapi:4000\u002F;\\n    proxy_set_header Host $host;\\n    proxy_set_header X-Forwarded-Proto $scheme;\\n    proxy_set_header X-Real-IP $remote_addr;\\n  }\\n\\n  location \u002Fadmin\u002F {\\n    proxy_pass http:\u002F\u002Fadmin:3001\u002F;\\n    proxy_set_header Host $host;\\n  }\\n\\n  location \u002F {\\n    proxy_pass http:\u002F\u002Fplatform:3000\u002F;\\n    proxy_set_header Host $host;\\n  }\\n}\"},\"type\":\"code\"},{\"id\":\"h2_api\",\"data\":{\"text\":\"API package (Fastify) — sessions and auth\",\"level\":2},\"type\":\"header\"},{\"id\":\"p_api\",\"data\":{\"text\":\"The API owns authentication and session state. The apps never mint tokens. They just send cookies back. Keep the session payload small. User id + role. That’s it.\"},\"type\":\"paragraph\"},{\"id\":\"code_server\",\"data\":{\"code\":\"\u002F\u002F packages\u002Fapi\u002Fsrc\u002Fserver.ts\\nimport Fastify from \\\"fastify\\\";\\nimport cookie from \\\"@fastify\u002Fcookie\\\";\\nimport session from \\\"@fastify\u002Fsession\\\";\\nimport { buildSessionStore } from \\\".\u002FsessionStore\\\";\\nimport { authRoutes } from \\\".\u002Froutes\u002Fauth\\\";\\nimport { meRoutes } from \\\".\u002Froutes\u002Fme\\\";\\n\\nexport async function buildServer() {\\n  const app = Fastify({ logger: true });\\n\\n  await app.register(cookie);\\n  await app.register(session, {\\n    secret: process.env.SESSION_SECRET || \\\"dev-secret-change-me\\\",\\n    cookieName: \\\"sid\\\",\\n    cookie: {\\n      httpOnly: true,\\n      secure: process.env.NODE_ENV === \\\"production\\\",\\n      sameSite: \\\"lax\\\",\\n      path: \\\"\u002F\\\"\\n    },\\n    store: buildSessionStore(),\\n    saveUninitialized: false\\n  });\\n\\n  app.register(authRoutes, { prefix: \\\"\u002Fapi\\\" });\\n  app.register(meRoutes, { prefix: \\\"\u002Fapi\\\" });\\n\\n  return app;\\n}\\n\\n\u002F\u002F packages\u002Fapi\u002Fsrc\u002Findex.ts\\nimport { buildServer } from \\\".\u002Fserver\\\";\\n\\n(async () => {\\n  const app = await buildServer();\\n  const port = Number(process.env.PORT || 4000);\\n  await app.listen({ port, host: \\\"0.0.0.0\\\" });\\n})();\"},\"type\":\"code\"},{\"id\":\"code_session_store\",\"data\":{\"code\":\"\u002F\u002F packages\u002Fapi\u002Fsrc\u002FsessionStore.ts\\nimport type { SessionStore } from \\\"@fastify\u002Fsession\\\";\\n\\nexport function buildSessionStore(): SessionStore {\\n  const mem = new Map\u003Cstring, { value: any; expiresAt: number }>();\\n\\n  return {\\n    get: (sid, cb) => {\\n      const hit = mem.get(sid);\\n      if (!hit) return cb(null, null);\\n      if (Date.now() > hit.expiresAt) {\\n        mem.delete(sid);\\n        return cb(null, null);\\n      }\\n      cb(null, hit.value);\\n    },\\n    set: (sid, session, cb) => {\\n      const ttlMs = 1000 * 60 * 60 * 8;\\n      mem.set(sid, { value: session, expiresAt: Date.now() + ttlMs });\\n      cb(null);\\n    },\\n    destroy: (sid, cb) => {\\n      mem.delete(sid);\\n      cb(null);\\n    }\\n  };\\n}\"},\"type\":\"code\"},{\"id\":\"h2_db\",\"data\":{\"text\":\"DB package (Prisma) — single client, shared types\",\"level\":2},\"type\":\"header\"},{\"id\":\"p_db\",\"data\":{\"text\":\"We expose one PrismaClient from @platform\u002Fdb. The API imports that and only that. If you create a second Prisma client in an app because it’s “faster to prototype”, you’re just creating a future incident.\"},\"type\":\"paragraph\"},{\"id\":\"code_db\",\"data\":{\"code\":\"\u002F\u002F packages\u002Fdb\u002Fsrc\u002Findex.ts\\nimport { PrismaClient } from \\\"@prisma\u002Fclient\\\";\\n\\nexport const db = new PrismaClient();\"},\"type\":\"code\"},{\"id\":\"h2_admin\",\"data\":{\"text\":\"Admin app — cookie session and \u002Fme guard\",\"level\":2},\"type\":\"header\"},{\"id\":\"p_admin\",\"data\":{\"text\":\"Admin is a normal Next.js App Router app. The only special part is that it must treat the API as the source of truth and always include cookies when calling \u002Fapi\u002Fme.\"},\"type\":\"paragraph\"},{\"id\":\"code_admin_guard\",\"data\":{\"code\":\"\u002F\u002F apps\u002Fadmin\u002Fapp\u002F(admin)\u002Flayout.tsx\\nimport { cookies } from \\\"next\u002Fheaders\\\";\\n\\nasync function getMe() {\\n  const cookieHeader = cookies().toString();\\n  const res = await fetch(`${process.env.ADMIN_BASE_URL}\u002Fapi\u002Fme`, {\\n    headers: { cookie: cookieHeader },\\n    cache: \\\"no-store\\\"\\n  });\\n  return res.json();\\n}\\n\\nexport default async function AdminLayout({ children }: { children: React.ReactNode }) {\\n  const me = await getMe();\\n  if (!me?.ok) {\\n    return (\\n      \u003Chtml>\\n        \u003Cbody>\\n          \u003Cp>Unauthorized. Go to \u002Fadmin\u002Flogin.\u003C\u002Fp>\\n        \u003C\u002Fbody>\\n      \u003C\u002Fhtml>\\n    );\\n  }\\n\\n  return (\\n    \u003Chtml>\\n      \u003Cbody>{children}\u003C\u002Fbody>\\n    \u003C\u002Fhtml>\\n  );\\n}\"},\"type\":\"code\"},{\"id\":\"h2_flow\",\"data\":{\"text\":\"Step-by-step: admin login flow\",\"level\":2},\"type\":\"header\"},{\"id\":\"list_flow\",\"data\":{\"items\":[\"Browser submits credentials: POST \u002Fapi\u002Flogin\",\"NGINX forwards \u002Fapi\u002F* to Fastify\",\"Fastify validates user against DB (Prisma)\",\"Fastify writes session into fakeRedis store (dev) and returns Set-Cookie: sid=…\",\"Browser stores the cookie (HTTP-only)\",\"Admin server components call GET \u002Fapi\u002Fme with the cookie attached\",\"Admin renders dashboard only if \u002Fme returns ok=true\"],\"style\":\"ordered\"},\"type\":\"list\"},{\"id\":\"h2_went_wrong\",\"data\":{\"text\":\"One thing that went wrong (so we don’t repeat it)\",\"level\":2},\"type\":\"header\"},{\"id\":\"p_went_wrong\",\"data\":{\"text\":\"We had a login loop even though \u002Fapi\u002Flogin returned 200. The cookie was never sent back on \u002Fapi\u002Fme. The cause was boring: cookie path + proxy routing mismatch. We set the cookie for \u002Fadmin by accident, but \u002Fapi\u002Fme lives under \u002Fapi. Browser did exactly what it should. It didn’t send the cookie. Fix was to set cookie path to \\\"\u002F\\\" and confirm NGINX wasn’t rewriting paths in a way that broke it.\"},\"type\":\"paragraph\"},{\"id\":\"h2_project_plan\",\"data\":{\"text\":\"Project plan (ship-first, refine later)\",\"level\":2},\"type\":\"header\"},{\"id\":\"list_plan\",\"data\":{\"items\":[\"Phase 0: monorepo wiring (Turbo, TS config, shared package imports). Keep it lean.\",\"Phase 1: API auth\u002Fsession endpoints: \u002Flogin, \u002Flogout, \u002Fme. Add basic rate limiting on \u002Flogin.\",\"Phase 2: Admin MVP: login screen + server-side guard + minimal dashboard shell.\",\"Phase 3: Content read endpoints for public platform: \u002Fapi\u002Fcontent\u002F* (read-only first).\",\"Phase 4: Content CRUD in admin + media upload (start simple, then swap storage later).\",\"Phase 5: Monitoring and hardening: Prometheus metrics endpoint, Grafana dashboards, headers, backups.\"],\"style\":\"ordered\"},\"type\":\"list\"},{\"id\":\"h2_definition_done\",\"data\":{\"text\":\"Definition of done (so we don’t argue later)\",\"level\":2},\"type\":\"header\"},{\"id\":\"list_dod\",\"data\":{\"items\":[\"A fresh browser can log into \u002Fadmin and stay logged in across refreshes.\",\"Public platform renders SSR content from \u002Fapi\u002Fcontent without exposing admin-only endpoints.\",\"NGINX routes \u002F, \u002Fadmin, \u002Fapi correctly in docker-compose.\",\"API can restart without corrupting session behavior (dev store resets are fine, but it must fail gracefully).\",\"At least one Prometheus scrape target exists (even if Grafana panels are placeholders).\"],\"style\":\"unordered\"},\"type\":\"list\"},{\"id\":\"hr_end\",\"data\":{},\"type\":\"delimiter\"},{\"id\":\"p_next\",\"data\":{\"text\":\"If you’re implementing a new endpoint, decide first: is it public, admin-only, or internal. Don’t guess. Then put it behind the right prefix and guard it. That’s the whole game.\"},\"type\":\"paragraph\"}],\"version\":\"2.29.1\"}",{"time":212,"blocks":409,"version":373},[410,413,416,419,429,432,434,437,444,447,450,452,455,458,460,462,465,468,470,473,476,479,482,492,495,498,501,510,513,521,523],{"id":215,"data":411,"type":42},{"text":412,"level":40},"Platform Monorepo: Next.js (Public + Admin) + Fastify API + Prisma DB + NGINX",{"id":219,"data":414,"type":222},{"text":415},"This document describes the current target architecture for a monorepo with a public Next.js site, an admin Next.js app with cookie sessions, a Fastify API, and a Prisma-based DB package. It’s written for teammates who will touch multiple packages and need predictable boundaries.",{"id":224,"data":417,"type":42},{"text":418,"level":227},"Constraints we’re not negotiating (for now)",{"id":229,"data":420,"type":240},{"items":421,"style":239},[422,423,424,425,426,427,428],"Public site must be SSR (Next.js App Router). No SPA-only shortcuts.","Public site talks to the backend only via public API routes under \u002Fapi (e.g., \u002Fapi\u002Fcontent, \u002Fapi\u002Fmedia).","Admin uses cookie-based session auth. HTTP-only cookie. No localStorage tokens.","API is Fastify. Sessions via @fastify\u002Fsession. “fakeRedis” store for development; Redis in production later.","DB access is via @platform\u002Fdb only. No direct Prisma clients scattered in apps.","NGINX terminates TLS and routes \u002F, \u002Fadmin, and \u002Fapi. One domain is fine; subdomain is optional later.","Monitoring exists from day one (Prometheus scrape + Grafana dashboard placeholders).",{"id":242,"data":430,"type":42},{"text":431,"level":227},"Repository layout",{"id":246,"data":433,"type":249},{"code":248},{"id":251,"data":435,"type":42},{"text":436,"level":227},"Trade-offs (the real ones)",{"id":255,"data":438,"type":240},{"items":439,"style":239},[440,441,442,443],"Cookie sessions are boring. That’s a feature. They work behind proxies and keep auth logic out of the frontend. The downside: you must get SameSite\u002FSecure\u002Fpath right, or you’ll chase “random” login bugs.","One \u002Fapi surface is clean, but it also means you must be strict about what is public vs admin-only. Don’t leak admin endpoints just because it’s convenient.","Prisma can be “multi-db ready” in the sense of swapping providers. It is not a magic bridge between SQL and Mongo with a single schema. If someone says it is, they haven’t shipped it.","Next.js SSR is good for SEO and first load. It can also slam the API if you don’t set caching and revalidation intentionally.",{"id":263,"data":445,"type":42},{"text":446,"level":227},"Routing contract (NGINX as the front door)",{"id":267,"data":448,"type":222},{"text":449},"We route everything through one domain for now. It’s simpler to debug. If we move admin to a subdomain later, we’ll revisit cookie scope and CSRF assumptions.",{"id":271,"data":451,"type":249},{"code":273},{"id":275,"data":453,"type":42},{"text":454,"level":227},"API package (Fastify) — sessions and auth",{"id":279,"data":456,"type":222},{"text":457},"The API owns authentication and session state. The apps never mint tokens. They just send cookies back. Keep the session payload small. User id + role. That’s it.",{"id":283,"data":459,"type":249},{"code":285},{"id":287,"data":461,"type":249},{"code":289},{"id":291,"data":463,"type":42},{"text":464,"level":227},"DB package (Prisma) — single client, shared types",{"id":295,"data":466,"type":222},{"text":467},"We expose one PrismaClient from @platform\u002Fdb. The API imports that and only that. If you create a second Prisma client in an app because it’s “faster to prototype”, you’re just creating a future incident.",{"id":299,"data":469,"type":249},{"code":301},{"id":303,"data":471,"type":42},{"text":472,"level":227},"Admin app — cookie session and \u002Fme guard",{"id":307,"data":474,"type":222},{"text":475},"Admin is a normal Next.js App Router app. The only special part is that it must treat the API as the source of truth and always include cookies when calling \u002Fapi\u002Fme.",{"id":311,"data":477,"type":249},{"code":478},"\u002F\u002F apps\u002Fadmin\u002Fapp\u002F(admin)\u002Flayout.tsx\nimport { cookies } from \"next\u002Fheaders\";\n\nasync function getMe() {\n  const cookieHeader = cookies().toString();\n  const res = await fetch(`${process.env.ADMIN_BASE_URL}\u002Fapi\u002Fme`, {\n    headers: { cookie: cookieHeader },\n    cache: \"no-store\"\n  });\n  return res.json();\n}\n\nexport default async function AdminLayout({ children }: { children: React.ReactNode }) {\n  const me = await getMe();\n  if (!me?.ok) {\n    return (\n      \u003Chtml>\n        \u003Cbody>\n          \u003Cp>Unauthorized. Go to \u002Fadmin\u002Flogin.\u003C\u002Fp>\n        \u003C\u002Fbody>\n      \u003C\u002Fhtml>\n    );\n  }\n\n  return (\n    \u003Chtml>\n      \u003Cbody>{children}\u003C\u002Fbody>\n    \u003C\u002Fhtml>\n  );\n}",{"id":315,"data":480,"type":42},{"text":481,"level":227},"Step-by-step: admin login flow",{"id":319,"data":483,"type":240},{"items":484,"style":329},[485,486,487,488,489,490,491],"Browser submits credentials: POST \u002Fapi\u002Flogin","NGINX forwards \u002Fapi\u002F* to Fastify","Fastify validates user against DB (Prisma)","Fastify writes session into fakeRedis store (dev) and returns Set-Cookie: sid=…","Browser stores the cookie (HTTP-only)","Admin server components call GET \u002Fapi\u002Fme with the cookie attached","Admin renders dashboard only if \u002Fme returns ok=true",{"id":331,"data":493,"type":42},{"text":494,"level":227},"One thing that went wrong (so we don’t repeat it)",{"id":335,"data":496,"type":222},{"text":497},"We had a login loop even though \u002Fapi\u002Flogin returned 200. The cookie was never sent back on \u002Fapi\u002Fme. The cause was boring: cookie path + proxy routing mismatch. We set the cookie for \u002Fadmin by accident, but \u002Fapi\u002Fme lives under \u002Fapi. Browser did exactly what it should. It didn’t send the cookie. Fix was to set cookie path to \"\u002F\" and confirm NGINX wasn’t rewriting paths in a way that broke it.",{"id":339,"data":499,"type":42},{"text":500,"level":227},"Project plan (ship-first, refine later)",{"id":343,"data":502,"type":240},{"items":503,"style":329},[504,505,506,507,508,509],"Phase 0: monorepo wiring (Turbo, TS config, shared package imports). Keep it lean.","Phase 1: API auth\u002Fsession endpoints: \u002Flogin, \u002Flogout, \u002Fme. Add basic rate limiting on \u002Flogin.","Phase 2: Admin MVP: login screen + server-side guard + minimal dashboard shell.","Phase 3: Content read endpoints for public platform: \u002Fapi\u002Fcontent\u002F* (read-only first).","Phase 4: Content CRUD in admin + media upload (start simple, then swap storage later).","Phase 5: Monitoring and hardening: Prometheus metrics endpoint, Grafana dashboards, headers, backups.",{"id":353,"data":511,"type":42},{"text":512,"level":227},"Definition of done (so we don’t argue later)",{"id":357,"data":514,"type":240},{"items":515,"style":239},[516,517,518,519,520],"A fresh browser can log into \u002Fadmin and stay logged in across refreshes.","Public platform renders SSR content from \u002Fapi\u002Fcontent without exposing admin-only endpoints.","NGINX routes \u002F, \u002Fadmin, \u002Fapi correctly in docker-compose.","API can restart without corrupting session behavior (dev store resets are fine, but it must fail gracefully).","At least one Prometheus scrape target exists (even if Grafana panels are placeholders).",{"id":366,"data":522,"type":368},{},{"id":370,"data":524,"type":222},{"text":525},"If you’re implementing a new endpoint, decide first: is it public, admin-only, or internal. Don’t guess. Then put it behind the right prefix and guard it. That’s the whole game.","Explore a practical monorepo architecture using Next.js, Fastify, Prisma, and NGINX, highlighting real-world integration and workflow.",{"lang":7,"title":208,"content":210,"contentJson":528,"excerpt":374},{"time":212,"blocks":529,"version":373},[530,532,534,536,539,541,543,545,548,550,552,554,556,558,560,562,564,566,568,570,572,574,576,579,581,583,585,588,590,593,595],{"id":215,"data":531,"type":42},{"text":217,"level":40},{"id":219,"data":533,"type":222},{"text":221},{"id":224,"data":535,"type":42},{"text":226,"level":227},{"id":229,"data":537,"type":240},{"items":538,"style":239},[232,233,234,235,236,237,238],{"id":242,"data":540,"type":42},{"text":244,"level":227},{"id":246,"data":542,"type":249},{"code":248},{"id":251,"data":544,"type":42},{"text":253,"level":227},{"id":255,"data":546,"type":240},{"items":547,"style":239},[258,259,260,261],{"id":263,"data":549,"type":42},{"text":265,"level":227},{"id":267,"data":551,"type":222},{"text":269},{"id":271,"data":553,"type":249},{"code":273},{"id":275,"data":555,"type":42},{"text":277,"level":227},{"id":279,"data":557,"type":222},{"text":281},{"id":283,"data":559,"type":249},{"code":285},{"id":287,"data":561,"type":249},{"code":289},{"id":291,"data":563,"type":42},{"text":293,"level":227},{"id":295,"data":565,"type":222},{"text":297},{"id":299,"data":567,"type":249},{"code":301},{"id":303,"data":569,"type":42},{"text":305,"level":227},{"id":307,"data":571,"type":222},{"text":309},{"id":311,"data":573,"type":249},{"code":313},{"id":315,"data":575,"type":42},{"text":317,"level":227},{"id":319,"data":577,"type":240},{"items":578,"style":329},[322,323,324,325,326,327,328],{"id":331,"data":580,"type":42},{"text":333,"level":227},{"id":335,"data":582,"type":222},{"text":337},{"id":339,"data":584,"type":42},{"text":341,"level":227},{"id":343,"data":586,"type":240},{"items":587,"style":329},[346,347,348,349,350,351],{"id":353,"data":589,"type":42},{"text":355,"level":227},{"id":357,"data":591,"type":240},{"items":592,"style":239},[360,361,362,363,364],{"id":366,"data":594,"type":368},{},{"id":370,"data":596,"type":222},{"text":372},"Post erfolgreich abgerufen",{"items":599,"source":621,"manualIds":622,"manualMatchedIds":623},[600,607,614],{"id":601,"slug":602,"title":603,"excerpt":604,"featuredImage":605,"publishedAt":606},"381","enterprise-grade-multi-tenant-architecture-for-an-international-platform","Arquitectura Multi-Inquilino de Grado Empresarial para una Plataforma Internacional","Loving Rocks es una plataforma de bodas de nivel empresarial diseñada con una verdadera arquitectura multiinquilino, bases de datos aisladas por inquilino e internacionalización integrada para escalabilidad global, seguridad y estabilidad operativa a largo plazo.","\u002Fuploads\u002F2026\u002F01\u002Fenterprise-grade-multi-tenant-architecture-for-an-international-platform-1769789121298-b6v7ak.webp","2026-01-30T12:04:00.000Z",{"id":608,"slug":609,"title":610,"excerpt":611,"featuredImage":612,"publishedAt":613},"2","multi-database-architecture","Arquitectura multi-base de datos con Prisma 7: Un Deep Dive para expertos","La gestión de paisajes de datos complejos requiere arquitecturas modernas. Prisma 7 ofrece funciones avanzadas para la integración multi-base de datos y aborda los desafíos de la persistencia políglota.","\u002Fuploads\u002F2014\u002F09\u002FSEO-Mobile-Webapplikation-Muenchen-www.stajic.de_1.webp","2025-10-31T04:31:00.000Z",{"id":615,"slug":616,"title":617,"excerpt":618,"featuredImage":619,"publishedAt":620},"473","openai-agents-api-vs-agents-sdk-vs-responses-api-what-should-you-build-on-in-2026","OpenAI Agents API vs Agents SDK vs Responses API: ¿Sobre qué deberías construir en 2026?","El stack de agentes de OpenAI cambió en septiembre de 2026. Esta guía de arquitectura separa la Agents API, el Agents SDK, la Responses API y el Codex SDK por propiedad del runtime—para que los equipos puedan elegir el límite de control adecuado en lugar de comparar nombres de productos.","\u002Fuploads\u002F2026\u002F09\u002Fopenai-agents-api-vs-agents-sdk-vs-responses-api-what-should-you-build-on-in-2026-1790351846714-zi7lus.webp","2026-09-25T11:56:00.000Z","fallback",[],[]]