open atlas
← Все проекты

fullstack · advanced · 9d

Next.js-приложение в продакшен

Собери мультитенантное контент-приложение на App Router — а потом эксплуатируй его: закрой авторизацию и секреты, наслои кэши, реши каждый выбор edge-vs-node и разберись с инцидентом, когда один тенант отравляет общую ISR-страницу.

Это капстоун трека Next.js: возьми всё из трека и доведи одно реальное App Router-приложение от и до, а затем эксплуатируй его. Мультитенантное контент-приложение — у каждого тенанта свой поддомен, свои данные, свои опубликованные страницы — вынуждает принимать решения, которые отделяют senior-инженера Next.js от того, кто умеет отрендерить страницу. Где живёт граница авторизации, чтобы тенант никогда не прочитал чужие данные? Какая работа идёт на edge, а какая на node, и почему? Как три кэша (Data/Full Route Cache фреймворка как ISR, CDN перед приложением и cache-теги по доменам) сочетаются, не отдавая страницу тенанта A тенанту B? Ты очертишь систему, построишь скелет RSC + server actions, проведёшь границы авторизации и секретов, наслоишь и протегируешь кэши, разместишь каждый вызов edge/node против бюджета ассетов, построишь иерархию error.tsx и подключишь трейсинг, задеплоишь через пайплайн, а затем переживёшь и разберёшь инцидент отравления ISR-кэша.

Результат

Задеплоенное мультитенантное Next.js-приложение с RSC + server actions, границей JWT/сессий и server-only-секретами, трёхслойным кэшем (ISR + CDN + cache-теги), иерархией error.tsx, подключённой к трейсингу, RED-дашбордами с SLO и написанным пост-мортемом инцидента отравления ISR-кэша.

Этапы

