[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"portal-settings:stajic:fr":3,"public-menus:all":38,"post:a-practical-monorepo-architecture-next-js-platform-admin-fastify-api-prisma-and-nginx:fr":221,"related:post:a-practical-monorepo-architecture-next-js-platform-admin-fastify-api-prisma-and-nginx:fr:1":613},{"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","fr","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,191,206],{"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-globe-2","custom",[],{"id":176,"title":177,"url":186,"target":61,"icon":187,"isActive":14,"type":188,"productId":10,"categoryId":10,"shopCategoryId":10,"articleId":10,"pageId":10,"portfolioId":189,"children":190},"menu-2916d060-8681-482a-af9a-c107a8bc30d8",{"de":178,"en":179,"es":180,"fr":181,"it":182,"ru":183,"sr":184,"zh":185},"Von Rohdaten zu verifizierbarem KI-Kontext: Eine praktische Architektur für agentenbereite Informationen","From Raw Data to Verifiable AI Context:","De datos sin procesar a contexto de IA verificable: una arquitectura práctica para información lista para agentes","Des données brutes au contexte d'IA vérifiable : une architecture pratique pour des informations prêtes pour les agents","Dai dati grezzi a un contesto IA verificabile: Un'architettura pratica per informazioni pronte per gli agenti","От необработанных данных к верифицируемому контексту ИИ: практическая архитектура информации, готовой для агентов","Od sirovih podataka do proverljivog AI konteksta: Praktična arhitektura za informacije spremne za agente","从原始数据到可验证的 AI 上下文：面向智能体就绪信息的实用架构","\u002Fportfolio\u002Ffrom-raw-data-to-verifiable-ai-context-a-practical-architecture-for-agent-ready-information","i-lucide-database","portfolio",22,[],{"id":192,"title":193,"url":202,"target":61,"icon":203,"isActive":14,"type":188,"productId":10,"categoryId":10,"shopCategoryId":10,"articleId":10,"pageId":10,"portfolioId":204,"children":205},"menu-e2061cd7-ff53-49d5-a0e8-3d49090577c0",{"de":194,"en":195,"es":196,"fr":197,"it":198,"ru":199,"sr":200,"zh":201},"Präsenzplattformen — Unternehmenskommunikation","Presence Platforms — Enterprise Communication","Plataformas de presencia — Comunicación empresarial","Plateformes de présence — Communication d'entreprise","Presence Platforms — Comunicazione aziendale","Платформы присутствия — Корпоративные коммуникации","Presence platforme — Poslovna komunikacija","Presence 平台 — 企业通信","\u002Fportfolio\u002Fpresence-platforms-enterprise-communication-that-wins-without-competing","i-lucide-badge-percent",21,[],{"id":207,"title":208,"url":217,"target":61,"icon":218,"isActive":14,"type":188,"productId":10,"categoryId":10,"shopCategoryId":10,"articleId":10,"pageId":10,"portfolioId":219,"children":220},"menu-a781582e-e90d-418e-b8c9-b070bc37c37e",{"de":209,"en":210,"es":211,"fr":212,"it":213,"ru":214,"sr":215,"zh":216},"Maximierung des POS-ROI durch Konsistenz","Maximizing POS ROI Through Consistent","Maximizar el ROI del POS a través de constante","Maximiser le ROI du PDV grâce à la cohérence","Massimizzare il ROI del POS attraverso costante","Максимизация ROI POS за счет последовательного","Maksimizacija POS ROI-ja kroz dosledno","通过持续最大化 POS 投资回报率","\u002Fportfolio\u002Ffrom-vision-to-value-maximizing-pos-roi-through-consistent-ci-creation-and-strategic-branding","i-lucide-briefcase-business",11,[],{"statusCode":4,"data":222,"message":612},{"id":223,"title":224,"slug":225,"content":226,"contentJson":227,"excerpt":390,"featuredImage":391,"featuredImageAlt":392,"featuredImageCaption":10,"featuredImageTitle":10,"featuredImageCopyright":10,"featuredImageAuthor":10,"featuredImageSourceUrl":10,"featuredImageLicense":10,"featuredImageIsAiGenerated":43,"status":393,"publishedAt":394,"createdAt":395,"updatedAt":396,"seoLocalePaths":397,"categories":406,"author":414,"translations":419},"382","Une Architecture Monorepo Pratique avec Next.js, Fastify, Prisma, et NGINX","a-practical-monorepo-architecture-next-js-platform-admin-fastify-api-prisma-and-nginx","\u003Ch1>Monorepo de plateforme : Next.js (Public + Admin) + API Fastify + DB Prisma + NGINX\u003C\u002Fh1>\n\u003Cp>Ce document décrit l'architecture cible actuelle pour un monorepo comprenant un site public Next.js, une application d'administration Next.js avec sessions par cookies, une API Fastify et un package de base de données basé sur Prisma. Il est rédigé pour les coéquipiers qui travailleront sur plusieurs packages et ont besoin de limites prévisibles.\u003C\u002Fp>\n\u003Ch2>Contraintes non négociables (pour l'instant)\u003C\u002Fh2>\n\u003Cul>\u003Cli>Le site public doit être SSR (Next.js App Router). Pas de raccourcis uniquement SPA.\u003C\u002Fli>\u003Cli>Le site public communique avec le backend uniquement via des routes API publiques sous \u002Fapi (par exemple, \u002Fapi\u002Fcontent, \u002Fapi\u002Fmedia).\u003C\u002Fli>\u003Cli>L'administration utilise l'authentification par session basée sur les cookies. Cookie HTTP-only. Pas de jetons localStorage.\u003C\u002Fli>\u003Cli>L'API est Fastify. Sessions via @fastify\u002Fsession. Stockage « fakeRedis » pour le développement ; Redis en production plus tard.\u003C\u002Fli>\u003Cli>L'accès à la base de données se fait uniquement via @platform\u002Fdb. Pas de clients Prisma directs dispersés dans les applications.\u003C\u002Fli>\u003Cli>NGINX termine le TLS et route \u002F, \u002Fadmin et \u002Fapi. Un seul domaine est acceptable ; un sous-domaine est facultatif plus tard.\u003C\u002Fli>\u003Cli>La surveillance existe dès le premier jour (récupération Prometheus + espaces réservés pour les tableaux de bord Grafana).\u003C\u002Fli>\u003C\u002Ful>\n\u003Ch2>Structure du dépôt\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>Compromis (les vrais)\u003C\u002Fh2>\n\u003Cul>\u003Cli>Les sessions par cookies sont ennuyeuses. C'est une fonctionnalité. Elles fonctionnent derrière les proxys et maintiennent la logique d'authentification hors du frontend. L'inconvénient : vous devez configurer correctement SameSite\u002FSecure\u002Fpath, sinon vous courrez après des bugs de connexion « aléatoires ».\u003C\u002Fli>\u003Cli>Une seule surface \u002Fapi est propre, mais cela signifie aussi que vous devez être strict sur ce qui est public et ce qui est réservé à l'administration. Ne divulguez pas d'endpoints d'administration juste parce que c'est pratique.\u003C\u002Fli>\u003Cli>Prisma peut être « prêt pour plusieurs bases de données » dans le sens où il permet de changer de fournisseur. Ce n'est pas un pont magique entre SQL et Mongo avec un schéma unique. Si quelqu'un dit que c'est le cas, il ne l'a pas encore livré.\u003C\u002Fli>\u003Cli>Le SSR de Next.js est bon pour le SEO et le premier chargement. Il peut aussi surcharger l'API si vous ne configurez pas intentionnellement la mise en cache et la revalidation.\u003C\u002Fli>\u003C\u002Ful>\n\u003Ch2>Contrat de routage (NGINX comme porte d'entrée)\u003C\u002Fh2>\n\u003Cp>Nous routons tout via un seul domaine pour l'instant. C'est plus simple à déboguer. Si nous déplaçons l'administration vers un sous-domaine plus tard, nous réexaminerons la portée des cookies et les hypothèses 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>Package API (Fastify) — sessions et authentification\u003C\u002Fh2>\n\u003Cp>L'API gère l'authentification et l'état de la session. Les applications ne créent jamais de jetons. Elles renvoient simplement des cookies. Gardez la charge utile de la session petite. ID utilisateur + rôle. C'est tout.\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>Package DB (Prisma) — client unique, types partagés\u003C\u002Fh2>\n\u003Cp>Nous exposons un seul PrismaClient depuis @platform\u002Fdb. L'API importe cela et seulement cela. Si vous créez un deuxième client Prisma dans une application parce que c'est « plus rapide à prototyper », vous ne faites que créer un incident futur.\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>Application d'administration — session par cookie et garde \u002Fme\u003C\u002Fh2>\n\u003Cp>L'administration est une application Next.js App Router normale. La seule particularité est qu'elle doit considérer l'API comme la source de vérité et toujours inclure les cookies lors de l'appel à \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;Unauthorized. Go to \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>Étape par étape : flux de connexion de l'administrateur\u003C\u002Fh2>\n\u003Col>\u003Cli>Le navigateur soumet les identifiants : POST \u002Fapi\u002Flogin\u003C\u002Fli>\u003Cli>NGINX transmet \u002Fapi\u002F* à Fastify\u003C\u002Fli>\u003Cli>Fastify valide l'utilisateur par rapport à la base de données (Prisma)\u003C\u002Fli>\u003Cli>Fastify écrit la session dans le stockage fakeRedis (dev) et renvoie Set-Cookie: sid=…\u003C\u002Fli>\u003Cli>Le navigateur stocke le cookie (HTTP-only)\u003C\u002Fli>\u003Cli>Les composants serveur de l'administration appellent GET \u002Fapi\u002Fme avec le cookie attaché\u003C\u002Fli>\u003Cli>L'administration affiche le tableau de bord uniquement si \u002Fme renvoie ok=true\u003C\u002Fli>\u003C\u002Fol>\n\u003Ch2>Une chose qui a mal tourné (pour ne pas la répéter)\u003C\u002Fh2>\n\u003Cp>Nous avions une boucle de connexion même si \u002Fapi\u002Flogin renvoyait 200. Le cookie n'était jamais renvoyé sur \u002Fapi\u002Fme. La cause était ennuyeuse : une incompatibilité entre le chemin du cookie et le routage du proxy. Nous avions défini le cookie pour \u002Fadmin par accident, mais \u002Fapi\u002Fme se trouve sous \u002Fapi. Le navigateur a fait exactement ce qu'il devait. Il n'a pas envoyé le cookie. La solution a été de définir le chemin du cookie sur \"\u002F\" et de confirmer que NGINX ne réécrivait pas les chemins d'une manière qui le cassait.\u003C\u002Fp>\n\u003Ch2>Plan de projet (livrer d'abord, affiner ensuite)\u003C\u002Fh2>\n\u003Col>\u003Cli>Phase 0 : câblage du monorepo (Turbo, configuration TS, importations de packages partagés). Restez léger.\u003C\u002Fli>\u003Cli>Phase 1 : Endpoints d'authentification\u002Fsession de l'API : \u002Flogin, \u002Flogout, \u002Fme. Ajouter une limitation de débit de base sur \u002Flogin.\u003C\u002Fli>\u003Cli>Phase 2 : MVP Admin : écran de connexion + garde côté serveur + shell de tableau de bord minimal.\u003C\u002Fli>\u003Cli>Phase 3 : Endpoints de lecture de contenu pour la plateforme publique : \u002Fapi\u002Fcontent\u002F* (lecture seule d'abord).\u003C\u002Fli>\u003Cli>Phase 4 : CRUD de contenu dans l'administration + téléchargement de médias (commencer simple, puis changer de stockage plus tard).\u003C\u002Fli>\u003Cli>Phase 5 : Surveillance et renforcement : endpoint de métriques Prometheus, tableaux de bord Grafana, en-têtes, sauvegardes.\u003C\u002Fli>\u003C\u002Fol>\n\u003Ch2>Définition de « terminé » (pour éviter les discussions ultérieures)\u003C\u002Fh2>\n\u003Cul>\u003Cli>Un nouveau navigateur peut se connecter à \u002Fadmin et rester connecté après des actualisations.\u003C\u002Fli>\u003Cli>La plateforme publique affiche le contenu SSR depuis \u002Fapi\u002Fcontent sans exposer les endpoints réservés à l'administration.\u003C\u002Fli>\u003Cli>NGINX route \u002F, \u002Fadmin, \u002Fapi correctement dans docker-compose.\u003C\u002Fli>\u003Cli>L'API peut redémarrer sans corrompre le comportement de la session (les réinitialisations du stockage de développement sont acceptables, mais elle doit échouer gracieusement).\u003C\u002Fli>\u003Cli>Au moins une cible de récupération Prometheus existe (même si les panneaux Grafana sont des espaces réservés).\u003C\u002Fli>\u003C\u002Ful>\n\u003Chr class=\"my-8\">\n\u003Cp>Si vous implémentez un nouvel endpoint, décidez d'abord : est-il public, réservé à l'administration ou interne. Ne devinez pas. Ensuite, placez-le derrière le bon préfixe et protégez-le. C'est tout le jeu.\u003C\u002Fp>",{"time":228,"blocks":229,"version":389},1769827200000,[230,234,239,244,257,261,266,270,278,282,286,290,294,298,302,306,310,314,318,322,326,330,334,346,350,354,358,368,372,381,385],{"id":231,"data":232,"type":42},"h1_arch",{"text":233,"level":40},"Monorepo de plateforme : Next.js (Public + Admin) + API Fastify + DB Prisma + NGINX",{"id":235,"data":236,"type":238},"p_scope",{"text":237},"Ce document décrit l'architecture cible actuelle pour un monorepo comprenant un site public Next.js, une application d'administration Next.js avec sessions par cookies, une API Fastify et un package de base de données basé sur Prisma. Il est rédigé pour les coéquipiers qui travailleront sur plusieurs packages et ont besoin de limites prévisibles.","paragraph",{"id":240,"data":241,"type":42},"h2_constraints",{"text":242,"level":243},"Contraintes non négociables (pour l'instant)",2,{"id":245,"data":246,"type":256},"list_constraints",{"items":247,"style":255},[248,249,250,251,252,253,254],"Le site public doit être SSR (Next.js App Router). Pas de raccourcis uniquement SPA.","Le site public communique avec le backend uniquement via des routes API publiques sous \u002Fapi (par exemple, \u002Fapi\u002Fcontent, \u002Fapi\u002Fmedia).","L'administration utilise l'authentification par session basée sur les cookies. Cookie HTTP-only. Pas de jetons localStorage.","L'API est Fastify. Sessions via @fastify\u002Fsession. Stockage « fakeRedis » pour le développement ; Redis en production plus tard.","L'accès à la base de données se fait uniquement via @platform\u002Fdb. Pas de clients Prisma directs dispersés dans les applications.","NGINX termine le TLS et route \u002F, \u002Fadmin et \u002Fapi. Un seul domaine est acceptable ; un sous-domaine est facultatif plus tard.","La surveillance existe dès le premier jour (récupération Prometheus + espaces réservés pour les tableaux de bord Grafana).","unordered","list",{"id":258,"data":259,"type":42},"h2_layout",{"text":260,"level":243},"Structure du dépôt",{"id":262,"data":263,"type":265},"code_tree",{"code":264},"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":267,"data":268,"type":42},"h2_tradeoffs",{"text":269,"level":243},"Compromis (les vrais)",{"id":271,"data":272,"type":256},"list_tradeoffs",{"items":273,"style":255},[274,275,276,277],"Les sessions par cookies sont ennuyeuses. C'est une fonctionnalité. Elles fonctionnent derrière les proxys et maintiennent la logique d'authentification hors du frontend. L'inconvénient : vous devez configurer correctement SameSite\u002FSecure\u002Fpath, sinon vous courrez après des bugs de connexion « aléatoires ».","Une seule surface \u002Fapi est propre, mais cela signifie aussi que vous devez être strict sur ce qui est public et ce qui est réservé à l'administration. Ne divulguez pas d'endpoints d'administration juste parce que c'est pratique.","Prisma peut être « prêt pour plusieurs bases de données » dans le sens où il permet de changer de fournisseur. Ce n'est pas un pont magique entre SQL et Mongo avec un schéma unique. Si quelqu'un dit que c'est le cas, il ne l'a pas encore livré.","Le SSR de Next.js est bon pour le SEO et le premier chargement. Il peut aussi surcharger l'API si vous ne configurez pas intentionnellement la mise en cache et la revalidation.",{"id":279,"data":280,"type":42},"h2_routing",{"text":281,"level":243},"Contrat de routage (NGINX comme porte d'entrée)",{"id":283,"data":284,"type":238},"p_routing",{"text":285},"Nous routons tout via un seul domaine pour l'instant. C'est plus simple à déboguer. Si nous déplaçons l'administration vers un sous-domaine plus tard, nous réexaminerons la portée des cookies et les hypothèses CSRF.",{"id":287,"data":288,"type":265},"code_nginx",{"code":289},"# 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":291,"data":292,"type":42},"h2_api",{"text":293,"level":243},"Package API (Fastify) — sessions et authentification",{"id":295,"data":296,"type":238},"p_api",{"text":297},"L'API gère l'authentification et l'état de la session. Les applications ne créent jamais de jetons. Elles renvoient simplement des cookies. Gardez la charge utile de la session petite. ID utilisateur + rôle. C'est tout.",{"id":299,"data":300,"type":265},"code_server",{"code":301},"\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":303,"data":304,"type":265},"code_session_store",{"code":305},"\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":307,"data":308,"type":42},"h2_db",{"text":309,"level":243},"Package DB (Prisma) — client unique, types partagés",{"id":311,"data":312,"type":238},"p_db",{"text":313},"Nous exposons un seul PrismaClient depuis @platform\u002Fdb. L'API importe cela et seulement cela. Si vous créez un deuxième client Prisma dans une application parce que c'est « plus rapide à prototyper », vous ne faites que créer un incident futur.",{"id":315,"data":316,"type":265},"code_db",{"code":317},"\u002F\u002F packages\u002Fdb\u002Fsrc\u002Findex.ts\nimport { PrismaClient } from \"@prisma\u002Fclient\";\n\nexport const db = new PrismaClient();",{"id":319,"data":320,"type":42},"h2_admin",{"text":321,"level":243},"Application d'administration — session par cookie et garde \u002Fme",{"id":323,"data":324,"type":238},"p_admin",{"text":325},"L'administration est une application Next.js App Router normale. La seule particularité est qu'elle doit considérer l'API comme la source de vérité et toujours inclure les cookies lors de l'appel à \u002Fapi\u002Fme.",{"id":327,"data":328,"type":265},"code_admin_guard",{"code":329},"\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":331,"data":332,"type":42},"h2_flow",{"text":333,"level":243},"Étape par étape : flux de connexion de l'administrateur",{"id":335,"data":336,"type":256},"list_flow",{"items":337,"style":345},[338,339,340,341,342,343,344],"Le navigateur soumet les identifiants : POST \u002Fapi\u002Flogin","NGINX transmet \u002Fapi\u002F* à Fastify","Fastify valide l'utilisateur par rapport à la base de données (Prisma)","Fastify écrit la session dans le stockage fakeRedis (dev) et renvoie Set-Cookie: sid=…","Le navigateur stocke le cookie (HTTP-only)","Les composants serveur de l'administration appellent GET \u002Fapi\u002Fme avec le cookie attaché","L'administration affiche le tableau de bord uniquement si \u002Fme renvoie ok=true","ordered",{"id":347,"data":348,"type":42},"h2_went_wrong",{"text":349,"level":243},"Une chose qui a mal tourné (pour ne pas la répéter)",{"id":351,"data":352,"type":238},"p_went_wrong",{"text":353},"Nous avions une boucle de connexion même si \u002Fapi\u002Flogin renvoyait 200. Le cookie n'était jamais renvoyé sur \u002Fapi\u002Fme. La cause était ennuyeuse : une incompatibilité entre le chemin du cookie et le routage du proxy. Nous avions défini le cookie pour \u002Fadmin par accident, mais \u002Fapi\u002Fme se trouve sous \u002Fapi. Le navigateur a fait exactement ce qu'il devait. Il n'a pas envoyé le cookie. La solution a été de définir le chemin du cookie sur \"\u002F\" et de confirmer que NGINX ne réécrivait pas les chemins d'une manière qui le cassait.",{"id":355,"data":356,"type":42},"h2_project_plan",{"text":357,"level":243},"Plan de projet (livrer d'abord, affiner ensuite)",{"id":359,"data":360,"type":256},"list_plan",{"items":361,"style":345},[362,363,364,365,366,367],"Phase 0 : câblage du monorepo (Turbo, configuration TS, importations de packages partagés). Restez léger.","Phase 1 : Endpoints d'authentification\u002Fsession de l'API : \u002Flogin, \u002Flogout, \u002Fme. Ajouter une limitation de débit de base sur \u002Flogin.","Phase 2 : MVP Admin : écran de connexion + garde côté serveur + shell de tableau de bord minimal.","Phase 3 : Endpoints de lecture de contenu pour la plateforme publique : \u002Fapi\u002Fcontent\u002F* (lecture seule d'abord).","Phase 4 : CRUD de contenu dans l'administration + téléchargement de médias (commencer simple, puis changer de stockage plus tard).","Phase 5 : Surveillance et renforcement : endpoint de métriques Prometheus, tableaux de bord Grafana, en-têtes, sauvegardes.",{"id":369,"data":370,"type":42},"h2_definition_done",{"text":371,"level":243},"Définition de « terminé » (pour éviter les discussions ultérieures)",{"id":373,"data":374,"type":256},"list_dod",{"items":375,"style":255},[376,377,378,379,380],"Un nouveau navigateur peut se connecter à \u002Fadmin et rester connecté après des actualisations.","La plateforme publique affiche le contenu SSR depuis \u002Fapi\u002Fcontent sans exposer les endpoints réservés à l'administration.","NGINX route \u002F, \u002Fadmin, \u002Fapi correctement dans docker-compose.","L'API peut redémarrer sans corrompre le comportement de la session (les réinitialisations du stockage de développement sont acceptables, mais elle doit échouer gracieusement).","Au moins une cible de récupération Prometheus existe (même si les panneaux Grafana sont des espaces réservés).",{"id":382,"data":383,"type":384},"hr_end",{},"delimiter",{"id":386,"data":387,"type":238},"p_next",{"text":388},"Si vous implémentez un nouvel endpoint, décidez d'abord : est-il public, réservé à l'administration ou interne. Ne devinez pas. Ensuite, placez-le derrière le bon préfixe et protégez-le. C'est tout le jeu.","2.29.1","Explorez une architecture de monorepo pratique utilisant Next.js, Fastify, Prisma et NGINX, mettant en évidence l'intégration et le flux de travail concrets.","\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":398,"de":399,"sr":400,"es":401,"fr":402,"it":403,"ru":404,"zh":405},"\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",[407,410],{"id":46,"name":408,"slug":409},"Aaasaasa","aaasaasa",{"id":411,"name":412,"slug":413},48,"Capacités","capabilities",{"id":415,"login":416,"email":417,"displayName":418},"20","rooth8233","","Aleksandar Stajić",[420,542],{"lang":421,"title":422,"content":423,"contentJson":424,"excerpt":541},"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":228,"blocks":425,"version":389},[426,429,432,435,445,448,450,453,460,463,466,468,471,474,476,478,481,484,486,489,492,494,497,507,510,513,516,525,528,536,538],{"id":231,"data":427,"type":42},{"text":428,"level":40},"Platform Monorepo: Next.js (Public + Admin) + Fastify API + Prisma DB + NGINX",{"id":235,"data":430,"type":238},{"text":431},"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":240,"data":433,"type":42},{"text":434,"level":243},"Constraints we’re not negotiating (for now)",{"id":245,"data":436,"type":256},{"items":437,"style":255},[438,439,440,441,442,443,444],"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":258,"data":446,"type":42},{"text":447,"level":243},"Repository layout",{"id":262,"data":449,"type":265},{"code":264},{"id":267,"data":451,"type":42},{"text":452,"level":243},"Trade-offs (the real ones)",{"id":271,"data":454,"type":256},{"items":455,"style":255},[456,457,458,459],"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":279,"data":461,"type":42},{"text":462,"level":243},"Routing contract (NGINX as the front door)",{"id":283,"data":464,"type":238},{"text":465},"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":287,"data":467,"type":265},{"code":289},{"id":291,"data":469,"type":42},{"text":470,"level":243},"API package (Fastify) — sessions and auth",{"id":295,"data":472,"type":238},{"text":473},"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":299,"data":475,"type":265},{"code":301},{"id":303,"data":477,"type":265},{"code":305},{"id":307,"data":479,"type":42},{"text":480,"level":243},"DB package (Prisma) — single client, shared types",{"id":311,"data":482,"type":238},{"text":483},"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":315,"data":485,"type":265},{"code":317},{"id":319,"data":487,"type":42},{"text":488,"level":243},"Admin app — cookie session and \u002Fme guard",{"id":323,"data":490,"type":238},{"text":491},"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":327,"data":493,"type":265},{"code":329},{"id":331,"data":495,"type":42},{"text":496,"level":243},"Step-by-step: admin login flow",{"id":335,"data":498,"type":256},{"items":499,"style":345},[500,501,502,503,504,505,506],"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":347,"data":508,"type":42},{"text":509,"level":243},"One thing that went wrong (so we don’t repeat it)",{"id":351,"data":511,"type":238},{"text":512},"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":355,"data":514,"type":42},{"text":515,"level":243},"Project plan (ship-first, refine later)",{"id":359,"data":517,"type":256},{"items":518,"style":345},[519,520,521,522,523,524],"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":369,"data":526,"type":42},{"text":527,"level":243},"Definition of done (so we don’t argue later)",{"id":373,"data":529,"type":256},{"items":530,"style":255},[531,532,533,534,535],"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":382,"data":537,"type":384},{},{"id":386,"data":539,"type":238},{"text":540},"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":224,"content":226,"contentJson":543,"excerpt":390},{"time":228,"blocks":544,"version":389},[545,547,549,551,554,556,558,560,563,565,567,569,571,573,575,577,579,581,583,585,587,589,591,594,596,598,600,603,605,608,610],{"id":231,"data":546,"type":42},{"text":233,"level":40},{"id":235,"data":548,"type":238},{"text":237},{"id":240,"data":550,"type":42},{"text":242,"level":243},{"id":245,"data":552,"type":256},{"items":553,"style":255},[248,249,250,251,252,253,254],{"id":258,"data":555,"type":42},{"text":260,"level":243},{"id":262,"data":557,"type":265},{"code":264},{"id":267,"data":559,"type":42},{"text":269,"level":243},{"id":271,"data":561,"type":256},{"items":562,"style":255},[274,275,276,277],{"id":279,"data":564,"type":42},{"text":281,"level":243},{"id":283,"data":566,"type":238},{"text":285},{"id":287,"data":568,"type":265},{"code":289},{"id":291,"data":570,"type":42},{"text":293,"level":243},{"id":295,"data":572,"type":238},{"text":297},{"id":299,"data":574,"type":265},{"code":301},{"id":303,"data":576,"type":265},{"code":305},{"id":307,"data":578,"type":42},{"text":309,"level":243},{"id":311,"data":580,"type":238},{"text":313},{"id":315,"data":582,"type":265},{"code":317},{"id":319,"data":584,"type":42},{"text":321,"level":243},{"id":323,"data":586,"type":238},{"text":325},{"id":327,"data":588,"type":265},{"code":329},{"id":331,"data":590,"type":42},{"text":333,"level":243},{"id":335,"data":592,"type":256},{"items":593,"style":345},[338,339,340,341,342,343,344],{"id":347,"data":595,"type":42},{"text":349,"level":243},{"id":351,"data":597,"type":238},{"text":353},{"id":355,"data":599,"type":42},{"text":357,"level":243},{"id":359,"data":601,"type":256},{"items":602,"style":345},[362,363,364,365,366,367],{"id":369,"data":604,"type":42},{"text":371,"level":243},{"id":373,"data":606,"type":256},{"items":607,"style":255},[376,377,378,379,380],{"id":382,"data":609,"type":384},{},{"id":386,"data":611,"type":238},{"text":388},"Post erfolgreich abgerufen",{"items":614,"source":643,"manualIds":644,"manualMatchedIds":645},[615,622,629,636],{"id":616,"slug":617,"title":618,"excerpt":619,"featuredImage":620,"publishedAt":621},"492","mcp-explained-what-it-connects-what-it-does-not-do-and-where-it-fits","MCP expliqué : ce qu'il connecte, ce qu'il ne fait pas et où il s'intègre","Le protocole de contexte de modèle connecte les applications d’IA à des outils, des ressources et des invites externes par le biais d’une frontière standard client-serveur. Découvrez ce que fait le MCP, ce qu’il ne fait pas et où il s’intègre dans l’architecture des agents.","\u002Fuploads\u002F2026\u002F10\u002Fmcp-explained-what-it-connects-what-it-does-not-do-and-where-it-fits-1791486640275-7ub1cq.webp","2026-10-08T15:09:00.000Z",{"id":623,"slug":624,"title":625,"excerpt":626,"featuredImage":627,"publishedAt":628},"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 : sur quoi devriez-vous construire en 2026 ?","La stack d'agents d'OpenAI a changé en septembre 2026. Ce guide d'architecture sépare l'API Agents, le SDK Agents, l'API Responses et le SDK Codex selon la propriété du runtime — afin que les équipes puissent choisir la bonne limite de contrôle au lieu de comparer des noms de produits.","\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",{"id":630,"slug":631,"title":632,"excerpt":633,"featuredImage":634,"publishedAt":635},"2","multi-database-architecture","Architecture multi-bases de données avec Prisma 7 : Un Deep Dive pour experts","La gestion de paysages de données complexes nécessite des architectures modernes. Prisma 7 offre des fonctionnalités avancées pour l'intégration multi-bases de données et adresse les défis de la persistance polyglotte.","\u002Fuploads\u002F2014\u002F09\u002FSEO-Mobile-Webapplikation-Muenchen-www.stajic.de_1.webp","2025-10-31T04:31:00.000Z",{"id":637,"slug":638,"title":639,"excerpt":640,"featuredImage":641,"publishedAt":642},"381","enterprise-grade-multi-tenant-architecture-for-an-international-platform","Enterprise-Grade Multi-Tenant Architecture for an International Platform","Loving Rocks is an enterprise-grade wedding platform designed with a true multi-tenant architecture, isolated databases per tenant, and built-in internationalization for global scalability, security, and long-term operational stability.","\u002Fuploads\u002F2026\u002F01\u002Fenterprise-grade-multi-tenant-architecture-for-an-international-platform-1769789121298-b6v7ak.webp","2026-01-30T12:04:00.000Z","fallback",[],[]]