[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"portal-settings:stajic:de":3,"public-menus:all":37,"post:a-practical-monorepo-architecture-next-js-platform-admin-fastify-api-prisma-and-nginx:de":204,"related:post:a-practical-monorepo-architecture-next-js-platform-admin-fastify-api-prisma-and-nginx:de:1":596},{"statusCode":4,"data":5,"message":36},200,{"tenantId":6,"lang":7,"defaultLang":7,"siteUrl":8,"contactEmail":9,"brandName":10,"logoUrl":11,"siteName":10,"siteDescription":12,"ogImage":9,"robotsIndex":13,"socialLinks":9,"reservedSlugs":9,"seoPolicy":14},"stajic","de","https:\u002F\u002Fstajic.de",null,"Stajic Platform","\u002FLogo_Planet.svg","Stajic Portal",true,{"branding":15,"relatedContent":16,"crossDomainLinks":17},{"logoUrl":11},{"enabled":13},[18,21,24,27,30,33],{"url":19,"label":20,"isActive":13,"showInFooter":13,"includeInSameAs":13},"https:\u002F\u002Ffigure.rocks","figure.rocks",{"url":22,"label":23,"isActive":13,"showInFooter":13,"includeInSameAs":13},"https:\u002F\u002Floving.rocks","loving.rocks",{"url":25,"label":26,"isActive":13,"showInFooter":13,"includeInSameAs":13},"https:\u002F\u002Fbazify.com","bazify.com",{"url":28,"label":29,"isActive":13,"showInFooter":13,"includeInSameAs":13},"https:\u002F\u002Fbazify.de","bazify.de",{"url":31,"label":32,"isActive":13,"showInFooter":13,"includeInSameAs":13},"https:\u002F\u002Fbazify.at","bazify.at",{"url":34,"label":35,"isActive":13,"showInFooter":13,"includeInSameAs":13},"https:\u002F\u002Fbazify.ba","bazify.ba","Portal settings resolved",[38,44],{"id":39,"name":40,"location":41,"isActive":13,"isDefault":42,"items":43},1,"main-navigation","header",false,[],{"id":45,"name":46,"location":47,"isActive":13,"isDefault":13,"items":48},4,"main-menu","sidebar",[49,65,78,92,102,117,132],{"id":50,"title":51,"url":59,"target":60,"icon":61,"isActive":13,"type":62,"productId":9,"categoryId":9,"shopCategoryId":9,"articleId":9,"pageId":63,"portfolioId":9,"children":64},"item-18",{"de":52,"en":53,"es":54,"fr":55,"it":53,"ru":56,"sr":57,"zh":58},"Startseite","Home","Inicio","Accueil","Главная","Почетна","首页","\u002Ffull-stack-web-developer-munich-performance-seo-and-maintainable-builds","_self","i-lucide-home","page",111,[],{"id":66,"title":67,"url":74,"target":60,"icon":75,"isActive":13,"type":62,"productId":9,"categoryId":9,"shopCategoryId":9,"articleId":9,"pageId":76,"portfolioId":9,"children":77},"item-22",{"de":68,"en":68,"es":69,"fr":68,"it":70,"ru":71,"sr":72,"zh":73},"Vision","Visión","Visione","Видение","Визија","想象","\u002Fueber-uns-webdesign-muenchen-webaplikation","i-lucide-eye",113,[],{"id":79,"title":80,"url":88,"target":60,"icon":89,"isActive":13,"type":62,"productId":9,"categoryId":9,"shopCategoryId":9,"articleId":9,"pageId":90,"portfolioId":9,"children":91},"item-19",{"de":81,"en":82,"es":83,"fr":82,"it":84,"ru":85,"sr":86,"zh":87},"Leistungen","Services","Servicios","Servizi","Услуги","Услуге","服务","\u002Fservices-dienstleistungen-muenchen","i-lucide-wrench",116,[],{"id":93,"title":94,"url":98,"target":60,"icon":99,"isActive":13,"type":62,"productId":9,"categoryId":9,"shopCategoryId":9,"articleId":9,"pageId":100,"portfolioId":9,"children":101},"item-23",{"de":95,"en":95,"es":95,"fr":95,"it":95,"ru":96,"sr":96,"zh":97},"Blog","Блог","博客","\u002Fblog","i-lucide-book-open",112,[],{"id":103,"title":104,"url":113,"target":60,"icon":114,"isActive":13,"type":62,"productId":9,"categoryId":9,"shopCategoryId":9,"articleId":9,"pageId":115,"portfolioId":9,"children":116},"item-32",{"de":105,"en":106,"es":107,"fr":108,"it":109,"ru":110,"sr":111,"zh":112},"Neue Technologien","New Technologies","Nuevas tecnologías","Nouvelles technologies","Nuove tecnologie","Новые технологии","Нове технологије","新技术！","\u002Fneue-webtechnologien","i-lucide-sparkles",122,[],{"id":118,"title":119,"url":128,"target":60,"icon":129,"isActive":13,"type":62,"productId":9,"categoryId":9,"shopCategoryId":9,"articleId":9,"pageId":130,"portfolioId":9,"children":131},"item-20",{"de":120,"en":121,"es":122,"fr":123,"it":124,"ru":125,"sr":126,"zh":127},"Kontakt","Contact us!","Contacto","Contact","Contatto","Контакт","Контактирајте нас","联系我们！","\u002Fcontact","i-lucide-mail",115,[],{"id":133,"title":134,"url":143,"target":60,"icon":144,"isActive":13,"type":62,"productId":9,"categoryId":9,"shopCategoryId":9,"articleId":9,"pageId":145,"portfolioId":9,"children":146},"item-21",{"de":135,"en":136,"es":137,"fr":138,"it":139,"ru":140,"sr":141,"zh":142},"Unsere Arbeit","Our Work","Nuestro trabajo","Nos réalisations","I nostri lavori","Наши работы","Наши радови","文件夹","\u002Fportfolio","i-lucide-briefcase",114,[147,160,174,180,192],{"id":148,"title":149,"url":143,"target":60,"icon":158,"isActive":13,"type":62,"productId":9,"categoryId":9,"shopCategoryId":9,"articleId":9,"pageId":145,"portfolioId":9,"children":159},"item-24",{"de":150,"en":151,"es":152,"fr":153,"it":154,"ru":155,"sr":156,"zh":157},"Alle Projekte","All Projects","Todos los proyectos","Tous les projets","Tutti i progetti","Все проекты","Сви пројекти","所有项目","i-lucide-grid-3x3",[],{"id":161,"title":162,"url":170,"target":60,"icon":171,"isActive":13,"type":172,"productId":9,"categoryId":9,"shopCategoryId":9,"articleId":9,"pageId":9,"portfolioId":9,"children":173},"item-29",{"de":163,"en":164,"es":165,"fr":166,"it":167,"ru":168,"sr":169,"zh":142},"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":175,"title":176,"url":178,"target":60,"icon":171,"isActive":13,"type":172,"productId":9,"categoryId":9,"shopCategoryId":9,"articleId":9,"pageId":9,"portfolioId":9,"children":179},"item-28",{"de":177,"en":177,"es":177,"fr":177,"it":177,"ru":177,"sr":177,"zh":177},"Solr Suggester","\u002Fportfolio\u002Fsolr-fuzzy-suggester-und-solr-infix-suggester-abfrage-ueber-ajax-und-filterung",[],{"id":181,"title":182,"url":190,"target":60,"icon":171,"isActive":13,"type":172,"productId":9,"categoryId":9,"shopCategoryId":9,"articleId":9,"pageId":9,"portfolioId":9,"children":191},"item-27",{"de":183,"en":184,"es":185,"fr":186,"it":187,"ru":188,"sr":189,"zh":184},"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":193,"title":194,"url":202,"target":60,"icon":171,"isActive":13,"type":172,"productId":9,"categoryId":9,"shopCategoryId":9,"articleId":9,"pageId":9,"portfolioId":9,"children":203},"item-31",{"de":195,"en":196,"es":197,"fr":198,"it":199,"ru":200,"sr":201,"zh":196},"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":205,"message":595},{"id":206,"title":207,"slug":208,"content":209,"contentJson":210,"excerpt":373,"featuredImage":374,"featuredImageAlt":375,"featuredImageCaption":9,"featuredImageTitle":9,"featuredImageCopyright":9,"featuredImageAuthor":9,"featuredImageSourceUrl":9,"featuredImageLicense":9,"featuredImageIsAiGenerated":42,"status":376,"publishedAt":377,"createdAt":378,"updatedAt":379,"seoLocalePaths":380,"categories":389,"author":397,"translations":402},"382","Eine Praktische Monorepo-Architektur mit Next.js, Fastify, Prisma und NGINX","a-practical-monorepo-architecture-next-js-platform-admin-fastify-api-prisma-and-nginx","\u003Ch1>Plattform-Monorepo: Next.js (Öffentlich + Admin) + Fastify API + Prisma DB + NGINX\u003C\u002Fh1>\n\u003Cp>Dieses Dokument beschreibt die aktuelle Zielarchitektur für ein Monorepo mit einer öffentlichen Next.js-Website, einer Next.js-Admin-App mit Cookie-Sessions, einer Fastify-API und einem Prisma-basierten DB-Paket. Es ist für Teammitglieder geschrieben, die mit mehreren Paketen arbeiten und vorhersehbare Grenzen benötigen.\u003C\u002Fp>\n\u003Ch2>Einschränkungen, die wir (vorerst) nicht verhandeln\u003C\u002Fh2>\n\u003Cul>\u003Cli>Die öffentliche Website muss SSR sein (Next.js App Router). Keine reinen SPA-Abkürzungen.\u003C\u002Fli>\u003Cli>Die öffentliche Website kommuniziert mit dem Backend nur über öffentliche API-Routen unter \u002Fapi (z.B. \u002Fapi\u002Fcontent, \u002Fapi\u002Fmedia).\u003C\u002Fli>\u003Cli>Admin verwendet Cookie-basierte Session-Authentifizierung. HTTP-only Cookie. Keine localStorage-Tokens.\u003C\u002Fli>\u003Cli>API ist Fastify. Sessions über @fastify\u002Fsession. „fakeRedis“-Speicher für die Entwicklung; Redis später in Produktion.\u003C\u002Fli>\u003Cli>DB-Zugriff erfolgt ausschließlich über @platform\u002Fdb. Keine direkten Prisma-Clients, die in Apps verstreut sind.\u003C\u002Fli>\u003Cli>NGINX beendet TLS und leitet \u002F, \u002Fadmin und \u002Fapi weiter. Eine Domain ist in Ordnung; Subdomain ist später optional.\u003C\u002Fli>\u003Cli>Monitoring existiert vom ersten Tag an (Prometheus Scrape + Grafana Dashboard-Platzhalter).\u003C\u002Fli>\u003C\u002Ful>\n\u003Ch2>Repository-Layout\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>Kompromisse (die echten)\u003C\u002Fh2>\n\u003Cul>\u003Cli>Cookie-Sessions sind langweilig. Das ist ein Feature. Sie funktionieren hinter Proxys und halten die Authentifizierungslogik vom Frontend fern. Der Nachteil: Man muss SameSite\u002FSecure\u002Fpath richtig einstellen, sonst jagt man „zufällige“ Anmeldefehler.\u003C\u002Fli>\u003Cli>Eine \u002Fapi-Oberfläche ist sauber, bedeutet aber auch, dass man streng sein muss, was öffentlich und was nur für Administratoren ist. Keine Admin-Endpunkte preisgeben, nur weil es bequem ist.\u003C\u002Fli>\u003Cli>Prisma kann im Sinne des Austauschs von Anbietern „multi-db-fähig“ sein. Es ist keine magische Brücke zwischen SQL und Mongo mit einem einzigen Schema. Wenn jemand das behauptet, hat er es noch nicht ausgeliefert.\u003C\u002Fli>\u003Cli>Next.js SSR ist gut für SEO und die erste Ladezeit. Es kann aber auch die API überlasten, wenn man Caching und Revalidierung nicht bewusst einstellt.\u003C\u002Fli>\u003C\u002Ful>\n\u003Ch2>Routing-Vertrag (NGINX als Front-Door)\u003C\u002Fh2>\n\u003Cp>Wir leiten vorerst alles über eine Domain. Das ist einfacher zu debuggen. Wenn wir den Admin-Bereich später auf eine Subdomain verschieben, werden wir den Cookie-Geltungsbereich und die CSRF-Annahmen überprüfen.\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) – Sessions und Authentifizierung\u003C\u002Fh2>\n\u003Cp>Die API ist für Authentifizierung und Session-Status zuständig. Die Apps erstellen niemals Tokens. Sie senden lediglich Cookies zurück. Halten Sie die Session-Nutzlast klein. Benutzer-ID + Rolle. Das ist alles.\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) – einzelner Client, gemeinsame Typen\u003C\u002Fh2>\n\u003Cp>Wir stellen einen PrismaClient von @platform\u002Fdb zur Verfügung. Die API importiert diesen und nur diesen. Wenn Sie einen zweiten Prisma-Client in einer App erstellen, weil es „schneller zu prototypisieren“ ist, schaffen Sie nur einen zukünftigen Vorfall.\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-App – Cookie-Session und \u002Fme-Schutz\u003C\u002Fh2>\n\u003Cp>Admin ist eine normale Next.js App Router-Anwendung. Das Besondere ist, dass sie die API als Quelle der Wahrheit behandeln und beim Aufruf von \u002Fapi\u002Fme immer Cookies mitsenden muss.\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>Schritt für Schritt: Admin-Login-Flow\u003C\u002Fh2>\n\u003Col>\u003Cli>Browser übermittelt Anmeldedaten: POST \u002Fapi\u002Flogin\u003C\u002Fli>\u003Cli>NGINX leitet \u002Fapi\u002F* an Fastify weiter\u003C\u002Fli>\u003Cli>Fastify validiert Benutzer gegen DB (Prisma)\u003C\u002Fli>\u003Cli>Fastify schreibt Session in fakeRedis-Speicher (Entwicklung) und gibt Set-Cookie: sid=… zurück\u003C\u002Fli>\u003Cli>Browser speichert das Cookie (HTTP-only)\u003C\u002Fli>\u003Cli>Admin-Serverkomponenten rufen GET \u002Fapi\u002Fme mit angehängtem Cookie auf\u003C\u002Fli>\u003Cli>Admin rendert Dashboard nur, wenn \u002Fme ok=true zurückgibt\u003C\u002Fli>\u003C\u002Fol>\n\u003Ch2>Eine Sache, die schiefgelaufen ist (damit wir sie nicht wiederholen)\u003C\u002Fh2>\n\u003Cp>Wir hatten eine Anmeldeschleife, obwohl \u002Fapi\u002Flogin 200 zurückgab. Das Cookie wurde nie an \u002Fapi\u002Fme zurückgesendet. Die Ursache war langweilig: Cookie-Pfad + Proxy-Routing-Fehler. Wir haben das Cookie versehentlich für \u002Fadmin gesetzt, aber \u002Fapi\u002Fme befindet sich unter \u002Fapi. Der Browser hat genau das getan, was er sollte. Er hat das Cookie nicht gesendet. Die Lösung war, den Cookie-Pfad auf „\u002F“ zu setzen und zu bestätigen, dass NGINX die Pfade nicht so umschrieb, dass es zu Problemen kam.\u003C\u002Fp>\n\u003Ch2>Projektplan (zuerst liefern, später verfeinern)\u003C\u002Fh2>\n\u003Col>\u003Cli>Phase 0: Monorepo-Verkabelung (Turbo, TS-Konfiguration, gemeinsame Paketimporte). Schlank halten.\u003C\u002Fli>\u003Cli>Phase 1: API-Authentifizierungs-\u002FSession-Endpunkte: \u002Flogin, \u002Flogout, \u002Fme. Grundlegende Ratenbegrenzung für \u002Flogin hinzufügen.\u003C\u002Fli>\u003Cli>Phase 2: Admin MVP: Anmeldebildschirm + serverseitiger Schutz + minimale Dashboard-Shell.\u003C\u002Fli>\u003Cli>Phase 3: Content-Lese-Endpunkte für die öffentliche Plattform: \u002Fapi\u002Fcontent\u002F* (zuerst nur lesend).\u003C\u002Fli>\u003Cli>Phase 4: Content-CRUD im Admin + Medien-Upload (einfach beginnen, dann später den Speicher wechseln).\u003C\u002Fli>\u003Cli>Phase 5: Überwachung und Härtung: Prometheus-Metrik-Endpunkt, Grafana-Dashboards, Header, Backups.\u003C\u002Fli>\u003C\u002Fol>\n\u003Ch2>Definition von „Fertig“ (damit wir später nicht streiten)\u003C\u002Fh2>\n\u003Cul>\u003Cli>Ein frischer Browser kann sich bei \u002Fadmin anmelden und bleibt über Aktualisierungen hinweg angemeldet.\u003C\u002Fli>\u003Cli>Die öffentliche Plattform rendert SSR-Inhalte von \u002Fapi\u002Fcontent, ohne nur für Administratoren zugängliche Endpunkte freizulegen.\u003C\u002Fli>\u003Cli>NGINX leitet \u002F, \u002Fadmin, \u002Fapi in docker-compose korrekt weiter.\u003C\u002Fli>\u003Cli>Die API kann neu gestartet werden, ohne das Session-Verhalten zu beschädigen (Entwicklungs-Speicher-Resets sind in Ordnung, aber sie muss graceful fehlschlagen).\u003C\u002Fli>\u003Cli>Mindestens ein Prometheus-Scrape-Ziel existiert (auch wenn Grafana-Panels Platzhalter sind).\u003C\u002Fli>\u003C\u002Ful>\n\u003Chr class=\"my-8\">\n\u003Cp>Wenn Sie einen neuen Endpunkt implementieren, entscheiden Sie zuerst: Ist er öffentlich, nur für Administratoren oder intern? Raten Sie nicht. Dann platzieren Sie ihn hinter dem richtigen Präfix und schützen ihn. Das ist das ganze Spiel.\u003C\u002Fp>",{"time":211,"blocks":212,"version":372},1769827200000,[213,217,222,227,240,244,249,253,261,265,269,273,277,281,285,289,293,297,301,305,309,313,317,329,333,337,341,351,355,364,368],{"id":214,"data":215,"type":41},"h1_arch",{"text":216,"level":39},"Plattform-Monorepo: Next.js (Öffentlich + Admin) + Fastify API + Prisma DB + NGINX",{"id":218,"data":219,"type":221},"p_scope",{"text":220},"Dieses Dokument beschreibt die aktuelle Zielarchitektur für ein Monorepo mit einer öffentlichen Next.js-Website, einer Next.js-Admin-App mit Cookie-Sessions, einer Fastify-API und einem Prisma-basierten DB-Paket. Es ist für Teammitglieder geschrieben, die mit mehreren Paketen arbeiten und vorhersehbare Grenzen benötigen.","paragraph",{"id":223,"data":224,"type":41},"h2_constraints",{"text":225,"level":226},"Einschränkungen, die wir (vorerst) nicht verhandeln",2,{"id":228,"data":229,"type":239},"list_constraints",{"items":230,"style":238},[231,232,233,234,235,236,237],"Die öffentliche Website muss SSR sein (Next.js App Router). Keine reinen SPA-Abkürzungen.","Die öffentliche Website kommuniziert mit dem Backend nur über öffentliche API-Routen unter \u002Fapi (z.B. \u002Fapi\u002Fcontent, \u002Fapi\u002Fmedia).","Admin verwendet Cookie-basierte Session-Authentifizierung. HTTP-only Cookie. Keine localStorage-Tokens.","API ist Fastify. Sessions über @fastify\u002Fsession. „fakeRedis“-Speicher für die Entwicklung; Redis später in Produktion.","DB-Zugriff erfolgt ausschließlich über @platform\u002Fdb. Keine direkten Prisma-Clients, die in Apps verstreut sind.","NGINX beendet TLS und leitet \u002F, \u002Fadmin und \u002Fapi weiter. Eine Domain ist in Ordnung; Subdomain ist später optional.","Monitoring existiert vom ersten Tag an (Prometheus Scrape + Grafana Dashboard-Platzhalter).","unordered","list",{"id":241,"data":242,"type":41},"h2_layout",{"text":243,"level":226},"Repository-Layout",{"id":245,"data":246,"type":248},"code_tree",{"code":247},"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":250,"data":251,"type":41},"h2_tradeoffs",{"text":252,"level":226},"Kompromisse (die echten)",{"id":254,"data":255,"type":239},"list_tradeoffs",{"items":256,"style":238},[257,258,259,260],"Cookie-Sessions sind langweilig. Das ist ein Feature. Sie funktionieren hinter Proxys und halten die Authentifizierungslogik vom Frontend fern. Der Nachteil: Man muss SameSite\u002FSecure\u002Fpath richtig einstellen, sonst jagt man „zufällige“ Anmeldefehler.","Eine \u002Fapi-Oberfläche ist sauber, bedeutet aber auch, dass man streng sein muss, was öffentlich und was nur für Administratoren ist. Keine Admin-Endpunkte preisgeben, nur weil es bequem ist.","Prisma kann im Sinne des Austauschs von Anbietern „multi-db-fähig“ sein. Es ist keine magische Brücke zwischen SQL und Mongo mit einem einzigen Schema. Wenn jemand das behauptet, hat er es noch nicht ausgeliefert.","Next.js SSR ist gut für SEO und die erste Ladezeit. Es kann aber auch die API überlasten, wenn man Caching und Revalidierung nicht bewusst einstellt.",{"id":262,"data":263,"type":41},"h2_routing",{"text":264,"level":226},"Routing-Vertrag (NGINX als Front-Door)",{"id":266,"data":267,"type":221},"p_routing",{"text":268},"Wir leiten vorerst alles über eine Domain. Das ist einfacher zu debuggen. Wenn wir den Admin-Bereich später auf eine Subdomain verschieben, werden wir den Cookie-Geltungsbereich und die CSRF-Annahmen überprüfen.",{"id":270,"data":271,"type":248},"code_nginx",{"code":272},"# 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":274,"data":275,"type":41},"h2_api",{"text":276,"level":226},"API-Paket (Fastify) – Sessions und Authentifizierung",{"id":278,"data":279,"type":221},"p_api",{"text":280},"Die API ist für Authentifizierung und Session-Status zuständig. Die Apps erstellen niemals Tokens. Sie senden lediglich Cookies zurück. Halten Sie die Session-Nutzlast klein. Benutzer-ID + Rolle. Das ist alles.",{"id":282,"data":283,"type":248},"code_server",{"code":284},"\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":286,"data":287,"type":248},"code_session_store",{"code":288},"\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":290,"data":291,"type":41},"h2_db",{"text":292,"level":226},"DB-Paket (Prisma) – einzelner Client, gemeinsame Typen",{"id":294,"data":295,"type":221},"p_db",{"text":296},"Wir stellen einen PrismaClient von @platform\u002Fdb zur Verfügung. Die API importiert diesen und nur diesen. Wenn Sie einen zweiten Prisma-Client in einer App erstellen, weil es „schneller zu prototypisieren“ ist, schaffen Sie nur einen zukünftigen Vorfall.",{"id":298,"data":299,"type":248},"code_db",{"code":300},"\u002F\u002F packages\u002Fdb\u002Fsrc\u002Findex.ts\nimport { PrismaClient } from \"@prisma\u002Fclient\";\n\nexport const db = new PrismaClient();",{"id":302,"data":303,"type":41},"h2_admin",{"text":304,"level":226},"Admin-App – Cookie-Session und \u002Fme-Schutz",{"id":306,"data":307,"type":221},"p_admin",{"text":308},"Admin ist eine normale Next.js App Router-Anwendung. Das Besondere ist, dass sie die API als Quelle der Wahrheit behandeln und beim Aufruf von \u002Fapi\u002Fme immer Cookies mitsenden muss.",{"id":310,"data":311,"type":248},"code_admin_guard",{"code":312},"\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":314,"data":315,"type":41},"h2_flow",{"text":316,"level":226},"Schritt für Schritt: Admin-Login-Flow",{"id":318,"data":319,"type":239},"list_flow",{"items":320,"style":328},[321,322,323,324,325,326,327],"Browser übermittelt Anmeldedaten: POST \u002Fapi\u002Flogin","NGINX leitet \u002Fapi\u002F* an Fastify weiter","Fastify validiert Benutzer gegen DB (Prisma)","Fastify schreibt Session in fakeRedis-Speicher (Entwicklung) und gibt Set-Cookie: sid=… zurück","Browser speichert das Cookie (HTTP-only)","Admin-Serverkomponenten rufen GET \u002Fapi\u002Fme mit angehängtem Cookie auf","Admin rendert Dashboard nur, wenn \u002Fme ok=true zurückgibt","ordered",{"id":330,"data":331,"type":41},"h2_went_wrong",{"text":332,"level":226},"Eine Sache, die schiefgelaufen ist (damit wir sie nicht wiederholen)",{"id":334,"data":335,"type":221},"p_went_wrong",{"text":336},"Wir hatten eine Anmeldeschleife, obwohl \u002Fapi\u002Flogin 200 zurückgab. Das Cookie wurde nie an \u002Fapi\u002Fme zurückgesendet. Die Ursache war langweilig: Cookie-Pfad + Proxy-Routing-Fehler. Wir haben das Cookie versehentlich für \u002Fadmin gesetzt, aber \u002Fapi\u002Fme befindet sich unter \u002Fapi. Der Browser hat genau das getan, was er sollte. Er hat das Cookie nicht gesendet. Die Lösung war, den Cookie-Pfad auf „\u002F“ zu setzen und zu bestätigen, dass NGINX die Pfade nicht so umschrieb, dass es zu Problemen kam.",{"id":338,"data":339,"type":41},"h2_project_plan",{"text":340,"level":226},"Projektplan (zuerst liefern, später verfeinern)",{"id":342,"data":343,"type":239},"list_plan",{"items":344,"style":328},[345,346,347,348,349,350],"Phase 0: Monorepo-Verkabelung (Turbo, TS-Konfiguration, gemeinsame Paketimporte). Schlank halten.","Phase 1: API-Authentifizierungs-\u002FSession-Endpunkte: \u002Flogin, \u002Flogout, \u002Fme. Grundlegende Ratenbegrenzung für \u002Flogin hinzufügen.","Phase 2: Admin MVP: Anmeldebildschirm + serverseitiger Schutz + minimale Dashboard-Shell.","Phase 3: Content-Lese-Endpunkte für die öffentliche Plattform: \u002Fapi\u002Fcontent\u002F* (zuerst nur lesend).","Phase 4: Content-CRUD im Admin + Medien-Upload (einfach beginnen, dann später den Speicher wechseln).","Phase 5: Überwachung und Härtung: Prometheus-Metrik-Endpunkt, Grafana-Dashboards, Header, Backups.",{"id":352,"data":353,"type":41},"h2_definition_done",{"text":354,"level":226},"Definition von „Fertig“ (damit wir später nicht streiten)",{"id":356,"data":357,"type":239},"list_dod",{"items":358,"style":238},[359,360,361,362,363],"Ein frischer Browser kann sich bei \u002Fadmin anmelden und bleibt über Aktualisierungen hinweg angemeldet.","Die öffentliche Plattform rendert SSR-Inhalte von \u002Fapi\u002Fcontent, ohne nur für Administratoren zugängliche Endpunkte freizulegen.","NGINX leitet \u002F, \u002Fadmin, \u002Fapi in docker-compose korrekt weiter.","Die API kann neu gestartet werden, ohne das Session-Verhalten zu beschädigen (Entwicklungs-Speicher-Resets sind in Ordnung, aber sie muss graceful fehlschlagen).","Mindestens ein Prometheus-Scrape-Ziel existiert (auch wenn Grafana-Panels Platzhalter sind).",{"id":365,"data":366,"type":367},"hr_end",{},"delimiter",{"id":369,"data":370,"type":221},"p_next",{"text":371},"Wenn Sie einen neuen Endpunkt implementieren, entscheiden Sie zuerst: Ist er öffentlich, nur für Administratoren oder intern? Raten Sie nicht. Dann platzieren Sie ihn hinter dem richtigen Präfix und schützen ihn. Das ist das ganze Spiel.","2.29.1","Erkunden Sie eine praktische Monorepo-Architektur mit Next.js, Fastify, Prisma und NGINX, die reale Integration und den Workflow hervorhebt.","\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":381,"de":382,"sr":383,"es":384,"fr":385,"it":386,"ru":387,"zh":388},"\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",[390,393],{"id":45,"name":391,"slug":392},"Aaasaasa","aaasaasa",{"id":394,"name":395,"slug":396},48,"Fähigkeiten","capabilities",{"id":398,"login":399,"email":400,"displayName":401},"20","rooth8233","aleksandar@stajic.de","Aleksandar Stajić",[403,473],{"lang":7,"title":207,"content":209,"contentJson":404,"excerpt":373},{"time":211,"blocks":405,"version":372},[406,408,410,412,415,417,419,421,424,426,428,430,432,434,436,438,440,442,444,446,448,450,452,455,457,459,461,464,466,469,471],{"id":214,"data":407,"type":41},{"text":216,"level":39},{"id":218,"data":409,"type":221},{"text":220},{"id":223,"data":411,"type":41},{"text":225,"level":226},{"id":228,"data":413,"type":239},{"items":414,"style":238},[231,232,233,234,235,236,237],{"id":241,"data":416,"type":41},{"text":243,"level":226},{"id":245,"data":418,"type":248},{"code":247},{"id":250,"data":420,"type":41},{"text":252,"level":226},{"id":254,"data":422,"type":239},{"items":423,"style":238},[257,258,259,260],{"id":262,"data":425,"type":41},{"text":264,"level":226},{"id":266,"data":427,"type":221},{"text":268},{"id":270,"data":429,"type":248},{"code":272},{"id":274,"data":431,"type":41},{"text":276,"level":226},{"id":278,"data":433,"type":221},{"text":280},{"id":282,"data":435,"type":248},{"code":284},{"id":286,"data":437,"type":248},{"code":288},{"id":290,"data":439,"type":41},{"text":292,"level":226},{"id":294,"data":441,"type":221},{"text":296},{"id":298,"data":443,"type":248},{"code":300},{"id":302,"data":445,"type":41},{"text":304,"level":226},{"id":306,"data":447,"type":221},{"text":308},{"id":310,"data":449,"type":248},{"code":312},{"id":314,"data":451,"type":41},{"text":316,"level":226},{"id":318,"data":453,"type":239},{"items":454,"style":328},[321,322,323,324,325,326,327],{"id":330,"data":456,"type":41},{"text":332,"level":226},{"id":334,"data":458,"type":221},{"text":336},{"id":338,"data":460,"type":41},{"text":340,"level":226},{"id":342,"data":462,"type":239},{"items":463,"style":328},[345,346,347,348,349,350],{"id":352,"data":465,"type":41},{"text":354,"level":226},{"id":356,"data":467,"type":239},{"items":468,"style":238},[359,360,361,362,363],{"id":365,"data":470,"type":367},{},{"id":369,"data":472,"type":221},{"text":371},{"lang":474,"title":475,"content":476,"contentJson":477,"excerpt":594},"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":211,"blocks":478,"version":372},[479,482,485,488,498,501,503,506,513,516,519,521,524,527,529,531,534,537,539,542,545,547,550,560,563,566,569,578,581,589,591],{"id":214,"data":480,"type":41},{"text":481,"level":39},"Platform Monorepo: Next.js (Public + Admin) + Fastify API + Prisma DB + NGINX",{"id":218,"data":483,"type":221},{"text":484},"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":223,"data":486,"type":41},{"text":487,"level":226},"Constraints we’re not negotiating (for now)",{"id":228,"data":489,"type":239},{"items":490,"style":238},[491,492,493,494,495,496,497],"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":241,"data":499,"type":41},{"text":500,"level":226},"Repository layout",{"id":245,"data":502,"type":248},{"code":247},{"id":250,"data":504,"type":41},{"text":505,"level":226},"Trade-offs (the real ones)",{"id":254,"data":507,"type":239},{"items":508,"style":238},[509,510,511,512],"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":262,"data":514,"type":41},{"text":515,"level":226},"Routing contract (NGINX as the front door)",{"id":266,"data":517,"type":221},{"text":518},"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":270,"data":520,"type":248},{"code":272},{"id":274,"data":522,"type":41},{"text":523,"level":226},"API package (Fastify) — sessions and auth",{"id":278,"data":525,"type":221},{"text":526},"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":282,"data":528,"type":248},{"code":284},{"id":286,"data":530,"type":248},{"code":288},{"id":290,"data":532,"type":41},{"text":533,"level":226},"DB package (Prisma) — single client, shared types",{"id":294,"data":535,"type":221},{"text":536},"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":298,"data":538,"type":248},{"code":300},{"id":302,"data":540,"type":41},{"text":541,"level":226},"Admin app — cookie session and \u002Fme guard",{"id":306,"data":543,"type":221},{"text":544},"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":310,"data":546,"type":248},{"code":312},{"id":314,"data":548,"type":41},{"text":549,"level":226},"Step-by-step: admin login flow",{"id":318,"data":551,"type":239},{"items":552,"style":328},[553,554,555,556,557,558,559],"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":330,"data":561,"type":41},{"text":562,"level":226},"One thing that went wrong (so we don’t repeat it)",{"id":334,"data":564,"type":221},{"text":565},"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":338,"data":567,"type":41},{"text":568,"level":226},"Project plan (ship-first, refine later)",{"id":342,"data":570,"type":239},{"items":571,"style":328},[572,573,574,575,576,577],"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":352,"data":579,"type":41},{"text":580,"level":226},"Definition of done (so we don’t argue later)",{"id":356,"data":582,"type":239},{"items":583,"style":238},[584,585,586,587,588],"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":365,"data":590,"type":367},{},{"id":369,"data":592,"type":221},{"text":593},"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.","Post erfolgreich abgerufen",{"items":597,"source":619,"manualIds":620,"manualMatchedIds":621},[598,605,612],{"id":599,"slug":600,"title":601,"excerpt":602,"featuredImage":603,"publishedAt":604},"381","enterprise-grade-multi-tenant-architecture-for-an-international-platform","Unternehmensfähige mandantenfähige Architektur für eine internationale Plattform","Loving Rocks ist eine Hochzeitsplattform auf Unternehmensniveau, konzipiert mit einer echten Mehrmandantenarchitektur, isolierten Datenbanken pro Mandant und integrierter Internationalisierung für globale Skalierbarkeit, Sicherheit und langfristige Betriebsstabilität.","\u002Fuploads\u002F2026\u002F01\u002Fenterprise-grade-multi-tenant-architecture-for-an-international-platform-1769789121298-b6v7ak.webp","2026-01-30T12:04:00.000Z",{"id":606,"slug":607,"title":608,"excerpt":609,"featuredImage":610,"publishedAt":611},"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: Worauf sollten Sie 2026 aufbauen?","Der Agent-Stack von OpenAI hat sich im September 2026 geändert. Dieser Architekturleitfaden unterscheidet die Agents API, das Agents SDK, die Responses API und das Codex SDK nach Runtime-Ownership—sodass Teams die richtige Kontrollgrenze wählen können, anstatt Produktnamen zu vergleichen.","\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":613,"slug":614,"title":615,"excerpt":616,"featuredImage":617,"publishedAt":618},"2","multi-database-architecture","Multi-Datenbank-Architektur mit Prisma 7: Ein Deep Dive für Experten","Die Verwaltung komplexer Datenlandschaften erfordert moderne Architekturen. Prisma 7 bietet erweiterte Funktionen für die Multi-Datenbank-Integration und adressiert die Herausforderungen der Polyglot Persistence.","\u002Fuploads\u002F2014\u002F09\u002FSEO-Mobile-Webapplikation-Muenchen-www.stajic.de_1.webp","2025-10-31T04:31:00.000Z","fallback",[],[]]