Skip to content

Latest commit

 

History

History
117 lines (74 loc) · 21 KB

File metadata and controls

117 lines (74 loc) · 21 KB

Деплой MVP в продакшн (первый боевой сайт)

Цель: поднять инфраструктуру Jalyk на бесплатных тарифах и подключить к ней свой сайт, который живёт в отдельном репозитории и тянет @jalyk/* из публичного npm. Деплоим по частям: сначала база и хранилище, потом api, потом платформа web, потом публикация пакетов, и только в конце — сам сайт-потребитель.

Архитектура в проде состоит из четырёх кусков, которые надо разместить: платформа apps/web (SSR на TanStack Start — аккаунты, проекты, API-ключи, OAuth), сервер контента apps/api (долгоживущий Effect-процесс с CRUD, SSE и ассетами), управляемый Postgres с расширениями pgvector и pg_trgm, и S3-совместимое объектное хранилище для ассетов. Пятый кусок — npm-пакеты, которые ставит сам сайт.

0. Общие развилки хостинга (выбрать перед стартом)

  • Postgres: Neon (serverless, free, есть pgvector; pg_trgm ставится CREATE EXTENSION) — либо Supabase (free, Postgres + S3-совместимое Storage в одном тарифе, оба расширения есть из коробки). Рекомендация: Supabase, потому что закрывает сразу и БД, и хранилище ассетов одним аккаунтом.
  • Объектное хранилище: Cloudflare R2 (free 10 ГБ, S3 API) либо Supabase Storage. Если БД на Supabase — берём его же Storage и не плодим сервисы.
  • apps/web (SSR): Vercel Hobby (free) — TanStack Start собирается под Nitro и едет на Vercel из коробки. Альтернатива — Netlify free или Cloudflare.
  • apps/api (долгоживущий Node + SSE): НЕ serverless — нужен процесс, который держит соединение для SSE. Render (free web service, засыпает при простое) либо Fly.io. Рекомендация: Fly.io (нет принудительного засыпания в той же мере), запасной — Render.

1. Postgres (управляемая БД) — СДЕЛАНО (инициализация)

  • Заведён проект Supabase (ref oudszegzoytgphexuhdh, eu-north-1).
  • Выбор окружения БД через DB_TARGET (local/supabase) реализован как Effect-стратегия в packages/db/src/config.ts: DbConfig + слои DbConfigLocal/DbConfigSupabase. Строки Supabase — SUPABASE_DATABASE_URL (transaction pooler :6543, рантайм) и SUPABASE_DIRECT_URL (session pooler :5432) в корневом .env.
  • Схема накатана на Supabase. Prisma CLI (db push/migrate) через пулер падает с P1017, а прямой хост db.<ref>.supabase.co — только IPv6 (у машины его нет). Обход: DDL применяется драйвером pg через transaction pooler — команда pnpm --filter @jalyk/db run ddl:push (packages/db/scripts/apply-ddl.mjs, берёт prisma/schema.sql).
  • Осталось: включить расширения CREATE EXTENSION IF NOT EXISTS vector; CREATE EXTENSION IF NOT EXISTS pg_trgm; (для поиска, позже).
  • На будущее: ddl:push — init-only (валит на непустой базе, т.к. schema.sql генерится migrate diff --from-empty). Для обновлений схемы на живой базе перейти на инкрементальный prisma migrate diff от текущего состояния БД к схеме.

2. Объектное хранилище ассетов — СДЕЛАНО (код)

  • В packages/core/src/storage.ts заглушка YandexAssetStorageLive заменена на S3AssetStorageLive через @aws-sdk/client-s3 (write/read/remove по ключу projectId/uuid, forcePathStyle: true). Конфиг читается Effect Config: JALYK_S3_ENDPOINT/REGION/BUCKET/ACCESS_KEY_ID/SECRET_ACCESS_KEY (секрет через Config.redacted).
  • Драйвер выбирается JALYK_STORAGE = local|s3 (apps/api/src/config.ts, apps/api/src/server.ts).
  • Раздача байтов остаётся проксированием через api (handler serve читает Uint8Array и стримит) — бакет приватный, presigned-URL не нужны.
  • Supabase: бакет vedro (приватный) создан, S3 Access Key выписан, JALYK_S3_* заполнены в .env (endpoint https://oudszegzoytgphexuhdh.storage.supabase.co/storage/v1/s3, region eu-north-1). Smoke-тест write→read→remove через S3AssetStorageLive прошёл успешно.

3. apps/api (сервер контента)

  • Прод-runtime: собрать слой с DatabaseLive + новым S3-AssetStorage (а не Local/Yandex-заглушкой).
  • CORS: разрешить origin будущего сайта и web-платформы (сейчас сервер локальный, политики нет) — проверить, что preflight и X-Api-Key/Authorization проходят.
  • Энв на хостинге: DATABASE_URL, BETTER_AUTH_SECRET (обязан совпадать с web), PORT, креды S3. start уже есть (tsx --env-file-if-exists).
  • Проверить, что выбранный хост держит SSE-поток /projects/:id/events без обрыва по таймауту; keepalive в коде есть (Stream.tick 25с).

4. apps/web (платформа)

  • Выбрать Nitro-preset под хост (Vercel/Netlify/Cloudflare) и проверить прод-билд pnpm --filter @jalyk/web build + start.
  • Энв: DATABASE_URL (та же прод-база), BETTER_AUTH_SECRET (== api), BETTER_AUTH_URL=https://<домен-платформы>, креды GitHub/Google.
  • OAuth: в настройках GitHub/Google прописать прод-callback https://<домен>/api/auth/callback/{github,google}; отозвать и перевыпустить скомпрометированные легаси-секреты (лежали в git-истории, см. заметку про env/oauth).
  • Завести первый проект и выпустить read/write API-ключ для своего сайта.

5. Публикация npm-пакетов (публичный npm)

  • Публикуемый граф для сайта: @jalyk/client, @jalyk/studio, @jalyk/schema, плюс их зависимости из workspace — как минимум @jalyk/ui, @jalyk/contract (проверить полный граф).
  • Сейчас к публикации готов только @jalyk/studio (есть tsup + publishConfig). У @jalyk/client/@jalyk/schema/@jalyk/ui exports смотрит в src, стоит private: true, нет билда и .d.ts — добавить сборку (tsup/tsc), files, publishConfig, снять private.
  • Заменить внутренние workspace:* на реальные диапазоны версий при публикации (Changesets или pnpm publish -r решают это; выбрать инструмент версионирования).
  • Учесть Tailwind у @jalyk/studio: потребителю нужен экспорт стилей (./styles.css уже есть) и инструкция по подключению пресета/слоёв.
  • React в peerDeps — проверить, что сайт даёт React 19.

5a. Автоматизация релиза пакетов (чтобы не забывать публиковать)

Сейчас публикация ручная: pnpm run release патч-бампит все пять публичных пакетов, собирает и выкладывает, после чего в personal-jalyk вручную переписываются версии и делается pnpm install. Любой пропущенный шаг ломает деплой сайта на Vercel (ставится старое или несогласованное). Причина хрупкости — связка «два репозитория + ручная публикация + версии 0.0.x»: для semver ^0.0.x не расширяется даже на патч, поэтому потребитель обязан править каждую версию руками.

  • Увести пакеты с 0.0.x на 1.0.0 (или хотя бы 0.1.0), тогда ^1.x в personal-jalyk сам подхватывает патчи и миноры — после публикации достаточно pnpm update, без ручной правки версий.
  • Публикацию повесить на CI: GitHub Action на main, который ставит зависимости, гоняет build:packages и публикует. Каноничный инструмент — Changesets (@changesets/cli + changesets/action): changeset на изменение, бот сам бампит версии, ведёт changelog и публикует в npm при мердже. Тогда «забыть опубликовать» физически нельзя.
  • После публикации автоматически дёргать редеплой personal-jalyk через Vercel deploy hook, чтобы он пересобрался на свежих версиях (в связке с ^1.x — полностью автоматический путь «запушил в jalyk → опубликовалось → сайт пересобрался»).
  • На хостинге прод-сборку запускать с pnpm install --frozen-lockfile, чтобы рассинхрон лок-файла падал явно, а не собирал втихую не ту версию.

6. Сайт-потребитель (отдельный репо)

  • Поставить @jalyk/client (+ @jalyk/schema) для чтения контента по API-ключу, направить на прод-URL apps/api.
  • Встроить @jalyk/studio (админка) с тем же API и bearer/ключом; подключить стили.
  • Задеплоить сайт на free-хост (тот же Vercel/Netlify) с переменными: URL api и публичный read-ключ.

Порядок выполнения

БД+расширения → S3-драйвер (раздел 2, блокер) → деплой api → деплой web и выпуск ключа → публикация пакетов → сайт.

Фичи и баги

  • баг: в тулбаре в презенс - редактировать профиль - ничего не делает и не понятно как появлось
  • переключение темы не работает в админке
  • серверная часть рассогласования (api/web): на отдаче клиентскому запросу документ со структурно-сломанным полем (conformancePaths непуст) режется целиком, а безобидные неизвестные поля просто срезаются. Сейчас в студии всё видно и чинится, но сам срез на выдаче ещё не реализован.
  • здоровье контента: перед тем как правка схемы (переименование/удаление поля, изменение predefined) уедет, показать в студии, сколько живых документов она уронит из клиентской выдачи (через conformancePaths по всем документам типа) — иначе массовый обвал контента видно не в студии, а по пропаже на сайте.

Навигация — оставшиеся хвосты

Ядро сделано (см. Архив). Не реализовано из решённого в дизайне:

  • асинхронная валидация при восстановлении — сейчас путь обрезается только по структуре (неизвестный key), но не проверяется, существует ли docId/fieldPath в базе; нужна ступенчатая проверка с лоадерами и обрезкой до последнего реально валидного звена (смыкается с багами про здоровье контента ниже).

defineUnion

defineUnion который позволяет хранить случайное значение в поле из предопределенных. у всего поля появляется действие переключить поле. это поле-обертка по сути. внутри прописываются массив из полей которые доступны например defineUnion([defineString, defineNumber, defineReference]), также указывается дефолтное поле (первое в списке по дефолту или можно перезаписать). особенность в том, что имена полей внутри используются как дискриминаторы, поэтому они сохраняются вместе со значением в жсоне, чтобы при сохранении defineUnion знал какой компонент ему ренедрить внутри. я пока не могу придумать, в какой ситуации это будет полезно, поэтому прошу тебя помочь придумать ситуации для этого.

  • пкм по превью документа - контекстно меню - дублировать, копировать, вставить.

Архив

Хедер сегмента и инлайн-создание/редактирование в поле ссылки (сделано)

Каждый сегмент навигации автоматически рисует хедер с названием (title из definePathSegment) и крестиком у правого края; крестик зовёт close уровня, у корневого его нет (canClose=false). Ширину и рамку колонки задаёт сам компонент вью, обёртка нейтральна (shrink-0), пропа стиля у definePathSegment нет. Поле ссылки через useSegmentNav/openDocument открывает сегмент документа: кнопка-карандаш редактирует выбранную цель, а меню «Новый документ» справа от поиска создаёт новую и сразу открывает её форму; дерево документ→документ рекурсивно, поэтому цепочка бесконечна. Реализация: packages/studio/src/data/navigation.tsx (SegmentHeader, useSegmentNav), packages/studio/src/fields/reference.tsx, рекурсия в views/segments.tsx.

Инлайн-разворот формы документа в поле ссылки (сделано)

У выбранного превью в поле ссылки две affordance: карандаш открывает документ в отдельном сегменте-редакторе (nav.go(openDocument)), а шеврон слева тоглит форму документа инлайн прямо под рядом поля. Форма берётся из конфига документа: добавлен variant='inline' в DocumentEditor (растёт по контенту, без h-full/скролла), удаление из неё чистит ссылку и сворачивает. Реализация: packages/studio/src/views/DocumentEditor.tsx, packages/studio/src/fields/reference.tsx.

Поле-картинка: кнопки источников и «Убрать» (сделано)

Подписи и иконки кнопок поля-картинки приведены к набору: «Загрузить» (UploadIcon, всегда — заливка с диска), «Выбрать» (ImagesIcon, всегда — из библиотеки, бывшее «Смотреть все») и «Убрать» (XIcon, ghost, приглушённая, отодвинута через ml-auto, только при наличии обложки, бывшее «Удалить»). Слово «Заменить» убрано: кнопки-источники статичны по источнику, а не по состоянию. Корзину не используем — кнопка лишь отвязывает ссылку (handle.set(null)), файл остаётся в библиотеке, поэтому крестик честнее. Реализация: packages/studio/src/fields/image.tsx.

Навигация — ядро (сделано)

Линейный стек сегментов с персистом в localStorage и восстановлением после перезагрузки, как папки в ОС. Путь — одна линейная цепочка (на каждом уровне один открытый потомок); колонки и слои — два рендера одного стека. Сегменты объявляются статическим деревом definePathSegment (view + children) с типизированными переходами open.<child>(params) и PathSegmentLink. Путь хранится только в localStorage по ключу navigation.key (без URL); восстановление идёт по дереву определений, неизвестное звено в середине обрезает путь (структурно). Реализация: packages/studio/src/data/navigation.tsx, встроенное дерево views/segments.tsx, MillerView/LayerView переписаны поверх useNavStack, проп <Studio navigation>. Кастомный пример с кнопкой «Джарибек» (панель хранилища) — в apps/test-client.