0/8 · 0%
  1. 01Очерти приложение: модель тенантности, стратегия рендера, SLO

    До любого кода реши две вещи, которые формируют всё дальше: как ключуется тенантность и как рендерится каждый роут. Выбери поддомен-на-тенанта (чистая изоляция, больше работы с DNS/сертификатами) против пути-на-тенанта (проще роутинг, легче утечь) и выпиши правило ключевания. Затем классифицируй каждый роут по стратегии рендера — статика (SSG) для маркетинга, ISR для опубликованного контента тенанта, динамический SSR для дашбордов — потому что эта классификация определяет твой кэш, твой раздел edge/node и радиус поражения позже. Выпиши 2–3 SLO (например, p75 LCP < 2.5 с на опубликованных страницах, p99 TTFB < 200 мс для кэшированных роутов) и явные non-goals, чтобы не вылизывать редактор, когда трафик несёт путь чтения.

    Критерии готовности
    • Ты выписал правило ключевания тенантности (откуда берётся id тенанта на каждом запросе) и почему поддомен, а не путь.
    • Каждый роут классифицирован как static / ISR / dynamic, и у тебя есть 2–3 SLO плюс два явных non-goal.
  2. 02Построй скелет RSC + server actions

    Подними скелет App Router с осознанно проведённой границей server/client. Держи получение данных в Server Components, чтобы креды и запросы не попадали в клиентский бандл; вынеси интерактивность в маленькие клиентские острова на листьях, а не в постраничный 'use client'. Проводи мутации через Server Actions, а не самописные route handlers для отправки форм, и обращайся к слою данных (ORM/SQL в RSC) прямо в дереве компонентов. Проверка хорошего скелета: опубликованная страница везёт почти нет JS, а граница гидратации — тонкая оболочка вокруг интерактивных кусков, а не вокруг страницы.

    Критерии готовности
    • Получение данных живёт в Server Components, и опубликованная страница везёт только JS, нужный её интерактивным островам (проверено в network-панели).
    • Минимум одна мутация идёт через Server Action, и ты можешь показать, где проходит граница server/client и почему.
  3. 03Проведи границы авторизации и секретов

    Сделай мультитенантную авторизацию герметичной. Реши session или JWT для своего случая (stateful-сессии отзываются мгновенно, но нужен стор; JWT масштабируются без состояния, но отзыв и ротация на тебе), затем форси границу тенанта на слое данных, чтобы каждый запрос скоупился по id тенанта — никогда не верь одному лишь поддомену. Помечай server-only-модули, чтобы секрет нельзя было импортнуть в клиентский компонент, держи URL базы и ключи подписи вне бандла и защищай Server Actions от CSRF и от действий над ресурсами чужого тенанта. Граница, которую ты доказываешь: залогиненный пользователь тенанта A не может прочитать, изменить или даже прозондировать тенанта B, что бы он ни положил в запрос.

    Критерии готовности
    • Каждый доступ к данным скоупится по id тенанта на слое данных, и у тебя есть тест, что тенант A не дотягивается до данных тенанта B.
    • Секреты живут за server-only-модулями (клиентский импорт ломает сборку), и Server Actions защищены от CSRF.
  4. 04Наслои кэши: ISR, CDN и cache-теги

    Сочетай три кэша, не утекая данные тенанта и не отдавая навсегда устаревший контент. Используй ISR (Full Route + Data Cache), чтобы опубликованные страницы тенанта были статически-быстрыми с фоновой ревалидацией; поставь CDN впереди, чтобы кэшированный HTML отдавался с edge; и тегируй записи кэша по доменам (например, tenant:{id}, post:{id}), чтобы одно редактирование инвалидировало ровно те страницы, что оно затронуло, через on-demand revalidation — не глобальный purge, не устаревшую страницу. Ловушка — общий ключ кэша: если CDN или Data Cache ключуется по URL, но не по тенанту, запрос может вернуть HTML чужого тенанта. Сделай тенанта частью каждого ключа кэша и докажи, что инвалидация хирургична.

    Критерии готовности
    • Опубликованные страницы кэшируются через ISR и отдаются с CDN, и id тенанта — часть каждого ключа кэша (ты проверил отсутствие кросс-тенантных попаданий).
    • Редактирование одного поста инвалидирует только его тегированные страницы по запросу, и ты можешь показать состояние кэша до/после.
  5. 05Реши edge vs node и держи бюджет ассетов

    Размести каждый кусок работы на правильном рантайме и держи страницу лёгкой. Edge-рантайм быстро стартует и близок к пользователю, но у него урезанная поверхность API и нет нативных Node-модулей — он подходит для роутинга тенантов, middleware-проверки авторизации и геолокационных rewrite'ов; node подходит для ORM, тяжёлой криптографии и всего, что тянет Node-билтины. Положи резолв тенанта и дешёвый auth-гейт в middleware на edge; держи работу с БД на node. Затем задай бюджет ассетов — оптимизируй картинки через next/image, сабсеть и self-host шрифты, чтобы убить layout shift, и анализируй бандл, чтобы случайный клиентский импорт не разнёс твой JS-бюджет. Цель: опубликованная страница под заданным JS-бюджетом (например, < 100 КБ gzip) с корректно отмасштабированной LCP-картинкой.

    Критерии готовности
    • Роутинг тенанта + auth-гейт работают в edge-middleware, работа с БД — на node, и ты можешь обосновать каждое размещение.
    • Опубликованная страница укладывается в заявленный JS-бюджет, картинки идут через next/image, а шрифты self-hosted без layout shift.
  6. 06Построй иерархию ошибок и подключи наблюдаемость

    Сделай отказ локализованным, а работающее приложение читаемым. Построй иерархию error.tsx / not-found.tsx, чтобы брошенная ошибка в одном сегменте роута деградировала это поддерево, а не всё приложение — падающий виджет в дашборде тенанта A не должен гасить тенанта B. Затем инструментируй: зарегистрируй OpenTelemetry через instrumentation.ts, снимай RED-метрики (rate, errors, duration) на путях чтения и действий, логируй структурные серверные события, запрашиваемые по тенанту, и пробрасывай трейсы через переходы RSC → слой данных, чтобы медленная страница указывала на медленный span. Преврати свои SLO в дашборд с error budget, чтобы видеть инцидент раньше, чем следующий этап его тебе вручит.

    Критерии готовности
    • Иерархия error.tsx локализует брошенную ошибку в её сегменте роута (отказ одного тенанта не гасит приложение).
    • Дашборд показывает rate, error rate и p50/p99 длительности, привязанные к SLO, а трейс медленной страницы показывает её span'ы RSC и слоя данных.
  7. 07Задеплой: рантайм, пулинг, выкатка

    Выкати так, как ты бы это эксплуатировал для тенантов. Выбери self-host или managed-платформу с открытыми глазами — managed даёт ISR/CDN/edge бесплатно, но биллит за инвокацию и за GB-отдачи; self-host на node даёт контроль, но стор кэша и обвязка CDN на тебе. Что бы ты ни выбрал, реши проблему serverless-БД: шторм соединений на инвокацию исчерпает Postgres, поэтому поставь пулер (PgBouncer или data-proxy платформы) между приложением и БД. Собери пайплайн, гейтящийся на тесте изоляции тенантов из этапа авторизации, держи секреты инжектируемыми при деплое и сделай плохую выкатку откатываемой за секунды.

    Критерии готовности
    • Пуш гоняет тесты (включая тест изоляции тенантов) → собирает → деплоит, и ты можешь откатиться без пересборки.
    • БД за пулером (нет шторма соединений на инвокацию), и секреты инжектятся при деплое, а не запекаются в сборку.
  8. 08Переживи инцидент отравления ISR-кэша, затем напиши пост-мортем

    Запрос с незаключённым в ключ заголовком (или тенант-слепым ключом кэша) заставляет Next.js закэшировать неправильный ответ в общую запись ISR/CDN; теперь тенанту B отдаётся HTML тенанта A — или подконтрольная атакующему страница — пока запись не ревалидируется, а параллельный шторм холодных стартов при ревалидации взвинчивает TTFB. Обнаружь это по своим метрикам (скачок кросс-тенантных 200, всплеск TTFB на ревалидации), смягчи вживую (вычисти отравленный тег, сузь ключ кэша, выбрось незаключённый вход), найди корневую причину и напиши пост-мортем. Держащийся фикс — ключевать кэш по всему, что варьирует ответ: тенант, состояние авторизации, заголовки, которые ты реально читаешь, — плюс коалесценция запросов на ревалидации, чтобы холодная ISR-запись не вызвала thundering herd. «Укоротить TTL» — не фикс; это лишь сужает окно отравления.

    Критерии готовности
    • Ты воспроизвёл отравление (запрос, кладущий неправильный ответ в общую запись ISR/CDN) и зафиксировал кросс-тенантное попадание и всплеск TTFB на дашборде.
    • Ты смягчил его, заключив в ключ кэша каждый варьирующий ответ вход плюс коалесценцию на ревалидации, и показал чистую изоляцию тенантов и возврат TTFB к SLO.
    • Твой пост-мортем называет триггер, радиус поражения (какие тенанты, как долго), фикс и одну превенцию, которая не «укоротить TTL».
    Самопроверка

    Вставь корневую причину из пост-мортема и пункт превенции; senior-ревьюер проверяет, что назван механизм ключевания кэша (ключ по каждому варьирующему ответ входу + коалесценция), а не только симптом (страница чужого тенанта / высокий TTFB).

