[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"portal-settings:stajic:it":3,"public-menus:all":38,"post:a-practical-monorepo-architecture-next-js-platform-admin-fastify-api-prisma-and-nginx:it":205,"related:post:a-practical-monorepo-architecture-next-js-platform-admin-fastify-api-prisma-and-nginx:it:1":597},{"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","it","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":596},{"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","Un'Architettura Monorepo Pratica con Next.js, Fastify, Prisma e NGINX","a-practical-monorepo-architecture-next-js-platform-admin-fastify-api-prisma-and-nginx","\u003Ch1>Monorepo della Piattaforma: Next.js (Pubblico + Admin) + API Fastify + DB Prisma + NGINX\u003C\u002Fh1>\n\u003Cp>Questo documento descrive l'architettura target attuale per un monorepo con un sito pubblico Next.js, un'app Next.js di amministrazione con sessioni basate su cookie, un'API Fastify e un pacchetto DB basato su Prisma. È scritto per i membri del team che lavoreranno su più pacchetti e necessitano di confini prevedibili.\u003C\u002Fp>\n\u003Ch2>Vincoli che non stiamo negoziando (per ora)\u003C\u002Fh2>\n\u003Cul>\u003Cli>Il sito pubblico deve essere SSR (Next.js App Router). Nessuna scorciatoia solo SPA.\u003C\u002Fli>\u003Cli>Il sito pubblico comunica con il backend solo tramite route API pubbliche sotto \u002Fapi (es. \u002Fapi\u002Fcontent, \u002Fapi\u002Fmedia).\u003C\u002Fli>\u003Cli>L'amministrazione utilizza l'autenticazione di sessione basata su cookie. Cookie HTTP-only. Nessun token in localStorage.\u003C\u002Fli>\u003Cli>L'API è Fastify. Sessioni tramite @fastify\u002Fsession. Store “fakeRedis” per lo sviluppo; Redis in produzione in seguito.\u003C\u002Fli>\u003Cli>L'accesso al DB avviene solo tramite @platform\u002Fdb. Nessun client Prisma diretto sparso nelle app.\u003C\u002Fli>\u003Cli>NGINX termina TLS e instrada \u002F, \u002Fadmin e \u002Fapi. Un unico dominio va bene; il sottodominio è opzionale in seguito.\u003C\u002Fli>\u003Cli>Il monitoraggio esiste dal primo giorno (scrape Prometheus + segnaposto per dashboard Grafana).\u003C\u002Fli>\u003C\u002Ful>\n\u003Ch2>Layout del repository\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>Compromessi (quelli veri)\u003C\u002Fh2>\n\u003Cul>\u003Cli>Le sessioni basate su cookie sono noiose. Questa è una caratteristica. Funzionano dietro i proxy e mantengono la logica di autenticazione fuori dal frontend. Lo svantaggio: devi impostare correttamente SameSite\u002FSecure\u002Fpath, altrimenti inseguirai bug di accesso “casuali”.\u003C\u002Fli>\u003Cli>Un'unica superficie \u002Fapi è pulita, ma significa anche che devi essere rigoroso su ciò che è pubblico rispetto a ciò che è solo per l'amministrazione. Non far trapelare endpoint di amministrazione solo perché è comodo.\u003C\u002Fli>\u003Cli>Prisma può essere “pronto per più DB” nel senso di scambiare i provider. Non è un ponte magico tra SQL e Mongo con un singolo schema. Se qualcuno dice che lo è, non l'ha ancora implementato.\u003C\u002Fli>\u003Cli>Next.js SSR è ottimo per la SEO e il primo caricamento. Può anche sovraccaricare l'API se non imposti intenzionalmente la cache e la riconvalida.\u003C\u002Fli>\u003C\u002Ful>\n\u003Ch2>Contratto di routing (NGINX come porta d'ingresso)\u003C\u002Fh2>\n\u003Cp>Per ora instradamo tutto tramite un unico dominio. È più semplice da debuggare. Se in seguito sposteremo l'amministrazione su un sottodominio, rivedremo l'ambito dei cookie e le ipotesi 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>Pacchetto API (Fastify) — sessioni e autenticazione\u003C\u002Fh2>\n\u003Cp>L'API gestisce l'autenticazione e lo stato della sessione. Le app non creano mai token. Si limitano a inviare i cookie. Mantieni il payload della sessione piccolo. ID utente + ruolo. Tutto qui.\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>Pacchetto DB (Prisma) — client singolo, tipi condivisi\u003C\u002Fh2>\n\u003Cp>Esporremo un unico PrismaClient da @platform\u002Fdb. L'API importa quello e solo quello. Se crei un secondo client Prisma in un'app perché è “più veloce da prototipare”, stai solo creando un futuro incidente.\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>App di amministrazione — sessione cookie e guardia \u002Fme\u003C\u002Fh2>\n\u003Cp>L'amministrazione è una normale app Next.js App Router. L'unica parte speciale è che deve trattare l'API come fonte di verità e includere sempre i cookie quando chiama \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>Passo dopo passo: flusso di accesso dell'amministratore\u003C\u002Fh2>\n\u003Col>\u003Cli>Il browser invia le credenziali: POST \u002Fapi\u002Flogin\u003C\u002Fli>\u003Cli>NGINX inoltra \u002Fapi\u002F* a Fastify\u003C\u002Fli>\u003Cli>Fastify convalida l'utente rispetto al DB (Prisma)\u003C\u002Fli>\u003Cli>Fastify scrive la sessione nello store fakeRedis (dev) e restituisce Set-Cookie: sid=…\u003C\u002Fli>\u003Cli>Il browser memorizza il cookie (HTTP-only)\u003C\u002Fli>\u003Cli>I componenti del server di amministrazione chiamano GET \u002Fapi\u002Fme con il cookie allegato\u003C\u002Fli>\u003Cli>L'amministrazione renderizza la dashboard solo se \u002Fme restituisce ok=true\u003C\u002Fli>\u003C\u002Fol>\n\u003Ch2>Una cosa che è andata storta (così non la ripetiamo)\u003C\u002Fh2>\n\u003Cp>Avevamo un loop di accesso anche se \u002Fapi\u002Flogin restituiva 200. Il cookie non veniva mai inviato di nuovo su \u002Fapi\u002Fme. La causa era banale: mancata corrispondenza tra il percorso del cookie e il routing del proxy. Avevamo impostato il cookie per \u002Fadmin per errore, ma \u002Fapi\u002Fme si trova sotto \u002Fapi. Il browser ha fatto esattamente quello che doveva. Non ha inviato il cookie. La soluzione è stata impostare il percorso del cookie su \"\u002F\" e confermare che NGINX non stesse riscrivendo i percorsi in modo da romperlo.\u003C\u002Fp>\n\u003Ch2>Piano di progetto (lanciare prima, perfezionare dopo)\u003C\u002Fh2>\n\u003Col>\u003Cli>Fase 0: cablaggio del monorepo (Turbo, configurazione TS, importazioni di pacchetti condivisi). Mantienilo snello.\u003C\u002Fli>\u003Cli>Fase 1: Endpoint API di autenticazione\u002Fsessione: \u002Flogin, \u002Flogout, \u002Fme. Aggiungi un rate limiting di base su \u002Flogin.\u003C\u002Fli>\u003Cli>Fase 2: MVP Admin: schermata di accesso + guardia lato server + shell minima della dashboard.\u003C\u002Fli>\u003Cli>Fase 3: Endpoint di lettura dei contenuti per la piattaforma pubblica: \u002Fapi\u002Fcontent\u002F* (solo lettura inizialmente).\u003C\u002Fli>\u003Cli>Fase 4: CRUD dei contenuti nell'amministrazione + caricamento media (inizia semplice, poi cambia lo storage in seguito).\u003C\u002Fli>\u003Cli>Fase 5: Monitoraggio e rafforzamento: endpoint metriche Prometheus, dashboard Grafana, header, backup.\u003C\u002Fli>\u003C\u002Fol>\n\u003Ch2>Definizione di completato (così non discuteremo dopo)\u003C\u002Fh2>\n\u003Cul>\u003Cli>Un browser nuovo può accedere a \u002Fadmin e rimanere loggato tra i refresh.\u003C\u002Fli>\u003Cli>La piattaforma pubblica renderizza contenuti SSR da \u002Fapi\u002Fcontent senza esporre endpoint solo per l'amministrazione.\u003C\u002Fli>\u003Cli>NGINX instrada \u002F, \u002Fadmin, \u002Fapi correttamente in docker-compose.\u003C\u002Fli>\u003Cli>L'API può riavviarsi senza corrompere il comportamento della sessione (i reset dello store di sviluppo vanno bene, ma deve fallire in modo elegante).\u003C\u002Fli>\u003Cli>Esiste almeno un target di scrape Prometheus (anche se i pannelli Grafana sono segnaposto).\u003C\u002Fli>\u003C\u002Ful>\n\u003Chr class=\"my-8\">\n\u003Cp>Se stai implementando un nuovo endpoint, decidi prima: è pubblico, solo per l'amministrazione o interno. Non indovinare. Poi mettilo dietro il prefisso giusto e proteggilo. Questo è tutto il gioco.\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 della Piattaforma: Next.js (Pubblico + Admin) + API Fastify + DB Prisma + NGINX",{"id":219,"data":220,"type":222},"p_scope",{"text":221},"Questo documento descrive l'architettura target attuale per un monorepo con un sito pubblico Next.js, un'app Next.js di amministrazione con sessioni basate su cookie, un'API Fastify e un pacchetto DB basato su Prisma. È scritto per i membri del team che lavoreranno su più pacchetti e necessitano di confini prevedibili.","paragraph",{"id":224,"data":225,"type":42},"h2_constraints",{"text":226,"level":227},"Vincoli che non stiamo negoziando (per ora)",2,{"id":229,"data":230,"type":240},"list_constraints",{"items":231,"style":239},[232,233,234,235,236,237,238],"Il sito pubblico deve essere SSR (Next.js App Router). Nessuna scorciatoia solo SPA.","Il sito pubblico comunica con il backend solo tramite route API pubbliche sotto \u002Fapi (es. \u002Fapi\u002Fcontent, \u002Fapi\u002Fmedia).","L'amministrazione utilizza l'autenticazione di sessione basata su cookie. Cookie HTTP-only. Nessun token in localStorage.","L'API è Fastify. Sessioni tramite @fastify\u002Fsession. Store “fakeRedis” per lo sviluppo; Redis in produzione in seguito.","L'accesso al DB avviene solo tramite @platform\u002Fdb. Nessun client Prisma diretto sparso nelle app.","NGINX termina TLS e instrada \u002F, \u002Fadmin e \u002Fapi. Un unico dominio va bene; il sottodominio è opzionale in seguito.","Il monitoraggio esiste dal primo giorno (scrape Prometheus + segnaposto per dashboard Grafana).","unordered","list",{"id":242,"data":243,"type":42},"h2_layout",{"text":244,"level":227},"Layout del repository",{"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},"Compromessi (quelli veri)",{"id":255,"data":256,"type":240},"list_tradeoffs",{"items":257,"style":239},[258,259,260,261],"Le sessioni basate su cookie sono noiose. Questa è una caratteristica. Funzionano dietro i proxy e mantengono la logica di autenticazione fuori dal frontend. Lo svantaggio: devi impostare correttamente SameSite\u002FSecure\u002Fpath, altrimenti inseguirai bug di accesso “casuali”.","Un'unica superficie \u002Fapi è pulita, ma significa anche che devi essere rigoroso su ciò che è pubblico rispetto a ciò che è solo per l'amministrazione. Non far trapelare endpoint di amministrazione solo perché è comodo.","Prisma può essere “pronto per più DB” nel senso di scambiare i provider. Non è un ponte magico tra SQL e Mongo con un singolo schema. Se qualcuno dice che lo è, non l'ha ancora implementato.","Next.js SSR è ottimo per la SEO e il primo caricamento. Può anche sovraccaricare l'API se non imposti intenzionalmente la cache e la riconvalida.",{"id":263,"data":264,"type":42},"h2_routing",{"text":265,"level":227},"Contratto di routing (NGINX come porta d'ingresso)",{"id":267,"data":268,"type":222},"p_routing",{"text":269},"Per ora instradamo tutto tramite un unico dominio. È più semplice da debuggare. Se in seguito sposteremo l'amministrazione su un sottodominio, rivedremo l'ambito dei cookie e le ipotesi 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},"Pacchetto API (Fastify) — sessioni e autenticazione",{"id":279,"data":280,"type":222},"p_api",{"text":281},"L'API gestisce l'autenticazione e lo stato della sessione. Le app non creano mai token. Si limitano a inviare i cookie. Mantieni il payload della sessione piccolo. ID utente + ruolo. Tutto qui.",{"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},"Pacchetto DB (Prisma) — client singolo, tipi condivisi",{"id":295,"data":296,"type":222},"p_db",{"text":297},"Esporremo un unico PrismaClient da @platform\u002Fdb. L'API importa quello e solo quello. Se crei un secondo client Prisma in un'app perché è “più veloce da prototipare”, stai solo creando un futuro incidente.",{"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},"App di amministrazione — sessione cookie e guardia \u002Fme",{"id":307,"data":308,"type":222},"p_admin",{"text":309},"L'amministrazione è una normale app Next.js App Router. L'unica parte speciale è che deve trattare l'API come fonte di verità e includere sempre i cookie quando chiama \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>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":316,"type":42},"h2_flow",{"text":317,"level":227},"Passo dopo passo: flusso di accesso dell'amministratore",{"id":319,"data":320,"type":240},"list_flow",{"items":321,"style":329},[322,323,324,325,326,327,328],"Il browser invia le credenziali: POST \u002Fapi\u002Flogin","NGINX inoltra \u002Fapi\u002F* a Fastify","Fastify convalida l'utente rispetto al DB (Prisma)","Fastify scrive la sessione nello store fakeRedis (dev) e restituisce Set-Cookie: sid=…","Il browser memorizza il cookie (HTTP-only)","I componenti del server di amministrazione chiamano GET \u002Fapi\u002Fme con il cookie allegato","L'amministrazione renderizza la dashboard solo se \u002Fme restituisce ok=true","ordered",{"id":331,"data":332,"type":42},"h2_went_wrong",{"text":333,"level":227},"Una cosa che è andata storta (così non la ripetiamo)",{"id":335,"data":336,"type":222},"p_went_wrong",{"text":337},"Avevamo un loop di accesso anche se \u002Fapi\u002Flogin restituiva 200. Il cookie non veniva mai inviato di nuovo su \u002Fapi\u002Fme. La causa era banale: mancata corrispondenza tra il percorso del cookie e il routing del proxy. Avevamo impostato il cookie per \u002Fadmin per errore, ma \u002Fapi\u002Fme si trova sotto \u002Fapi. Il browser ha fatto esattamente quello che doveva. Non ha inviato il cookie. La soluzione è stata impostare il percorso del cookie su \"\u002F\" e confermare che NGINX non stesse riscrivendo i percorsi in modo da romperlo.",{"id":339,"data":340,"type":42},"h2_project_plan",{"text":341,"level":227},"Piano di progetto (lanciare prima, perfezionare dopo)",{"id":343,"data":344,"type":240},"list_plan",{"items":345,"style":329},[346,347,348,349,350,351],"Fase 0: cablaggio del monorepo (Turbo, configurazione TS, importazioni di pacchetti condivisi). Mantienilo snello.","Fase 1: Endpoint API di autenticazione\u002Fsessione: \u002Flogin, \u002Flogout, \u002Fme. Aggiungi un rate limiting di base su \u002Flogin.","Fase 2: MVP Admin: schermata di accesso + guardia lato server + shell minima della dashboard.","Fase 3: Endpoint di lettura dei contenuti per la piattaforma pubblica: \u002Fapi\u002Fcontent\u002F* (solo lettura inizialmente).","Fase 4: CRUD dei contenuti nell'amministrazione + caricamento media (inizia semplice, poi cambia lo storage in seguito).","Fase 5: Monitoraggio e rafforzamento: endpoint metriche Prometheus, dashboard Grafana, header, backup.",{"id":353,"data":354,"type":42},"h2_definition_done",{"text":355,"level":227},"Definizione di completato (così non discuteremo dopo)",{"id":357,"data":358,"type":240},"list_dod",{"items":359,"style":239},[360,361,362,363,364],"Un browser nuovo può accedere a \u002Fadmin e rimanere loggato tra i refresh.","La piattaforma pubblica renderizza contenuti SSR da \u002Fapi\u002Fcontent senza esporre endpoint solo per l'amministrazione.","NGINX instrada \u002F, \u002Fadmin, \u002Fapi correttamente in docker-compose.","L'API può riavviarsi senza corrompere il comportamento della sessione (i reset dello store di sviluppo vanno bene, ma deve fallire in modo elegante).","Esiste almeno un target di scrape Prometheus (anche se i pannelli Grafana sono segnaposto).",{"id":366,"data":367,"type":368},"hr_end",{},"delimiter",{"id":370,"data":371,"type":222},"p_next",{"text":372},"Se stai implementando un nuovo endpoint, decidi prima: è pubblico, solo per l'amministrazione o interno. Non indovinare. Poi mettilo dietro il prefisso giusto e proteggilo. Questo è tutto il gioco.","2.29.1","Esplora un'architettura monorepo pratica che utilizza Next.js, Fastify, Prisma e NGINX, evidenziando l'integrazione e il flusso di lavoro nel mondo reale.","\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,"Capacità","capabilities",{"id":399,"login":400,"email":401,"displayName":402},"20","rooth8233","aleksandar@stajic.de","Aleksandar Stajić",[404,526],{"lang":405,"title":406,"content":407,"contentJson":408,"excerpt":525},"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,478,481,491,494,497,500,509,512,520,522],{"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":313},{"id":315,"data":479,"type":42},{"text":480,"level":227},"Step-by-step: admin login flow",{"id":319,"data":482,"type":240},{"items":483,"style":329},[484,485,486,487,488,489,490],"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":492,"type":42},{"text":493,"level":227},"One thing that went wrong (so we don’t repeat it)",{"id":335,"data":495,"type":222},{"text":496},"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":498,"type":42},{"text":499,"level":227},"Project plan (ship-first, refine later)",{"id":343,"data":501,"type":240},{"items":502,"style":329},[503,504,505,506,507,508],"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":510,"type":42},{"text":511,"level":227},"Definition of done (so we don’t argue later)",{"id":357,"data":513,"type":240},{"items":514,"style":239},[515,516,517,518,519],"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":521,"type":368},{},{"id":370,"data":523,"type":222},{"text":524},"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":527,"excerpt":374},{"time":212,"blocks":528,"version":373},[529,531,533,535,538,540,542,544,547,549,551,553,555,557,559,561,563,565,567,569,571,573,575,578,580,582,584,587,589,592,594],{"id":215,"data":530,"type":42},{"text":217,"level":40},{"id":219,"data":532,"type":222},{"text":221},{"id":224,"data":534,"type":42},{"text":226,"level":227},{"id":229,"data":536,"type":240},{"items":537,"style":239},[232,233,234,235,236,237,238],{"id":242,"data":539,"type":42},{"text":244,"level":227},{"id":246,"data":541,"type":249},{"code":248},{"id":251,"data":543,"type":42},{"text":253,"level":227},{"id":255,"data":545,"type":240},{"items":546,"style":239},[258,259,260,261],{"id":263,"data":548,"type":42},{"text":265,"level":227},{"id":267,"data":550,"type":222},{"text":269},{"id":271,"data":552,"type":249},{"code":273},{"id":275,"data":554,"type":42},{"text":277,"level":227},{"id":279,"data":556,"type":222},{"text":281},{"id":283,"data":558,"type":249},{"code":285},{"id":287,"data":560,"type":249},{"code":289},{"id":291,"data":562,"type":42},{"text":293,"level":227},{"id":295,"data":564,"type":222},{"text":297},{"id":299,"data":566,"type":249},{"code":301},{"id":303,"data":568,"type":42},{"text":305,"level":227},{"id":307,"data":570,"type":222},{"text":309},{"id":311,"data":572,"type":249},{"code":313},{"id":315,"data":574,"type":42},{"text":317,"level":227},{"id":319,"data":576,"type":240},{"items":577,"style":329},[322,323,324,325,326,327,328],{"id":331,"data":579,"type":42},{"text":333,"level":227},{"id":335,"data":581,"type":222},{"text":337},{"id":339,"data":583,"type":42},{"text":341,"level":227},{"id":343,"data":585,"type":240},{"items":586,"style":329},[346,347,348,349,350,351],{"id":353,"data":588,"type":42},{"text":355,"level":227},{"id":357,"data":590,"type":240},{"items":591,"style":239},[360,361,362,363,364],{"id":366,"data":593,"type":368},{},{"id":370,"data":595,"type":222},{"text":372},"Post erfolgreich abgerufen",{"items":598,"source":620,"manualIds":621,"manualMatchedIds":622},[599,606,613],{"id":600,"slug":601,"title":602,"excerpt":603,"featuredImage":604,"publishedAt":605},"381","enterprise-grade-multi-tenant-architecture-for-an-international-platform","Architettura Multi-Tenant di Livello Enterprise per una Piattaforma Internazionale","Loving Rocks è una piattaforma per matrimoni di livello enterprise progettata con una vera architettura multi-tenant, database isolati per tenant e internazionalizzazione integrata per scalabilità globale, sicurezza e stabilità operativa a lungo termine.","\u002Fuploads\u002F2026\u002F01\u002Fenterprise-grade-multi-tenant-architecture-for-an-international-platform-1769789121298-b6v7ak.webp","2026-01-30T12:04:00.000Z",{"id":607,"slug":608,"title":609,"excerpt":610,"featuredImage":611,"publishedAt":612},"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: Su cosa dovresti sviluppare nel 2026?","Lo stack di agenti di OpenAI è cambiato a settembre 2026. Questa guida all'architettura separa Agents API, Agents SDK, Responses API e Codex SDK in base alla proprietà del runtime—in modo che i team possano scegliere il giusto confine di controllo invece di confrontare i nomi dei prodotti.","\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":614,"slug":615,"title":616,"excerpt":617,"featuredImage":618,"publishedAt":619},"2","multi-database-architecture","Architettura multi-database con Prisma 7: Un Deep Dive per esperti","La gestione di paesaggi di dati complessi richiede architetture moderne. Prisma 7 offre funzionalità avanzate per l'integrazione multi-database e affronta le sfide della persistenza poliglotto.","\u002Fuploads\u002F2014\u002F09\u002FSEO-Mobile-Webapplikation-Muenchen-www.stajic.de_1.webp","2025-10-31T04:31:00.000Z","fallback",[],[]]