[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"portal-settings:stajic:ru":3,"public-menus:all":38,"post:a-practical-monorepo-architecture-next-js-platform-admin-fastify-api-prisma-and-nginx:ru":205,"related:post:a-practical-monorepo-architecture-next-js-platform-admin-fastify-api-prisma-and-nginx:ru:1":597},{"statusCode":4,"data":5,"message":37},200,{"tenantId":6,"lang":7,"defaultLang":8,"siteUrl":9,"contactEmail":10,"brandName":11,"logoUrl":12,"siteName":11,"siteDescription":13,"ogImage":10,"robotsIndex":14,"socialLinks":10,"reservedSlugs":10,"seoPolicy":15},"stajic","ru","de","https:\u002F\u002Fstajic.de",null,"Stajic Platform","\u002FLogo_Planet.svg","Stajic Portal",true,{"branding":16,"relatedContent":17,"crossDomainLinks":18},{"logoUrl":12},{"enabled":14},[19,22,25,28,31,34],{"url":20,"label":21,"isActive":14,"showInFooter":14,"includeInSameAs":14},"https:\u002F\u002Ffigure.rocks","figure.rocks",{"url":23,"label":24,"isActive":14,"showInFooter":14,"includeInSameAs":14},"https:\u002F\u002Floving.rocks","loving.rocks",{"url":26,"label":27,"isActive":14,"showInFooter":14,"includeInSameAs":14},"https:\u002F\u002Fbazify.com","bazify.com",{"url":29,"label":30,"isActive":14,"showInFooter":14,"includeInSameAs":14},"https:\u002F\u002Fbazify.de","bazify.de",{"url":32,"label":33,"isActive":14,"showInFooter":14,"includeInSameAs":14},"https:\u002F\u002Fbazify.at","bazify.at",{"url":35,"label":36,"isActive":14,"showInFooter":14,"includeInSameAs":14},"https:\u002F\u002Fbazify.ba","bazify.ba","Portal settings resolved",[39,45],{"id":40,"name":41,"location":42,"isActive":14,"isDefault":43,"items":44},1,"main-navigation","header",false,[],{"id":46,"name":47,"location":48,"isActive":14,"isDefault":14,"items":49},4,"main-menu","sidebar",[50,66,79,93,103,118,133],{"id":51,"title":52,"url":60,"target":61,"icon":62,"isActive":14,"type":63,"productId":10,"categoryId":10,"shopCategoryId":10,"articleId":10,"pageId":64,"portfolioId":10,"children":65},"item-18",{"de":53,"en":54,"es":55,"fr":56,"it":54,"ru":57,"sr":58,"zh":59},"Startseite","Home","Inicio","Accueil","Главная","Почетна","首页","\u002Ffull-stack-web-developer-munich-performance-seo-and-maintainable-builds","_self","i-lucide-home","page",111,[],{"id":67,"title":68,"url":75,"target":61,"icon":76,"isActive":14,"type":63,"productId":10,"categoryId":10,"shopCategoryId":10,"articleId":10,"pageId":77,"portfolioId":10,"children":78},"item-22",{"de":69,"en":69,"es":70,"fr":69,"it":71,"ru":72,"sr":73,"zh":74},"Vision","Visión","Visione","Видение","Визија","想象","\u002Fueber-uns-webdesign-muenchen-webaplikation","i-lucide-eye",113,[],{"id":80,"title":81,"url":89,"target":61,"icon":90,"isActive":14,"type":63,"productId":10,"categoryId":10,"shopCategoryId":10,"articleId":10,"pageId":91,"portfolioId":10,"children":92},"item-19",{"de":82,"en":83,"es":84,"fr":83,"it":85,"ru":86,"sr":87,"zh":88},"Leistungen","Services","Servicios","Servizi","Услуги","Услуге","服务","\u002Fservices-dienstleistungen-muenchen","i-lucide-wrench",116,[],{"id":94,"title":95,"url":99,"target":61,"icon":100,"isActive":14,"type":63,"productId":10,"categoryId":10,"shopCategoryId":10,"articleId":10,"pageId":101,"portfolioId":10,"children":102},"item-23",{"de":96,"en":96,"es":96,"fr":96,"it":96,"ru":97,"sr":97,"zh":98},"Blog","Блог","博客","\u002Fblog","i-lucide-book-open",112,[],{"id":104,"title":105,"url":114,"target":61,"icon":115,"isActive":14,"type":63,"productId":10,"categoryId":10,"shopCategoryId":10,"articleId":10,"pageId":116,"portfolioId":10,"children":117},"item-32",{"de":106,"en":107,"es":108,"fr":109,"it":110,"ru":111,"sr":112,"zh":113},"Neue Technologien","New Technologies","Nuevas tecnologías","Nouvelles technologies","Nuove tecnologie","Новые технологии","Нове технологије","新技术！","\u002Fneue-webtechnologien","i-lucide-sparkles",122,[],{"id":119,"title":120,"url":129,"target":61,"icon":130,"isActive":14,"type":63,"productId":10,"categoryId":10,"shopCategoryId":10,"articleId":10,"pageId":131,"portfolioId":10,"children":132},"item-20",{"de":121,"en":122,"es":123,"fr":124,"it":125,"ru":126,"sr":127,"zh":128},"Kontakt","Contact us!","Contacto","Contact","Contatto","Контакт","Контактирајте нас","联系我们！","\u002Fcontact","i-lucide-mail",115,[],{"id":134,"title":135,"url":144,"target":61,"icon":145,"isActive":14,"type":63,"productId":10,"categoryId":10,"shopCategoryId":10,"articleId":10,"pageId":146,"portfolioId":10,"children":147},"item-21",{"de":136,"en":137,"es":138,"fr":139,"it":140,"ru":141,"sr":142,"zh":143},"Unsere Arbeit","Our Work","Nuestro trabajo","Nos réalisations","I nostri lavori","Наши работы","Наши радови","文件夹","\u002Fportfolio","i-lucide-briefcase",114,[148,161,175,181,193],{"id":149,"title":150,"url":144,"target":61,"icon":159,"isActive":14,"type":63,"productId":10,"categoryId":10,"shopCategoryId":10,"articleId":10,"pageId":146,"portfolioId":10,"children":160},"item-24",{"de":151,"en":152,"es":153,"fr":154,"it":155,"ru":156,"sr":157,"zh":158},"Alle Projekte","All Projects","Todos los proyectos","Tous les projets","Tutti i progetti","Все проекты","Сви пројекти","所有项目","i-lucide-grid-3x3",[],{"id":162,"title":163,"url":171,"target":61,"icon":172,"isActive":14,"type":173,"productId":10,"categoryId":10,"shopCategoryId":10,"articleId":10,"pageId":10,"portfolioId":10,"children":174},"item-29",{"de":164,"en":165,"es":166,"fr":167,"it":168,"ru":169,"sr":170,"zh":143},"Local Roots, Global Reach","Local Roots - Global Reach","Empresa local ","Entreprise locale","Azienda locale","Местная компания","Локално предузеће глобално тржиште","\u002Fportfolio\u002Flocal-roots-global-reach-communication-media-systems-for-modern-business","i-lucide-folder","custom",[],{"id":176,"title":177,"url":179,"target":61,"icon":172,"isActive":14,"type":173,"productId":10,"categoryId":10,"shopCategoryId":10,"articleId":10,"pageId":10,"portfolioId":10,"children":180},"item-28",{"de":178,"en":178,"es":178,"fr":178,"it":178,"ru":178,"sr":178,"zh":178},"Solr Suggester","\u002Fportfolio\u002Fsolr-fuzzy-suggester-und-solr-infix-suggester-abfrage-ueber-ajax-und-filterung",[],{"id":182,"title":183,"url":191,"target":61,"icon":172,"isActive":14,"type":173,"productId":10,"categoryId":10,"shopCategoryId":10,"articleId":10,"pageId":10,"portfolioId":10,"children":192},"item-27",{"de":184,"en":185,"es":186,"fr":187,"it":188,"ru":189,"sr":190,"zh":185},"Firmenwebseite SEO","Company Website SEO","Sitio web corporativo SEO","Site web d’entreprise SEO","Sito web aziendale SEO","Корпоративный сайт SEO","Пословна веб-страница SEO","\u002Fportfolio\u002Fseo-sem-branding-mobile-webseite-muenchen",[],{"id":194,"title":195,"url":203,"target":61,"icon":172,"isActive":14,"type":173,"productId":10,"categoryId":10,"shopCategoryId":10,"articleId":10,"pageId":10,"portfolioId":10,"children":204},"item-31",{"de":196,"en":197,"es":198,"fr":199,"it":200,"ru":201,"sr":202,"zh":197},"Digitalisierungsportal","Digitalization Portal","Portal de digitalización","Portail de numérisation","Portale di digitalizzazione","Портал цифровизации","Портал за дигитализацију","\u002Fportfolio\u002Fdigitalisierungsportal-archiv-museum-bibliothek-ead-lido-mets-mods",[],{"statusCode":4,"data":206,"message":596},{"id":207,"title":208,"slug":209,"content":210,"contentJson":211,"excerpt":374,"featuredImage":375,"featuredImageAlt":376,"featuredImageCaption":10,"featuredImageTitle":10,"featuredImageCopyright":10,"featuredImageAuthor":10,"featuredImageSourceUrl":10,"featuredImageLicense":10,"featuredImageIsAiGenerated":43,"status":377,"publishedAt":378,"createdAt":379,"updatedAt":380,"seoLocalePaths":381,"categories":390,"author":398,"translations":403},"382","Практическая архитектура монорепозитория с Next.js, Fastify, Prisma и NGINX","a-practical-monorepo-architecture-next-js-platform-admin-fastify-api-prisma-and-nginx","\u003Ch1>Монорепозиторий платформы: Next.js (Публичный + Админ) + Fastify API + Prisma DB + NGINX\u003C\u002Fh1>\n\u003Cp>Этот документ описывает текущую целевую архитектуру монорепозитория с публичным сайтом на Next.js, административным приложением на Next.js с сессиями на основе куки, Fastify API и пакетом базы данных на основе Prisma. Он написан для членов команды, которые будут работать с несколькими пакетами и нуждаются в предсказуемых границах.\u003C\u002Fp>\n\u003Ch2>Ограничения, которые мы не обсуждаем (пока)\u003C\u002Fh2>\n\u003Cul>\u003Cli>Публичный сайт должен быть SSR (Next.js App Router). Никаких сокращений только для SPA.\u003C\u002Fli>\u003Cli>Публичный сайт взаимодействует с бэкендом только через публичные маршруты API по адресу \u002Fapi (например, \u002Fapi\u002Fcontent, \u002Fapi\u002Fmedia).\u003C\u002Fli>\u003Cli>Админ использует аутентификацию на основе куки-сессий. Куки только для HTTP. Никаких токенов в localStorage.\u003C\u002Fli>\u003Cli>API — Fastify. Сессии через @fastify\u002Fsession. Хранилище «fakeRedis» для разработки; Redis в продакшене позже.\u003C\u002Fli>\u003Cli>Доступ к БД осуществляется только через @platform\u002Fdb. Никаких прямых клиентов Prisma, разбросанных по приложениям.\u003C\u002Fli>\u003Cli>NGINX завершает TLS и маршрутизирует \u002F, \u002Fadmin и \u002Fapi. Один домен подходит; поддомен необязателен позже.\u003C\u002Fli>\u003Cli>Мониторинг существует с первого дня (сбор данных Prometheus + заглушки для дашбордов Grafana).\u003C\u002Fli>\u003C\u002Ful>\n\u003Ch2>Структура репозитория\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>Компромиссы (реальные)\u003C\u002Fh2>\n\u003Cul>\u003Cli>Куки-сессии скучны. Это особенность. Они работают за прокси и удерживают логику аутентификации вне фронтенда. Недостаток: вы должны правильно настроить SameSite\u002FSecure\u002Fpath, иначе будете гоняться за «случайными» ошибками входа.\u003C\u002Fli>\u003Cli>Единый интерфейс \u002Fapi чист, но это также означает, что вы должны строго разделять публичные и только для администратора эндпоинты. Не допускайте утечки административных эндпоинтов просто потому, что это удобно.\u003C\u002Fli>\u003Cli>Prisma может быть «готовой к работе с несколькими БД» в смысле замены провайдеров. Это не волшебный мост между SQL и Mongo с единой схемой. Если кто-то говорит, что это так, они еще не выпустили продукт.\u003C\u002Fli>\u003Cli>Next.js SSR хорош для SEO и первой загрузки. Он также может перегрузить API, если вы намеренно не настроите кеширование и ревалидацию.\u003C\u002Fli>\u003C\u002Ful>\n\u003Ch2>Контракт маршрутизации (NGINX как входная дверь)\u003C\u002Fh2>\n\u003Cp>Пока мы маршрутизируем все через один домен. Это проще для отладки. Если позже мы перенесем админку на поддомен, мы пересмотрим область действия куки и предположения о CSRF.\u003C\u002Fp>\n\u003Cpre class=\"code-block\">\u003Ccode># INFRASTRUCTURE\u002Fnginx\u002Fsites\u002Fapp.conf\nserver {\n  listen 80;\n  server_name yourdomain.com;\n\n  location \u002Fapi\u002F {\n    proxy_pass http:\u002F\u002Fapi:4000\u002F;\n    proxy_set_header Host $host;\n    proxy_set_header X-Forwarded-Proto $scheme;\n    proxy_set_header X-Real-IP $remote_addr;\n  }\n\n  location \u002Fadmin\u002F {\n    proxy_pass http:\u002F\u002Fadmin:3001\u002F;\n    proxy_set_header Host $host;\n  }\n\n  location \u002F {\n    proxy_pass http:\u002F\u002Fplatform:3000\u002F;\n    proxy_set_header Host $host;\n  }\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2>Пакет API (Fastify) — сессии и аутентификация\u003C\u002Fh2>\n\u003Cp>API владеет аутентификацией и состоянием сессии. Приложения никогда не выпускают токены. Они просто отправляют куки обратно. Сохраняйте полезную нагрузку сессии небольшой. ID пользователя + роль. Вот и все.\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>Пакет БД (Prisma) — один клиент, общие типы\u003C\u002Fh2>\n\u003Cp>Мы предоставляем один PrismaClient из @platform\u002Fdb. API импортирует только его. Если вы создадите второй клиент Prisma в приложении, потому что это «быстрее для прототипирования», вы просто создадите будущий инцидент.\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>Админ-приложение — куки-сессия и защита \u002Fme\u003C\u002Fh2>\n\u003Cp>Админ — это обычное приложение Next.js App Router. Единственная особенность заключается в том, что оно должно рассматривать API как источник истины и всегда включать куки при вызове \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>Пошагово: процесс входа администратора\u003C\u002Fh2>\n\u003Col>\u003Cli>Браузер отправляет учетные данные: POST \u002Fapi\u002Flogin\u003C\u002Fli>\u003Cli>NGINX перенаправляет \u002Fapi\u002F* на Fastify\u003C\u002Fli>\u003Cli>Fastify проверяет пользователя по БД (Prisma)\u003C\u002Fli>\u003Cli>Fastify записывает сессию в хранилище fakeRedis (разработка) и возвращает Set-Cookie: sid=…\u003C\u002Fli>\u003Cli>Браузер сохраняет куки (только HTTP)\u003C\u002Fli>\u003Cli>Серверные компоненты админки вызывают GET \u002Fapi\u002Fme с прикрепленным куки\u003C\u002Fli>\u003Cli>Админка отображает дашборд только если \u002Fme возвращает ok=true\u003C\u002Fli>\u003C\u002Fol>\n\u003Ch2>Одна вещь, которая пошла не так (чтобы мы не повторяли)\u003C\u002Fh2>\n\u003Cp>У нас был цикл входа, хотя \u002Fapi\u002Flogin возвращал 200. Куки никогда не отправлялись обратно на \u002Fapi\u002Fme. Причина была скучной: несоответствие пути куки и маршрутизации прокси. Мы случайно установили куки для \u002Fadmin, но \u002Fapi\u002Fme находится по адресу \u002Fapi. Браузер сделал именно то, что должен был. Он не отправил куки. Решение заключалось в установке пути куки на «\u002F» и подтверждении того, что NGINX не переписывал пути таким образом, чтобы это нарушало работу.\u003C\u002Fp>\n\u003Ch2>План проекта (сначала выпуск, потом доработка)\u003C\u002Fh2>\n\u003Col>\u003Cli>Фаза 0: настройка монорепозитория (Turbo, TS config, импорты общих пакетов). Сохраняйте его минималистичным.\u003C\u002Fli>\u003Cli>Фаза 1: Эндпоинты аутентификации\u002Fсессий API: \u002Flogin, \u002Flogout, \u002Fme. Добавить базовое ограничение скорости на \u002Flogin.\u003C\u002Fli>\u003Cli>Фаза 2: MVP админки: экран входа + серверная защита + минимальная оболочка дашборда.\u003C\u002Fli>\u003Cli>Фаза 3: Эндпоинты для чтения контента для публичной платформы: \u002Fapi\u002Fcontent\u002F* (сначала только чтение).\u003C\u002Fli>\u003Cli>Фаза 4: CRUD контента в админке + загрузка медиа (начните с простого, затем замените хранилище).\u003C\u002Fli>\u003Cli>Фаза 5: Мониторинг и усиление безопасности: эндпоинт метрик Prometheus, дашборды Grafana, заголовки, резервные копии.\u003C\u002Fli>\u003C\u002Fol>\n\u003Ch2>Определение готовности (чтобы потом не спорить)\u003C\u002Fh2>\n\u003Cul>\u003Cli>Новый браузер может войти в \u002Fadmin и оставаться авторизованным при обновлениях страницы.\u003C\u002Fli>\u003Cli>Публичная платформа отображает SSR контент из \u002Fapi\u002Fcontent, не раскрывая эндпоинты только для администратора.\u003C\u002Fli>\u003Cli>NGINX корректно маршрутизирует \u002F, \u002Fadmin, \u002Fapi в docker-compose.\u003C\u002Fli>\u003Cli>API может перезапускаться без нарушения поведения сессии (сбросы хранилища разработки допустимы, но он должен завершаться корректно).\u003C\u002Fli>\u003Cli>Существует хотя бы одна цель сбора метрик Prometheus (даже если панели Grafana являются заглушками).\u003C\u002Fli>\u003C\u002Ful>\n\u003Chr class=\"my-8\">\n\u003Cp>Если вы реализуете новый эндпоинт, сначала решите: он публичный, только для администратора или внутренний. Не гадайте. Затем поместите его за правильный префикс и защитите. В этом вся суть.\u003C\u002Fp>",{"time":212,"blocks":213,"version":373},1769827200000,[214,218,223,228,241,245,250,254,262,266,270,274,278,282,286,290,294,298,302,306,310,314,318,330,334,338,342,352,356,365,369],{"id":215,"data":216,"type":42},"h1_arch",{"text":217,"level":40},"Монорепозиторий платформы: Next.js (Публичный + Админ) + Fastify API + Prisma DB + NGINX",{"id":219,"data":220,"type":222},"p_scope",{"text":221},"Этот документ описывает текущую целевую архитектуру монорепозитория с публичным сайтом на Next.js, административным приложением на Next.js с сессиями на основе куки, Fastify API и пакетом базы данных на основе Prisma. Он написан для членов команды, которые будут работать с несколькими пакетами и нуждаются в предсказуемых границах.","paragraph",{"id":224,"data":225,"type":42},"h2_constraints",{"text":226,"level":227},"Ограничения, которые мы не обсуждаем (пока)",2,{"id":229,"data":230,"type":240},"list_constraints",{"items":231,"style":239},[232,233,234,235,236,237,238],"Публичный сайт должен быть SSR (Next.js App Router). Никаких сокращений только для SPA.","Публичный сайт взаимодействует с бэкендом только через публичные маршруты API по адресу \u002Fapi (например, \u002Fapi\u002Fcontent, \u002Fapi\u002Fmedia).","Админ использует аутентификацию на основе куки-сессий. Куки только для HTTP. Никаких токенов в localStorage.","API — Fastify. Сессии через @fastify\u002Fsession. Хранилище «fakeRedis» для разработки; Redis в продакшене позже.","Доступ к БД осуществляется только через @platform\u002Fdb. Никаких прямых клиентов Prisma, разбросанных по приложениям.","NGINX завершает TLS и маршрутизирует \u002F, \u002Fadmin и \u002Fapi. Один домен подходит; поддомен необязателен позже.","Мониторинг существует с первого дня (сбор данных Prometheus + заглушки для дашбордов Grafana).","unordered","list",{"id":242,"data":243,"type":42},"h2_layout",{"text":244,"level":227},"Структура репозитория",{"id":246,"data":247,"type":249},"code_tree",{"code":248},"repo\u002F\n  apps\u002F\n    platform\u002F        # public site (SSR)\n    admin\u002F           # admin UI (cookie session)\n  packages\u002F\n    api\u002F             # Fastify backend (auth\u002Fusers\u002Fcontent\u002Fmedia)\n    db\u002F              # Prisma client + migrations\n    shared\u002F          # shared types, UI bits, utilities\n  INFRASTRUCTURE\u002F\n    nginx\u002F\n      nginx.conf\n      sites\u002F\n        app.conf\n    monitoring\u002F\n      prometheus.yml\n      grafana\u002F\n  docker-compose.yml\n  turbo.json\n  package.json","code",{"id":251,"data":252,"type":42},"h2_tradeoffs",{"text":253,"level":227},"Компромиссы (реальные)",{"id":255,"data":256,"type":240},"list_tradeoffs",{"items":257,"style":239},[258,259,260,261],"Куки-сессии скучны. Это особенность. Они работают за прокси и удерживают логику аутентификации вне фронтенда. Недостаток: вы должны правильно настроить SameSite\u002FSecure\u002Fpath, иначе будете гоняться за «случайными» ошибками входа.","Единый интерфейс \u002Fapi чист, но это также означает, что вы должны строго разделять публичные и только для администратора эндпоинты. Не допускайте утечки административных эндпоинтов просто потому, что это удобно.","Prisma может быть «готовой к работе с несколькими БД» в смысле замены провайдеров. Это не волшебный мост между SQL и Mongo с единой схемой. Если кто-то говорит, что это так, они еще не выпустили продукт.","Next.js SSR хорош для SEO и первой загрузки. Он также может перегрузить API, если вы намеренно не настроите кеширование и ревалидацию.",{"id":263,"data":264,"type":42},"h2_routing",{"text":265,"level":227},"Контракт маршрутизации (NGINX как входная дверь)",{"id":267,"data":268,"type":222},"p_routing",{"text":269},"Пока мы маршрутизируем все через один домен. Это проще для отладки. Если позже мы перенесем админку на поддомен, мы пересмотрим область действия куки и предположения о CSRF.",{"id":271,"data":272,"type":249},"code_nginx",{"code":273},"# INFRASTRUCTURE\u002Fnginx\u002Fsites\u002Fapp.conf\nserver {\n  listen 80;\n  server_name yourdomain.com;\n\n  location \u002Fapi\u002F {\n    proxy_pass http:\u002F\u002Fapi:4000\u002F;\n    proxy_set_header Host $host;\n    proxy_set_header X-Forwarded-Proto $scheme;\n    proxy_set_header X-Real-IP $remote_addr;\n  }\n\n  location \u002Fadmin\u002F {\n    proxy_pass http:\u002F\u002Fadmin:3001\u002F;\n    proxy_set_header Host $host;\n  }\n\n  location \u002F {\n    proxy_pass http:\u002F\u002Fplatform:3000\u002F;\n    proxy_set_header Host $host;\n  }\n}",{"id":275,"data":276,"type":42},"h2_api",{"text":277,"level":227},"Пакет API (Fastify) — сессии и аутентификация",{"id":279,"data":280,"type":222},"p_api",{"text":281},"API владеет аутентификацией и состоянием сессии. Приложения никогда не выпускают токены. Они просто отправляют куки обратно. Сохраняйте полезную нагрузку сессии небольшой. ID пользователя + роль. Вот и все.",{"id":283,"data":284,"type":249},"code_server",{"code":285},"\u002F\u002F packages\u002Fapi\u002Fsrc\u002Fserver.ts\nimport Fastify from \"fastify\";\nimport cookie from \"@fastify\u002Fcookie\";\nimport session from \"@fastify\u002Fsession\";\nimport { buildSessionStore } from \".\u002FsessionStore\";\nimport { authRoutes } from \".\u002Froutes\u002Fauth\";\nimport { meRoutes } from \".\u002Froutes\u002Fme\";\n\nexport async function buildServer() {\n  const app = Fastify({ logger: true });\n\n  await app.register(cookie);\n  await app.register(session, {\n    secret: process.env.SESSION_SECRET || \"dev-secret-change-me\",\n    cookieName: \"sid\",\n    cookie: {\n      httpOnly: true,\n      secure: process.env.NODE_ENV === \"production\",\n      sameSite: \"lax\",\n      path: \"\u002F\"\n    },\n    store: buildSessionStore(),\n    saveUninitialized: false\n  });\n\n  app.register(authRoutes, { prefix: \"\u002Fapi\" });\n  app.register(meRoutes, { prefix: \"\u002Fapi\" });\n\n  return app;\n}\n\n\u002F\u002F packages\u002Fapi\u002Fsrc\u002Findex.ts\nimport { buildServer } from \".\u002Fserver\";\n\n(async () => {\n  const app = await buildServer();\n  const port = Number(process.env.PORT || 4000);\n  await app.listen({ port, host: \"0.0.0.0\" });\n})();",{"id":287,"data":288,"type":249},"code_session_store",{"code":289},"\u002F\u002F packages\u002Fapi\u002Fsrc\u002FsessionStore.ts\nimport type { SessionStore } from \"@fastify\u002Fsession\";\n\nexport function buildSessionStore(): SessionStore {\n  const mem = new Map\u003Cstring, { value: any; expiresAt: number }>();\n\n  return {\n    get: (sid, cb) => {\n      const hit = mem.get(sid);\n      if (!hit) return cb(null, null);\n      if (Date.now() > hit.expiresAt) {\n        mem.delete(sid);\n        return cb(null, null);\n      }\n      cb(null, hit.value);\n    },\n    set: (sid, session, cb) => {\n      const ttlMs = 1000 * 60 * 60 * 8;\n      mem.set(sid, { value: session, expiresAt: Date.now() + ttlMs });\n      cb(null);\n    },\n    destroy: (sid, cb) => {\n      mem.delete(sid);\n      cb(null);\n    }\n  };\n}",{"id":291,"data":292,"type":42},"h2_db",{"text":293,"level":227},"Пакет БД (Prisma) — один клиент, общие типы",{"id":295,"data":296,"type":222},"p_db",{"text":297},"Мы предоставляем один PrismaClient из @platform\u002Fdb. API импортирует только его. Если вы создадите второй клиент Prisma в приложении, потому что это «быстрее для прототипирования», вы просто создадите будущий инцидент.",{"id":299,"data":300,"type":249},"code_db",{"code":301},"\u002F\u002F packages\u002Fdb\u002Fsrc\u002Findex.ts\nimport { PrismaClient } from \"@prisma\u002Fclient\";\n\nexport const db = new PrismaClient();",{"id":303,"data":304,"type":42},"h2_admin",{"text":305,"level":227},"Админ-приложение — куки-сессия и защита \u002Fme",{"id":307,"data":308,"type":222},"p_admin",{"text":309},"Админ — это обычное приложение Next.js App Router. Единственная особенность заключается в том, что оно должно рассматривать API как источник истины и всегда включать куки при вызове \u002Fapi\u002Fme.",{"id":311,"data":312,"type":249},"code_admin_guard",{"code":313},"\u002F\u002F apps\u002Fadmin\u002Fapp\u002F(admin)\u002Flayout.tsx\nimport { cookies } from \"next\u002Fheaders\";\n\nasync function getMe() {\n  const cookieHeader = cookies().toString();\n  const res = await fetch(`${process.env.ADMIN_BASE_URL}\u002Fapi\u002Fme`, {\n    headers: { cookie: cookieHeader },\n    cache: \"no-store\"\n  });\n  return res.json();\n}\n\nexport default async function AdminLayout({ children }: { children: React.ReactNode }) {\n  const me = await getMe();\n  if (!me?.ok) {\n    return (\n      \u003Chtml>\n        \u003Cbody>\n          \u003Cp>Unauthorized. Go to \u002Fadmin\u002Flogin.\u003C\u002Fp>\n        \u003C\u002Fbody>\n      \u003C\u002Fhtml>\n    );\n  }\n\n  return (\n    \u003Chtml>\n      \u003Cbody>{children}\u003C\u002Fbody>\n    \u003C\u002Fhtml>\n  );\n}",{"id":315,"data":316,"type":42},"h2_flow",{"text":317,"level":227},"Пошагово: процесс входа администратора",{"id":319,"data":320,"type":240},"list_flow",{"items":321,"style":329},[322,323,324,325,326,327,328],"Браузер отправляет учетные данные: POST \u002Fapi\u002Flogin","NGINX перенаправляет \u002Fapi\u002F* на Fastify","Fastify проверяет пользователя по БД (Prisma)","Fastify записывает сессию в хранилище fakeRedis (разработка) и возвращает Set-Cookie: sid=…","Браузер сохраняет куки (только HTTP)","Серверные компоненты админки вызывают GET \u002Fapi\u002Fme с прикрепленным куки","Админка отображает дашборд только если \u002Fme возвращает ok=true","ordered",{"id":331,"data":332,"type":42},"h2_went_wrong",{"text":333,"level":227},"Одна вещь, которая пошла не так (чтобы мы не повторяли)",{"id":335,"data":336,"type":222},"p_went_wrong",{"text":337},"У нас был цикл входа, хотя \u002Fapi\u002Flogin возвращал 200. Куки никогда не отправлялись обратно на \u002Fapi\u002Fme. Причина была скучной: несоответствие пути куки и маршрутизации прокси. Мы случайно установили куки для \u002Fadmin, но \u002Fapi\u002Fme находится по адресу \u002Fapi. Браузер сделал именно то, что должен был. Он не отправил куки. Решение заключалось в установке пути куки на «\u002F» и подтверждении того, что NGINX не переписывал пути таким образом, чтобы это нарушало работу.",{"id":339,"data":340,"type":42},"h2_project_plan",{"text":341,"level":227},"План проекта (сначала выпуск, потом доработка)",{"id":343,"data":344,"type":240},"list_plan",{"items":345,"style":329},[346,347,348,349,350,351],"Фаза 0: настройка монорепозитория (Turbo, TS config, импорты общих пакетов). Сохраняйте его минималистичным.","Фаза 1: Эндпоинты аутентификации\u002Fсессий API: \u002Flogin, \u002Flogout, \u002Fme. Добавить базовое ограничение скорости на \u002Flogin.","Фаза 2: MVP админки: экран входа + серверная защита + минимальная оболочка дашборда.","Фаза 3: Эндпоинты для чтения контента для публичной платформы: \u002Fapi\u002Fcontent\u002F* (сначала только чтение).","Фаза 4: CRUD контента в админке + загрузка медиа (начните с простого, затем замените хранилище).","Фаза 5: Мониторинг и усиление безопасности: эндпоинт метрик Prometheus, дашборды Grafana, заголовки, резервные копии.",{"id":353,"data":354,"type":42},"h2_definition_done",{"text":355,"level":227},"Определение готовности (чтобы потом не спорить)",{"id":357,"data":358,"type":240},"list_dod",{"items":359,"style":239},[360,361,362,363,364],"Новый браузер может войти в \u002Fadmin и оставаться авторизованным при обновлениях страницы.","Публичная платформа отображает SSR контент из \u002Fapi\u002Fcontent, не раскрывая эндпоинты только для администратора.","NGINX корректно маршрутизирует \u002F, \u002Fadmin, \u002Fapi в docker-compose.","API может перезапускаться без нарушения поведения сессии (сбросы хранилища разработки допустимы, но он должен завершаться корректно).","Существует хотя бы одна цель сбора метрик Prometheus (даже если панели Grafana являются заглушками).",{"id":366,"data":367,"type":368},"hr_end",{},"delimiter",{"id":370,"data":371,"type":222},"p_next",{"text":372},"Если вы реализуете новый эндпоинт, сначала решите: он публичный, только для администратора или внутренний. Не гадайте. Затем поместите его за правильный префикс и защитите. В этом вся суть.","2.29.1","Исследуйте практическую архитектуру монорепозитория с использованием Next.js, Fastify, Prisma и NGINX, подчеркивающую реальную интеграцию и рабочий процесс.","\u002Fuploads\u002F2026\u002F01\u002Fa-practical-monorepo-architecture-next-js-platform-admin-fastify-api-prisma-and-nginx-1769885526116-q1100n.webp","a-practical-monorepo-architecture-next-js-platform-admin-fastify-api-prisma-and-nginx-1769885526116-q1100n","PUBLISHED","2026-01-31T08:18:00.000Z","2026-01-31T17:18:53.110Z","2026-02-20T20:41:28.972Z",{"en":382,"de":383,"sr":384,"es":385,"fr":386,"it":387,"ru":388,"zh":389},"\u002Fblog\u002Fa-practical-monorepo-architecture-next-js-platform-admin-fastify-api-prisma-and-nginx","\u002Fde\u002Fblog\u002Fa-practical-monorepo-architecture-next-js-platform-admin-fastify-api-prisma-and-nginx","\u002Fsr\u002Fblog\u002Fa-practical-monorepo-architecture-next-js-platform-admin-fastify-api-prisma-and-nginx","\u002Fes\u002Fblog\u002Fa-practical-monorepo-architecture-next-js-platform-admin-fastify-api-prisma-and-nginx","\u002Ffr\u002Fblog\u002Fa-practical-monorepo-architecture-next-js-platform-admin-fastify-api-prisma-and-nginx","\u002Fit\u002Fblog\u002Fa-practical-monorepo-architecture-next-js-platform-admin-fastify-api-prisma-and-nginx","\u002Fru\u002Fblog\u002Fa-practical-monorepo-architecture-next-js-platform-admin-fastify-api-prisma-and-nginx","\u002Fzh\u002Fblog\u002Fa-practical-monorepo-architecture-next-js-platform-admin-fastify-api-prisma-and-nginx",[391,394],{"id":46,"name":392,"slug":393},"Aaasaasa","aaasaasa",{"id":395,"name":396,"slug":397},48,"Возможности","capabilities",{"id":399,"login":400,"email":401,"displayName":402},"20","rooth8233","aleksandar@stajic.de","Aleksandar Stajić",[404,526],{"lang":405,"title":406,"content":407,"contentJson":408,"excerpt":525},"en","A Practical Monorepo Architecture with Next.js, Fastify, Prisma, and NGINX","{\"time\":1769827200000,\"blocks\":[{\"id\":\"h1_arch\",\"data\":{\"text\":\"Platform Monorepo: Next.js (Public + Admin) + Fastify API + Prisma DB + NGINX\",\"level\":1},\"type\":\"header\"},{\"id\":\"p_scope\",\"data\":{\"text\":\"This document describes the current target architecture for a monorepo with a public Next.js site, an admin Next.js app with cookie sessions, a Fastify API, and a Prisma-based DB package. It’s written for teammates who will touch multiple packages and need predictable boundaries.\"},\"type\":\"paragraph\"},{\"id\":\"h2_constraints\",\"data\":{\"text\":\"Constraints we’re not negotiating (for now)\",\"level\":2},\"type\":\"header\"},{\"id\":\"list_constraints\",\"data\":{\"items\":[\"Public site must be SSR (Next.js App Router). No SPA-only shortcuts.\",\"Public site talks to the backend only via public API routes under \u002Fapi (e.g., \u002Fapi\u002Fcontent, \u002Fapi\u002Fmedia).\",\"Admin uses cookie-based session auth. HTTP-only cookie. No localStorage tokens.\",\"API is Fastify. Sessions via @fastify\u002Fsession. “fakeRedis” store for development; Redis in production later.\",\"DB access is via @platform\u002Fdb only. No direct Prisma clients scattered in apps.\",\"NGINX terminates TLS and routes \u002F, \u002Fadmin, and \u002Fapi. One domain is fine; subdomain is optional later.\",\"Monitoring exists from day one (Prometheus scrape + Grafana dashboard placeholders).\"],\"style\":\"unordered\"},\"type\":\"list\"},{\"id\":\"h2_layout\",\"data\":{\"text\":\"Repository layout\",\"level\":2},\"type\":\"header\"},{\"id\":\"code_tree\",\"data\":{\"code\":\"repo\u002F\\n  apps\u002F\\n    platform\u002F        # public site (SSR)\\n    admin\u002F           # admin UI (cookie session)\\n  packages\u002F\\n    api\u002F             # Fastify backend (auth\u002Fusers\u002Fcontent\u002Fmedia)\\n    db\u002F              # Prisma client + migrations\\n    shared\u002F          # shared types, UI bits, utilities\\n  INFRASTRUCTURE\u002F\\n    nginx\u002F\\n      nginx.conf\\n      sites\u002F\\n        app.conf\\n    monitoring\u002F\\n      prometheus.yml\\n      grafana\u002F\\n  docker-compose.yml\\n  turbo.json\\n  package.json\"},\"type\":\"code\"},{\"id\":\"h2_tradeoffs\",\"data\":{\"text\":\"Trade-offs (the real ones)\",\"level\":2},\"type\":\"header\"},{\"id\":\"list_tradeoffs\",\"data\":{\"items\":[\"Cookie sessions are boring. That’s a feature. They work behind proxies and keep auth logic out of the frontend. The downside: you must get SameSite\u002FSecure\u002Fpath right, or you’ll chase “random” login bugs.\",\"One \u002Fapi surface is clean, but it also means you must be strict about what is public vs admin-only. Don’t leak admin endpoints just because it’s convenient.\",\"Prisma can be “multi-db ready” in the sense of swapping providers. It is not a magic bridge between SQL and Mongo with a single schema. If someone says it is, they haven’t shipped it.\",\"Next.js SSR is good for SEO and first load. It can also slam the API if you don’t set caching and revalidation intentionally.\"],\"style\":\"unordered\"},\"type\":\"list\"},{\"id\":\"h2_routing\",\"data\":{\"text\":\"Routing contract (NGINX as the front door)\",\"level\":2},\"type\":\"header\"},{\"id\":\"p_routing\",\"data\":{\"text\":\"We route everything through one domain for now. It’s simpler to debug. If we move admin to a subdomain later, we’ll revisit cookie scope and CSRF assumptions.\"},\"type\":\"paragraph\"},{\"id\":\"code_nginx\",\"data\":{\"code\":\"# INFRASTRUCTURE\u002Fnginx\u002Fsites\u002Fapp.conf\\nserver {\\n  listen 80;\\n  server_name yourdomain.com;\\n\\n  location \u002Fapi\u002F {\\n    proxy_pass http:\u002F\u002Fapi:4000\u002F;\\n    proxy_set_header Host $host;\\n    proxy_set_header X-Forwarded-Proto $scheme;\\n    proxy_set_header X-Real-IP $remote_addr;\\n  }\\n\\n  location \u002Fadmin\u002F {\\n    proxy_pass http:\u002F\u002Fadmin:3001\u002F;\\n    proxy_set_header Host $host;\\n  }\\n\\n  location \u002F {\\n    proxy_pass http:\u002F\u002Fplatform:3000\u002F;\\n    proxy_set_header Host $host;\\n  }\\n}\"},\"type\":\"code\"},{\"id\":\"h2_api\",\"data\":{\"text\":\"API package (Fastify) — sessions and auth\",\"level\":2},\"type\":\"header\"},{\"id\":\"p_api\",\"data\":{\"text\":\"The API owns authentication and session state. The apps never mint tokens. They just send cookies back. Keep the session payload small. User id + role. That’s it.\"},\"type\":\"paragraph\"},{\"id\":\"code_server\",\"data\":{\"code\":\"\u002F\u002F packages\u002Fapi\u002Fsrc\u002Fserver.ts\\nimport Fastify from \\\"fastify\\\";\\nimport cookie from \\\"@fastify\u002Fcookie\\\";\\nimport session from \\\"@fastify\u002Fsession\\\";\\nimport { buildSessionStore } from \\\".\u002FsessionStore\\\";\\nimport { authRoutes } from \\\".\u002Froutes\u002Fauth\\\";\\nimport { meRoutes } from \\\".\u002Froutes\u002Fme\\\";\\n\\nexport async function buildServer() {\\n  const app = Fastify({ logger: true });\\n\\n  await app.register(cookie);\\n  await app.register(session, {\\n    secret: process.env.SESSION_SECRET || \\\"dev-secret-change-me\\\",\\n    cookieName: \\\"sid\\\",\\n    cookie: {\\n      httpOnly: true,\\n      secure: process.env.NODE_ENV === \\\"production\\\",\\n      sameSite: \\\"lax\\\",\\n      path: \\\"\u002F\\\"\\n    },\\n    store: buildSessionStore(),\\n    saveUninitialized: false\\n  });\\n\\n  app.register(authRoutes, { prefix: \\\"\u002Fapi\\\" });\\n  app.register(meRoutes, { prefix: \\\"\u002Fapi\\\" });\\n\\n  return app;\\n}\\n\\n\u002F\u002F packages\u002Fapi\u002Fsrc\u002Findex.ts\\nimport { buildServer } from \\\".\u002Fserver\\\";\\n\\n(async () => {\\n  const app = await buildServer();\\n  const port = Number(process.env.PORT || 4000);\\n  await app.listen({ port, host: \\\"0.0.0.0\\\" });\\n})();\"},\"type\":\"code\"},{\"id\":\"code_session_store\",\"data\":{\"code\":\"\u002F\u002F packages\u002Fapi\u002Fsrc\u002FsessionStore.ts\\nimport type { SessionStore } from \\\"@fastify\u002Fsession\\\";\\n\\nexport function buildSessionStore(): SessionStore {\\n  const mem = new Map\u003Cstring, { value: any; expiresAt: number }>();\\n\\n  return {\\n    get: (sid, cb) => {\\n      const hit = mem.get(sid);\\n      if (!hit) return cb(null, null);\\n      if (Date.now() > hit.expiresAt) {\\n        mem.delete(sid);\\n        return cb(null, null);\\n      }\\n      cb(null, hit.value);\\n    },\\n    set: (sid, session, cb) => {\\n      const ttlMs = 1000 * 60 * 60 * 8;\\n      mem.set(sid, { value: session, expiresAt: Date.now() + ttlMs });\\n      cb(null);\\n    },\\n    destroy: (sid, cb) => {\\n      mem.delete(sid);\\n      cb(null);\\n    }\\n  };\\n}\"},\"type\":\"code\"},{\"id\":\"h2_db\",\"data\":{\"text\":\"DB package (Prisma) — single client, shared types\",\"level\":2},\"type\":\"header\"},{\"id\":\"p_db\",\"data\":{\"text\":\"We expose one PrismaClient from @platform\u002Fdb. The API imports that and only that. If you create a second Prisma client in an app because it’s “faster to prototype”, you’re just creating a future incident.\"},\"type\":\"paragraph\"},{\"id\":\"code_db\",\"data\":{\"code\":\"\u002F\u002F packages\u002Fdb\u002Fsrc\u002Findex.ts\\nimport { PrismaClient } from \\\"@prisma\u002Fclient\\\";\\n\\nexport const db = new PrismaClient();\"},\"type\":\"code\"},{\"id\":\"h2_admin\",\"data\":{\"text\":\"Admin app — cookie session and \u002Fme guard\",\"level\":2},\"type\":\"header\"},{\"id\":\"p_admin\",\"data\":{\"text\":\"Admin is a normal Next.js App Router app. The only special part is that it must treat the API as the source of truth and always include cookies when calling \u002Fapi\u002Fme.\"},\"type\":\"paragraph\"},{\"id\":\"code_admin_guard\",\"data\":{\"code\":\"\u002F\u002F apps\u002Fadmin\u002Fapp\u002F(admin)\u002Flayout.tsx\\nimport { cookies } from \\\"next\u002Fheaders\\\";\\n\\nasync function getMe() {\\n  const cookieHeader = cookies().toString();\\n  const res = await fetch(`${process.env.ADMIN_BASE_URL}\u002Fapi\u002Fme`, {\\n    headers: { cookie: cookieHeader },\\n    cache: \\\"no-store\\\"\\n  });\\n  return res.json();\\n}\\n\\nexport default async function AdminLayout({ children }: { children: React.ReactNode }) {\\n  const me = await getMe();\\n  if (!me?.ok) {\\n    return (\\n      \u003Chtml>\\n        \u003Cbody>\\n          \u003Cp>Unauthorized. Go to \u002Fadmin\u002Flogin.\u003C\u002Fp>\\n        \u003C\u002Fbody>\\n      \u003C\u002Fhtml>\\n    );\\n  }\\n\\n  return (\\n    \u003Chtml>\\n      \u003Cbody>{children}\u003C\u002Fbody>\\n    \u003C\u002Fhtml>\\n  );\\n}\"},\"type\":\"code\"},{\"id\":\"h2_flow\",\"data\":{\"text\":\"Step-by-step: admin login flow\",\"level\":2},\"type\":\"header\"},{\"id\":\"list_flow\",\"data\":{\"items\":[\"Browser submits credentials: POST \u002Fapi\u002Flogin\",\"NGINX forwards \u002Fapi\u002F* to Fastify\",\"Fastify validates user against DB (Prisma)\",\"Fastify writes session into fakeRedis store (dev) and returns Set-Cookie: sid=…\",\"Browser stores the cookie (HTTP-only)\",\"Admin server components call GET \u002Fapi\u002Fme with the cookie attached\",\"Admin renders dashboard only if \u002Fme returns ok=true\"],\"style\":\"ordered\"},\"type\":\"list\"},{\"id\":\"h2_went_wrong\",\"data\":{\"text\":\"One thing that went wrong (so we don’t repeat it)\",\"level\":2},\"type\":\"header\"},{\"id\":\"p_went_wrong\",\"data\":{\"text\":\"We had a login loop even though \u002Fapi\u002Flogin returned 200. The cookie was never sent back on \u002Fapi\u002Fme. The cause was boring: cookie path + proxy routing mismatch. We set the cookie for \u002Fadmin by accident, but \u002Fapi\u002Fme lives under \u002Fapi. Browser did exactly what it should. It didn’t send the cookie. Fix was to set cookie path to \\\"\u002F\\\" and confirm NGINX wasn’t rewriting paths in a way that broke it.\"},\"type\":\"paragraph\"},{\"id\":\"h2_project_plan\",\"data\":{\"text\":\"Project plan (ship-first, refine later)\",\"level\":2},\"type\":\"header\"},{\"id\":\"list_plan\",\"data\":{\"items\":[\"Phase 0: monorepo wiring (Turbo, TS config, shared package imports). Keep it lean.\",\"Phase 1: API auth\u002Fsession endpoints: \u002Flogin, \u002Flogout, \u002Fme. Add basic rate limiting on \u002Flogin.\",\"Phase 2: Admin MVP: login screen + server-side guard + minimal dashboard shell.\",\"Phase 3: Content read endpoints for public platform: \u002Fapi\u002Fcontent\u002F* (read-only first).\",\"Phase 4: Content CRUD in admin + media upload (start simple, then swap storage later).\",\"Phase 5: Monitoring and hardening: Prometheus metrics endpoint, Grafana dashboards, headers, backups.\"],\"style\":\"ordered\"},\"type\":\"list\"},{\"id\":\"h2_definition_done\",\"data\":{\"text\":\"Definition of done (so we don’t argue later)\",\"level\":2},\"type\":\"header\"},{\"id\":\"list_dod\",\"data\":{\"items\":[\"A fresh browser can log into \u002Fadmin and stay logged in across refreshes.\",\"Public platform renders SSR content from \u002Fapi\u002Fcontent without exposing admin-only endpoints.\",\"NGINX routes \u002F, \u002Fadmin, \u002Fapi correctly in docker-compose.\",\"API can restart without corrupting session behavior (dev store resets are fine, but it must fail gracefully).\",\"At least one Prometheus scrape target exists (even if Grafana panels are placeholders).\"],\"style\":\"unordered\"},\"type\":\"list\"},{\"id\":\"hr_end\",\"data\":{},\"type\":\"delimiter\"},{\"id\":\"p_next\",\"data\":{\"text\":\"If you’re implementing a new endpoint, decide first: is it public, admin-only, or internal. Don’t guess. Then put it behind the right prefix and guard it. That’s the whole game.\"},\"type\":\"paragraph\"}],\"version\":\"2.29.1\"}",{"time":212,"blocks":409,"version":373},[410,413,416,419,429,432,434,437,444,447,450,452,455,458,460,462,465,468,470,473,476,478,481,491,494,497,500,509,512,520,522],{"id":215,"data":411,"type":42},{"text":412,"level":40},"Platform Monorepo: Next.js (Public + Admin) + Fastify API + Prisma DB + NGINX",{"id":219,"data":414,"type":222},{"text":415},"This document describes the current target architecture for a monorepo with a public Next.js site, an admin Next.js app with cookie sessions, a Fastify API, and a Prisma-based DB package. It’s written for teammates who will touch multiple packages and need predictable boundaries.",{"id":224,"data":417,"type":42},{"text":418,"level":227},"Constraints we’re not negotiating (for now)",{"id":229,"data":420,"type":240},{"items":421,"style":239},[422,423,424,425,426,427,428],"Public site must be SSR (Next.js App Router). No SPA-only shortcuts.","Public site talks to the backend only via public API routes under \u002Fapi (e.g., \u002Fapi\u002Fcontent, \u002Fapi\u002Fmedia).","Admin uses cookie-based session auth. HTTP-only cookie. No localStorage tokens.","API is Fastify. Sessions via @fastify\u002Fsession. “fakeRedis” store for development; Redis in production later.","DB access is via @platform\u002Fdb only. No direct Prisma clients scattered in apps.","NGINX terminates TLS and routes \u002F, \u002Fadmin, and \u002Fapi. One domain is fine; subdomain is optional later.","Monitoring exists from day one (Prometheus scrape + Grafana dashboard placeholders).",{"id":242,"data":430,"type":42},{"text":431,"level":227},"Repository layout",{"id":246,"data":433,"type":249},{"code":248},{"id":251,"data":435,"type":42},{"text":436,"level":227},"Trade-offs (the real ones)",{"id":255,"data":438,"type":240},{"items":439,"style":239},[440,441,442,443],"Cookie sessions are boring. That’s a feature. They work behind proxies and keep auth logic out of the frontend. The downside: you must get SameSite\u002FSecure\u002Fpath right, or you’ll chase “random” login bugs.","One \u002Fapi surface is clean, but it also means you must be strict about what is public vs admin-only. Don’t leak admin endpoints just because it’s convenient.","Prisma can be “multi-db ready” in the sense of swapping providers. It is not a magic bridge between SQL and Mongo with a single schema. If someone says it is, they haven’t shipped it.","Next.js SSR is good for SEO and first load. It can also slam the API if you don’t set caching and revalidation intentionally.",{"id":263,"data":445,"type":42},{"text":446,"level":227},"Routing contract (NGINX as the front door)",{"id":267,"data":448,"type":222},{"text":449},"We route everything through one domain for now. It’s simpler to debug. If we move admin to a subdomain later, we’ll revisit cookie scope and CSRF assumptions.",{"id":271,"data":451,"type":249},{"code":273},{"id":275,"data":453,"type":42},{"text":454,"level":227},"API package (Fastify) — sessions and auth",{"id":279,"data":456,"type":222},{"text":457},"The API owns authentication and session state. The apps never mint tokens. They just send cookies back. Keep the session payload small. User id + role. That’s it.",{"id":283,"data":459,"type":249},{"code":285},{"id":287,"data":461,"type":249},{"code":289},{"id":291,"data":463,"type":42},{"text":464,"level":227},"DB package (Prisma) — single client, shared types",{"id":295,"data":466,"type":222},{"text":467},"We expose one PrismaClient from @platform\u002Fdb. The API imports that and only that. If you create a second Prisma client in an app because it’s “faster to prototype”, you’re just creating a future incident.",{"id":299,"data":469,"type":249},{"code":301},{"id":303,"data":471,"type":42},{"text":472,"level":227},"Admin app — cookie session and \u002Fme guard",{"id":307,"data":474,"type":222},{"text":475},"Admin is a normal Next.js App Router app. The only special part is that it must treat the API as the source of truth and always include cookies when calling \u002Fapi\u002Fme.",{"id":311,"data":477,"type":249},{"code":313},{"id":315,"data":479,"type":42},{"text":480,"level":227},"Step-by-step: admin login flow",{"id":319,"data":482,"type":240},{"items":483,"style":329},[484,485,486,487,488,489,490],"Browser submits credentials: POST \u002Fapi\u002Flogin","NGINX forwards \u002Fapi\u002F* to Fastify","Fastify validates user against DB (Prisma)","Fastify writes session into fakeRedis store (dev) and returns Set-Cookie: sid=…","Browser stores the cookie (HTTP-only)","Admin server components call GET \u002Fapi\u002Fme with the cookie attached","Admin renders dashboard only if \u002Fme returns ok=true",{"id":331,"data":492,"type":42},{"text":493,"level":227},"One thing that went wrong (so we don’t repeat it)",{"id":335,"data":495,"type":222},{"text":496},"We had a login loop even though \u002Fapi\u002Flogin returned 200. The cookie was never sent back on \u002Fapi\u002Fme. The cause was boring: cookie path + proxy routing mismatch. We set the cookie for \u002Fadmin by accident, but \u002Fapi\u002Fme lives under \u002Fapi. Browser did exactly what it should. It didn’t send the cookie. Fix was to set cookie path to \"\u002F\" and confirm NGINX wasn’t rewriting paths in a way that broke it.",{"id":339,"data":498,"type":42},{"text":499,"level":227},"Project plan (ship-first, refine later)",{"id":343,"data":501,"type":240},{"items":502,"style":329},[503,504,505,506,507,508],"Phase 0: monorepo wiring (Turbo, TS config, shared package imports). Keep it lean.","Phase 1: API auth\u002Fsession endpoints: \u002Flogin, \u002Flogout, \u002Fme. Add basic rate limiting on \u002Flogin.","Phase 2: Admin MVP: login screen + server-side guard + minimal dashboard shell.","Phase 3: Content read endpoints for public platform: \u002Fapi\u002Fcontent\u002F* (read-only first).","Phase 4: Content CRUD in admin + media upload (start simple, then swap storage later).","Phase 5: Monitoring and hardening: Prometheus metrics endpoint, Grafana dashboards, headers, backups.",{"id":353,"data":510,"type":42},{"text":511,"level":227},"Definition of done (so we don’t argue later)",{"id":357,"data":513,"type":240},{"items":514,"style":239},[515,516,517,518,519],"A fresh browser can log into \u002Fadmin and stay logged in across refreshes.","Public platform renders SSR content from \u002Fapi\u002Fcontent without exposing admin-only endpoints.","NGINX routes \u002F, \u002Fadmin, \u002Fapi correctly in docker-compose.","API can restart without corrupting session behavior (dev store resets are fine, but it must fail gracefully).","At least one Prometheus scrape target exists (even if Grafana panels are placeholders).",{"id":366,"data":521,"type":368},{},{"id":370,"data":523,"type":222},{"text":524},"If you’re implementing a new endpoint, decide first: is it public, admin-only, or internal. Don’t guess. Then put it behind the right prefix and guard it. That’s the whole game.","Explore a practical monorepo architecture using Next.js, Fastify, Prisma, and NGINX, highlighting real-world integration and workflow.",{"lang":7,"title":208,"content":210,"contentJson":527,"excerpt":374},{"time":212,"blocks":528,"version":373},[529,531,533,535,538,540,542,544,547,549,551,553,555,557,559,561,563,565,567,569,571,573,575,578,580,582,584,587,589,592,594],{"id":215,"data":530,"type":42},{"text":217,"level":40},{"id":219,"data":532,"type":222},{"text":221},{"id":224,"data":534,"type":42},{"text":226,"level":227},{"id":229,"data":536,"type":240},{"items":537,"style":239},[232,233,234,235,236,237,238],{"id":242,"data":539,"type":42},{"text":244,"level":227},{"id":246,"data":541,"type":249},{"code":248},{"id":251,"data":543,"type":42},{"text":253,"level":227},{"id":255,"data":545,"type":240},{"items":546,"style":239},[258,259,260,261],{"id":263,"data":548,"type":42},{"text":265,"level":227},{"id":267,"data":550,"type":222},{"text":269},{"id":271,"data":552,"type":249},{"code":273},{"id":275,"data":554,"type":42},{"text":277,"level":227},{"id":279,"data":556,"type":222},{"text":281},{"id":283,"data":558,"type":249},{"code":285},{"id":287,"data":560,"type":249},{"code":289},{"id":291,"data":562,"type":42},{"text":293,"level":227},{"id":295,"data":564,"type":222},{"text":297},{"id":299,"data":566,"type":249},{"code":301},{"id":303,"data":568,"type":42},{"text":305,"level":227},{"id":307,"data":570,"type":222},{"text":309},{"id":311,"data":572,"type":249},{"code":313},{"id":315,"data":574,"type":42},{"text":317,"level":227},{"id":319,"data":576,"type":240},{"items":577,"style":329},[322,323,324,325,326,327,328],{"id":331,"data":579,"type":42},{"text":333,"level":227},{"id":335,"data":581,"type":222},{"text":337},{"id":339,"data":583,"type":42},{"text":341,"level":227},{"id":343,"data":585,"type":240},{"items":586,"style":329},[346,347,348,349,350,351],{"id":353,"data":588,"type":42},{"text":355,"level":227},{"id":357,"data":590,"type":240},{"items":591,"style":239},[360,361,362,363,364],{"id":366,"data":593,"type":368},{},{"id":370,"data":595,"type":222},{"text":372},"Post erfolgreich abgerufen",{"items":598,"source":620,"manualIds":621,"manualMatchedIds":622},[599,606,613],{"id":600,"slug":601,"title":602,"excerpt":603,"featuredImage":604,"publishedAt":605},"473","openai-agents-api-vs-agents-sdk-vs-responses-api-what-should-you-build-on-in-2026","OpenAI Agents API против Agents SDK против Responses API: на чем вам стоит разрабатывать в 2026 году?","Стек агентов OpenAI изменился в сентябре 2026 года. Это архитектурное руководство разделяет Agents API, Agents SDK, Responses API и Codex SDK по владению средой выполнения — чтобы команды могли выбрать правильную границу контроля вместо сравнения названий продуктов.","\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":607,"slug":608,"title":609,"excerpt":610,"featuredImage":611,"publishedAt":612},"381","enterprise-grade-multi-tenant-architecture-for-an-international-platform","Мультитенантная архитектура корпоративного уровня для международной платформы","Loving Rocks является корпоративной свадебной платформой, разработанной с истинной многоарендной архитектурой, изолированными базами данных для каждого арендатора и встроенной интернационализацией для глобальной масштабируемости, безопасности и долгосрочной операционной стабильности.","\u002Fuploads\u002F2026\u002F01\u002Fenterprise-grade-multi-tenant-architecture-for-an-international-platform-1769789121298-b6v7ak.webp","2026-01-30T12:04:00.000Z",{"id":614,"slug":615,"title":616,"excerpt":617,"featuredImage":618,"publishedAt":619},"2","multi-database-architecture","Мульти-базовая архитектура с Prisma 7: Глубокое погружение для экспертов","Управление сложными ландшафтами данных требует современных архитектур. Prisma 7 предлагает расширенные функции для интеграции с несколькими базами данных и решает проблемы полиглотной персистентности.","\u002Fuploads\u002F2014\u002F09\u002FSEO-Mobile-Webapplikation-Muenchen-www.stajic.de_1.webp","2025-10-31T04:31:00.000Z","fallback",[],[]]