Рубрика

Джуниор Миддл Сеньор
Слои кэширования и корректность ISR Страницы либо полностью статические (без ревалидации), либо полностью динамические (без кэширования); контент тенанта не тегируется и инвалидация — это полная очистка. Опубликованные страницы кэшируются через ISR, отдаются с CDN и тегируются по тенанту и посту, чтобы одно редактирование инвалидировало ровно затронутые страницы по требованию; неселективная полная очистка не нужна. Id тенанта — часть каждого ключа кэша на каждом слое (Data Cache, Full Route Cache, Vary-заголовки CDN); ты воспроизвёл инцидент отравления ISR (тенант-слепой ключ, отдающий чужой HTML) и фикс ключует по каждому варьирующему ответ входу плюс коалесценцию на ревалидации, чтобы холодная ISR-запись не вызвала thundering herd. «Укоротить TTL» отсутствует в твоём разделе превенций.
Управление переменными окружения и секретами Секреты загружаются из process.env в серверных компонентах, но никакой guard на этапе сборки не предотвращает их импорт в клиентский бандл. Чувствительные модули помечены 'server-only', поэтому клиентский импорт ломает сборку; секреты инжектируются при деплое, а не запекаются в артефакт сборки. Server Actions защищены от CSRF и верифицируются по аутентифицированному тенанту до выполнения; у тебя есть тест, что тенант A не может получить данные тенанта B даже через прямой запрос Server Action. БД за пулером, чтобы предотвратить штормы соединений на инвокацию в serverless.
Наблюдаемость и границы ошибок Ошибки проявляются как необработанные исключения, гасящие всю страницу; структурного логирования и трейсинга нет. Иерархия error.tsx / not-found.tsx локализует отказы в их сегменте роута (ошибка одного тенанта не гасит приложение); OpenTelemetry зарегистрирован, и RED-дашборд показывает rate, error rate и p50/p99 длительности, привязанные к SLO. Трейсы распространяются через переходы RSC → слой данных, чтобы медленная страница указывала на медленный span, а не на агрегат. Структурные логи запрашиваемы по тенанту, чтобы кросс-тенантный инцидент можно было охватить за минуты. Ты превратил SLO в error budget, чтобы всплеск TTFB из инцидентного этапа был виден до того, как станет жалобой клиента.
Безопасность деплоя и отката Деплои ручные; откат требует пересборки, и секреты могут быть закоммичены в репозиторий. CI-пайплайн гейтится на тест-сьюте (включая тест изоляции тенантов) перед сборкой и деплоем; откат не требует пересборки. Ты можешь разграничить компромиссы self-host vs managed деплоя с числами (биллинг на инвокацию vs фиксированная стоимость инфраструктуры, ISR/CDN включено vs ручная обвязка) и защитить выбор для мультитенантной read-heavy нагрузки. Деплой-пайплайн отвергает секрет, запечённый в артефакт сборки, через шаг статического сканирования.
Эталонный разбор (спойлер)

