[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"portal-settings:stajic:sr":3,"public-menus:all":38,"post:a-practical-monorepo-architecture-next-js-platform-admin-fastify-api-prisma-and-nginx:sr":221,"related:post:a-practical-monorepo-architecture-next-js-platform-admin-fastify-api-prisma-and-nginx:sr: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","sr","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","Praktična monorepo arhitektura sa Next.js, Fastify, Prisma i NGINX","a-practical-monorepo-architecture-next-js-platform-admin-fastify-api-prisma-and-nginx","\u003Ch1>Platformski Monorepo: Next.js (Javni + Admin) + Fastify API + Prisma DB + NGINX\u003C\u002Fh1>\n\u003Cp>Ovaj dokument opisuje trenutnu ciljnu arhitekturu za monorepo sa javnim Next.js sajtom, Next.js admin aplikacijom sa sesijama zasnovanim na kolačićima, Fastify API-jem i DB paketom zasnovanim na Prismi. Napisan je za članove tima koji će raditi sa više paketa i potrebne su im predvidive granice.\u003C\u002Fp>\n\u003Ch2>Ograničenja o kojima ne pregovaramo (za sada)\u003C\u002Fh2>\n\u003Cul>\u003Cli>Javni sajt mora biti SSR (Next.js App Router). Bez prečica samo za SPA.\u003C\u002Fli>\u003Cli>Javni sajt komunicira sa bekendom samo putem javnih API ruta pod \u002Fapi (npr. \u002Fapi\u002Fcontent, \u002Fapi\u002Fmedia).\u003C\u002Fli>\u003Cli>Admin koristi autentifikaciju sesije zasnovanu na kolačićima. HTTP-only kolačić. Bez tokena u localStorage-u.\u003C\u002Fli>\u003Cli>API je Fastify. Sesije putem @fastify\u002Fsession. „fakeRedis“ skladište za razvoj; Redis u produkciji kasnije.\u003C\u002Fli>\u003Cli>Pristup bazi podataka je samo putem @platform\u002Fdb. Bez direktnih Prisma klijenata razbacanih po aplikacijama.\u003C\u002Fli>\u003Cli>NGINX terminira TLS i rutira \u002F, \u002Fadmin i \u002Fapi. Jedan domen je u redu; poddomen je opcionalan kasnije.\u003C\u002Fli>\u003Cli>Monitoring postoji od prvog dana (Prometheus scrape + Grafana dashboard placeholderi).\u003C\u002Fli>\u003C\u002Ful>\n\u003Ch2>Raspored repozitorijuma\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>Kompromisi (pravi)\u003C\u002Fh2>\n\u003Cul>\u003Cli>Sesije sa kolačićima su dosadne. To je funkcija. Rade iza proksija i drže logiku autentifikacije van frontenda. Loša strana: morate ispravno podesiti SameSite\u002FSecure\u002Fpath, inače ćete juriti „nasumične“ greške pri prijavi.\u003C\u002Fli>\u003Cli>Jedna \u002Fapi površina je čista, ali to takođe znači da morate biti strogi u pogledu toga šta je javno, a šta samo za administratore. Ne propuštajte administratorske krajnje tačke samo zato što je to zgodno.\u003C\u002Fli>\u003Cli>Prisma može biti „spremna za više baza podataka“ u smislu zamene provajdera. Nije magični most između SQL-a i Mongo-a sa jednom šemom. Ako neko kaže da jeste, nije je isporučio.\u003C\u002Fli>\u003Cli>Next.js SSR je dobar za SEO i prvo učitavanje. Takođe može preopteretiti API ako namerno ne podesite keširanje i revalidaciju.\u003C\u002Fli>\u003C\u002Ful>\n\u003Ch2>Ugovor o rutiranju (NGINX kao ulazna vrata)\u003C\u002Fh2>\n\u003Cp>Za sada sve rutiramo kroz jedan domen. Jednostavnije je za debagovanje. Ako kasnije premestimo admin na poddomen, ponovo ćemo razmotriti opseg kolačića i pretpostavke o CSRF-u.\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>API paket (Fastify) — sesije i autentifikacija\u003C\u002Fh2>\n\u003Cp>API poseduje autentifikaciju i stanje sesije. Aplikacije nikada ne kreiraju tokene. One samo šalju kolačiće nazad. Neka sadržaj sesije bude mali. ID korisnika + uloga. To je to.\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>DB paket (Prisma) — jedan klijent, deljeni tipovi\u003C\u002Fh2>\n\u003Cp>Izlažemo jedan PrismaClient iz @platform\u002Fdb. API uvozi to i samo to. Ako kreirate drugi Prisma klijent u aplikaciji jer je „brže za prototipovanje“, samo stvarate budući incident.\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>Admin aplikacija — sesija sa kolačićima i \u002Fme zaštita\u003C\u002Fh2>\n\u003Cp>Admin je normalna Next.js App Router aplikacija. Jedini poseban deo je da mora tretirati API kao izvor istine i uvek uključivati kolačiće prilikom pozivanja \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>Korak po korak: tok prijave administratora\u003C\u002Fh2>\n\u003Col>\u003Cli>Pretraživač šalje akreditive: POST \u002Fapi\u002Flogin\u003C\u002Fli>\u003Cli>NGINX prosleđuje \u002Fapi\u002F* na Fastify\u003C\u002Fli>\u003Cli>Fastify validira korisnika u odnosu na DB (Prisma)\u003C\u002Fli>\u003Cli>Fastify upisuje sesiju u fakeRedis skladište (dev) i vraća Set-Cookie: sid=…\u003C\u002Fli>\u003Cli>Pretraživač čuva kolačić (HTTP-only)\u003C\u002Fli>\u003Cli>Admin serverske komponente pozivaju GET \u002Fapi\u002Fme sa priloženim kolačićem\u003C\u002Fli>\u003Cli>Admin prikazuje kontrolnu tablu samo ako \u002Fme vrati ok=true\u003C\u002Fli>\u003C\u002Fol>\n\u003Ch2>Jedna stvar koja je pošla naopako (da je ne ponavljamo)\u003C\u002Fh2>\n\u003Cp>Imali smo petlju za prijavu iako je \u002Fapi\u002Flogin vratio 200. Kolačić nikada nije poslat nazad na \u002Fapi\u002Fme. Uzrok je bio dosadan: neusklađenost putanje kolačića + rutiranja proksija. Slučajno smo postavili kolačić za \u002Fadmin, ali \u002Fapi\u002Fme živi pod \u002Fapi. Pretraživač je uradio tačno ono što je trebalo. Nije poslao kolačić. Rešenje je bilo da se putanja kolačića postavi na „\u002F“ i potvrdi da NGINX nije prepisivao putanje na način koji bi to pokvario.\u003C\u002Fp>\n\u003Ch2>Plan projekta (prvo isporuka, kasnije dorada)\u003C\u002Fh2>\n\u003Col>\u003Cli>Faza 0: povezivanje monorepoa (Turbo, TS konfiguracija, uvoz deljenih paketa). Neka bude jednostavno.\u003C\u002Fli>\u003Cli>Faza 1: API krajnje tačke za autentifikaciju\u002Fsesiju: \u002Flogin, \u002Flogout, \u002Fme. Dodati osnovno ograničenje stope na \u002Flogin.\u003C\u002Fli>\u003Cli>Faza 2: Admin MVP: ekran za prijavu + serverska zaštita + minimalna ljuska kontrolne table.\u003C\u002Fli>\u003Cli>Faza 3: Krajnje tačke za čitanje sadržaja za javnu platformu: \u002Fapi\u002Fcontent\u002F* (prvo samo za čitanje).\u003C\u002Fli>\u003Cli>Faza 4: CRUD sadržaja u adminu + otpremanje medija (početi jednostavno, zatim kasnije zameniti skladište).\u003C\u002Fli>\u003Cli>Faza 5: Nadzor i učvršćivanje: Prometheus krajnja tačka za metrike, Grafana kontrolne table, zaglavlja, rezervne kopije.\u003C\u002Fli>\u003C\u002Fol>\n\u003Ch2>Definicija završenog (da se kasnije ne bismo svađali)\u003C\u002Fh2>\n\u003Cul>\u003Cli>Novi pretraživač se može prijaviti na \u002Fadmin i ostati prijavljen tokom osvežavanja.\u003C\u002Fli>\u003Cli>Javna platforma prikazuje SSR sadržaj sa \u002Fapi\u002Fcontent bez izlaganja krajnjih tačaka samo za administratore.\u003C\u002Fli>\u003Cli>NGINX ispravno rutira \u002F, \u002Fadmin, \u002Fapi u docker-compose-u.\u003C\u002Fli>\u003Cli>API se može restartovati bez narušavanja ponašanja sesije (resetovanja dev skladišta su u redu, ali mora graciozno da otkaže).\u003C\u002Fli>\u003Cli>Postoji barem jedna Prometheus scrape meta (čak i ako su Grafana paneli placeholderi).\u003C\u002Fli>\u003C\u002Ful>\n\u003Chr class=\"my-8\">\n\u003Cp>Ako implementirate novu krajnju tačku, prvo odlučite: da li je javna, samo za administratore ili interna. Ne nagađajte. Zatim je stavite iza odgovarajućeg prefiksa i zaštitite je. To je cela igra.\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},"Platformski Monorepo: Next.js (Javni + Admin) + Fastify API + Prisma DB + NGINX",{"id":235,"data":236,"type":238},"p_scope",{"text":237},"Ovaj dokument opisuje trenutnu ciljnu arhitekturu za monorepo sa javnim Next.js sajtom, Next.js admin aplikacijom sa sesijama zasnovanim na kolačićima, Fastify API-jem i DB paketom zasnovanim na Prismi. Napisan je za članove tima koji će raditi sa više paketa i potrebne su im predvidive granice.","paragraph",{"id":240,"data":241,"type":42},"h2_constraints",{"text":242,"level":243},"Ograničenja o kojima ne pregovaramo (za sada)",2,{"id":245,"data":246,"type":256},"list_constraints",{"items":247,"style":255},[248,249,250,251,252,253,254],"Javni sajt mora biti SSR (Next.js App Router). Bez prečica samo za SPA.","Javni sajt komunicira sa bekendom samo putem javnih API ruta pod \u002Fapi (npr. \u002Fapi\u002Fcontent, \u002Fapi\u002Fmedia).","Admin koristi autentifikaciju sesije zasnovanu na kolačićima. HTTP-only kolačić. Bez tokena u localStorage-u.","API je Fastify. Sesije putem @fastify\u002Fsession. „fakeRedis“ skladište za razvoj; Redis u produkciji kasnije.","Pristup bazi podataka je samo putem @platform\u002Fdb. Bez direktnih Prisma klijenata razbacanih po aplikacijama.","NGINX terminira TLS i rutira \u002F, \u002Fadmin i \u002Fapi. Jedan domen je u redu; poddomen je opcionalan kasnije.","Monitoring postoji od prvog dana (Prometheus scrape + Grafana dashboard placeholderi).","unordered","list",{"id":258,"data":259,"type":42},"h2_layout",{"text":260,"level":243},"Raspored repozitorijuma",{"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},"Kompromisi (pravi)",{"id":271,"data":272,"type":256},"list_tradeoffs",{"items":273,"style":255},[274,275,276,277],"Sesije sa kolačićima su dosadne. To je funkcija. Rade iza proksija i drže logiku autentifikacije van frontenda. Loša strana: morate ispravno podesiti SameSite\u002FSecure\u002Fpath, inače ćete juriti „nasumične“ greške pri prijavi.","Jedna \u002Fapi površina je čista, ali to takođe znači da morate biti strogi u pogledu toga šta je javno, a šta samo za administratore. Ne propuštajte administratorske krajnje tačke samo zato što je to zgodno.","Prisma može biti „spremna za više baza podataka“ u smislu zamene provajdera. Nije magični most između SQL-a i Mongo-a sa jednom šemom. Ako neko kaže da jeste, nije je isporučio.","Next.js SSR je dobar za SEO i prvo učitavanje. Takođe može preopteretiti API ako namerno ne podesite keširanje i revalidaciju.",{"id":279,"data":280,"type":42},"h2_routing",{"text":281,"level":243},"Ugovor o rutiranju (NGINX kao ulazna vrata)",{"id":283,"data":284,"type":238},"p_routing",{"text":285},"Za sada sve rutiramo kroz jedan domen. Jednostavnije je za debagovanje. Ako kasnije premestimo admin na poddomen, ponovo ćemo razmotriti opseg kolačića i pretpostavke o CSRF-u.",{"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},"API paket (Fastify) — sesije i autentifikacija",{"id":295,"data":296,"type":238},"p_api",{"text":297},"API poseduje autentifikaciju i stanje sesije. Aplikacije nikada ne kreiraju tokene. One samo šalju kolačiće nazad. Neka sadržaj sesije bude mali. ID korisnika + uloga. To je to.",{"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},"DB paket (Prisma) — jedan klijent, deljeni tipovi",{"id":311,"data":312,"type":238},"p_db",{"text":313},"Izlažemo jedan PrismaClient iz @platform\u002Fdb. API uvozi to i samo to. Ako kreirate drugi Prisma klijent u aplikaciji jer je „brže za prototipovanje“, samo stvarate budući incident.",{"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},"Admin aplikacija — sesija sa kolačićima i \u002Fme zaštita",{"id":323,"data":324,"type":238},"p_admin",{"text":325},"Admin je normalna Next.js App Router aplikacija. Jedini poseban deo je da mora tretirati API kao izvor istine i uvek uključivati kolačiće prilikom pozivanja \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},"Korak po korak: tok prijave administratora",{"id":335,"data":336,"type":256},"list_flow",{"items":337,"style":345},[338,339,340,341,342,343,344],"Pretraživač šalje akreditive: POST \u002Fapi\u002Flogin","NGINX prosleđuje \u002Fapi\u002F* na Fastify","Fastify validira korisnika u odnosu na DB (Prisma)","Fastify upisuje sesiju u fakeRedis skladište (dev) i vraća Set-Cookie: sid=…","Pretraživač čuva kolačić (HTTP-only)","Admin serverske komponente pozivaju GET \u002Fapi\u002Fme sa priloženim kolačićem","Admin prikazuje kontrolnu tablu samo ako \u002Fme vrati ok=true","ordered",{"id":347,"data":348,"type":42},"h2_went_wrong",{"text":349,"level":243},"Jedna stvar koja je pošla naopako (da je ne ponavljamo)",{"id":351,"data":352,"type":238},"p_went_wrong",{"text":353},"Imali smo petlju za prijavu iako je \u002Fapi\u002Flogin vratio 200. Kolačić nikada nije poslat nazad na \u002Fapi\u002Fme. Uzrok je bio dosadan: neusklađenost putanje kolačića + rutiranja proksija. Slučajno smo postavili kolačić za \u002Fadmin, ali \u002Fapi\u002Fme živi pod \u002Fapi. Pretraživač je uradio tačno ono što je trebalo. Nije poslao kolačić. Rešenje je bilo da se putanja kolačića postavi na „\u002F“ i potvrdi da NGINX nije prepisivao putanje na način koji bi to pokvario.",{"id":355,"data":356,"type":42},"h2_project_plan",{"text":357,"level":243},"Plan projekta (prvo isporuka, kasnije dorada)",{"id":359,"data":360,"type":256},"list_plan",{"items":361,"style":345},[362,363,364,365,366,367],"Faza 0: povezivanje monorepoa (Turbo, TS konfiguracija, uvoz deljenih paketa). Neka bude jednostavno.","Faza 1: API krajnje tačke za autentifikaciju\u002Fsesiju: \u002Flogin, \u002Flogout, \u002Fme. Dodati osnovno ograničenje stope na \u002Flogin.","Faza 2: Admin MVP: ekran za prijavu + serverska zaštita + minimalna ljuska kontrolne table.","Faza 3: Krajnje tačke za čitanje sadržaja za javnu platformu: \u002Fapi\u002Fcontent\u002F* (prvo samo za čitanje).","Faza 4: CRUD sadržaja u adminu + otpremanje medija (početi jednostavno, zatim kasnije zameniti skladište).","Faza 5: Nadzor i učvršćivanje: Prometheus krajnja tačka za metrike, Grafana kontrolne table, zaglavlja, rezervne kopije.",{"id":369,"data":370,"type":42},"h2_definition_done",{"text":371,"level":243},"Definicija završenog (da se kasnije ne bismo svađali)",{"id":373,"data":374,"type":256},"list_dod",{"items":375,"style":255},[376,377,378,379,380],"Novi pretraživač se može prijaviti na \u002Fadmin i ostati prijavljen tokom osvežavanja.","Javna platforma prikazuje SSR sadržaj sa \u002Fapi\u002Fcontent bez izlaganja krajnjih tačaka samo za administratore.","NGINX ispravno rutira \u002F, \u002Fadmin, \u002Fapi u docker-compose-u.","API se može restartovati bez narušavanja ponašanja sesije (resetovanja dev skladišta su u redu, ali mora graciozno da otkaže).","Postoji barem jedna Prometheus scrape meta (čak i ako su Grafana paneli placeholderi).",{"id":382,"data":383,"type":384},"hr_end",{},"delimiter",{"id":386,"data":387,"type":238},"p_next",{"text":388},"Ako implementirate novu krajnju tačku, prvo odlučite: da li je javna, samo za administratore ili interna. Ne nagađajte. Zatim je stavite iza odgovarajućeg prefiksa i zaštitite je. To je cela igra.","2.29.1","Istražite praktičnu monorepo arhitekturu koristeći Next.js, Fastify, Prisma i NGINX, ističući integraciju i tok rada iz stvarnog sveta.","\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,"Sposobnosti","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},"381","enterprise-grade-multi-tenant-architecture-for-an-international-platform","Višezakupna arhitektura korporativnog nivoa za međunarodnu platformu","Loving Rocks je platforma za venčanja poslovne klase, dizajnirana sa istinskom više-zakupnom arhitekturom, izolovanim bazama podataka po zakupcu i ugrađenom internacionalizacijom za globalnu skalabilnost, bezbednost i dugoročnu operativnu stabilnost.","\u002Fuploads\u002F2026\u002F01\u002Fenterprise-grade-multi-tenant-architecture-for-an-international-platform-1769789121298-b6v7ak.webp","2026-01-30T12:04:00.000Z",{"id":623,"slug":624,"title":625,"excerpt":626,"featuredImage":627,"publishedAt":628},"2","multi-database-architecture","Arhitektura više baza podataka sa Prisma 7: Duboki zaron za stručnjake","Upravljanje složenim pejzažima podataka zahteva moderne arhitekture. Prisma 7 nudi napredne funkcije za integraciju više baza podataka i rešava izazove poliglotne perzistencije.","\u002Fuploads\u002F2014\u002F09\u002FSEO-Mobile-Webapplikation-Muenchen-www.stajic.de_1.webp","2025-10-31T04:31:00.000Z",{"id":630,"slug":631,"title":632,"excerpt":633,"featuredImage":634,"publishedAt":635},"492","mcp-explained-what-it-connects-what-it-does-not-do-and-where-it-fits","MCP objašnjen: Šta povezuje, šta ne radi i gde se uklapa","Model Context Protocol povezuje AI aplikacije sa eksternim alatima, resursima i promptovima kroz standardnu granicu klijent-server. Saznajte šta MCP radi, šta ne radi i gde se uklapa u arhitekturu agenata.","\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":637,"slug":638,"title":639,"excerpt":640,"featuredImage":641,"publishedAt":642},"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: Na čemu bi trebalo da gradite u 2026?","OpenAI-jev stek agenata promenio se u septembru 2026. Ovaj arhitekturni vodič razdvaja Agents API, Agents SDK, Responses API i Codex SDK prema vlasništvu nad izvršnim okruženjem—kako bi timovi mogli da izaberu pravu granicu kontrole umesto da porede nazive proizvoda.","\u002Fuploads\u002F2026\u002F09\u002Fopenai-agents-api-vs-agents-sdk-vs-responses-api-what-should-you-build-on-in-2026-1790351846714-zi7lus.webp","2026-09-25T11:56:00.000Z","fallback",[],[]]