Отравление ISR-кэша: ключ кэша, не включающий тенант-дискриминатор (поддомен, заголовок тенанта), позволяет ответу одного тенанта попасть в общую запись CDN или Data Cache другого. Решение — включить каждый варьирующий ответ вход в ключ. «Укоротить TTL» лишь сужает окно отравления — но не предотвращает его.

Thundering herd при ревалидации ISR: когда популярная ISR-страница истекает, первые N конкурентных запросов все промахиваются мимо кэша и все одновременно запускают рендер на сервере. Коалесценция запросов (Data Cache Next.js дедуплицирует летящие запросы) ограничивает это одним upstream-вызовом на ключ кэша на цикл ревалидации — но только если ключ стабилен и запрос не обходится через dynamic() или cache: 'no-store'.

Размещение edge vs node: edge-рантайм стартует менее чем за 1 мс глобально, но не имеет Node.js built-ins (нет fs, нативной крипты, драйверов ORM). Роутинг тенанта и дешёвая проверка JWT в middleware принадлежат edge. Доступ к БД, bcrypt и всё, требующее Node-внутренностей, — node. Размещение работы с БД на edge либо падает (отсутствующий модуль), либо молча откатывается на node, добавляя переход edge→node поверх round trip к БД.

Гигиена server-only-секретов: Next.js поставляет пакет 'server-only', который бросает ошибку на этапе сборки, если модуль импортируется в клиентский компонент. Любой модуль, импортирующий строку подключения к БД, ключ подписи или API-секрет третьей стороны, должен реэкспортироваться только из файлов, защищённых 'server-only'. Анализатор бандла, запускаемый в CI, ловит утечки клиентских секретов, пропущенные 'server-only' из-за косвенной цепочки импортов.

Сделай по-сеньорски

  • Поддержи кастомные домены на тенанта с автоматической выдачей TLS и потоком верификации домена, не ослабляя тенантное ключевание кэша.
  • Добавь Partial Prerendering / стриминговый раздел, чтобы статическая оболочка тенанта отдавалась мгновенно, а динамические, на пользователя, части стримились за Suspense.
  • Сделай multi-region: отдавай опубликованные страницы read-local с ближайшего edge и явно рассуждай о консистентности пути записи на ревалидации.
  • Добавь rate limiting и атрибуцию стоимости на тенанта, чтобы трафик одного тяжёлого тенанта не исчерпал общую ёмкость и не прятался в агрегированном счёте.

Навыки

RSC and server actionsauth boundaries and secret hygienelayered caching (ISR/CDN/tags)edge-vs-node runtime decisionsasset and bundle budgetserror boundaries and tracingobservability (RED/SLO)incident response and post-mortems

Рекомендуемый стек

Next.jsReacta Postgres databasean ORM (Prisma or Drizzle)a CDN in front of the appan OpenTelemetry-compatible tracer