Перейти к содержимому
Версия 1.0 ещё не выпущена; проект находится на стадии pre-MVP. См. возможности и ограничения версии 1.0.

Автономный бэкенд: детали реализации

Здесь описано устройство сгенерированного автономного бэкенда: файлы и зависимости, ресурсы Cloudflare, соответствие контента таблицам, экспорт управляемого рендерера и кеш ответов. Маршруты, аутентификация и развёртывание описаны в справочнике API.

Дерево сокращено. В нём основные файлы, а не каждый генерируемый файл.

.
├── package.json
├── .nvmrc / .npmrc
├── wrangler.jsonc / wrangler.example.jsonc / wrangler.build.jsonc
├── graduation.manifest.json / graduation.generator.json
├── .dee-wan-files.json / .dee-wan-customization.json
├── prisma/schema.prisma
├── migrations/
├── access/read-tokens.json
├── data/published-snapshot.json
│ (data/media-manifest.json after a media transfer)
├── src/
│ ├── app.ts / worker.ts / access.ts / access-policy.ts / auth.ts / security.ts / db.ts
│ ├── access-config-rules.js / access-config-rules.d.ts / credential-digest.ts
│ ├── response-cache.ts / query-manifest.ts / trusted-routes.ts / relation-routes.ts
│ ├── sqlite-batch.ts / d1-column-types.ts / d1-schema-columns.ts
│ ├── backend-customization.ts / backend-config.ts / customization-runtime.ts
│ ├── local.ts / serve.ts / serve-cli.ts / migrate.ts
│ ├── generated/prisma/ / generated/guard/ / generated/routes/
│ └── owned/backend.ts / owned/routes.ts
├── scripts/
│ ├── dee-wan-generator.mjs / regenerate.mjs / check-customization.mjs
│ ├── install-dependencies.mjs / emit-migration.mjs / portable-client.mjs
│ ├── load-snapshot.mjs / configure-deployment.mjs / file-manifest.mjs
│ ├── check-relation-ids.mjs / deploy-from-repository.mjs
│ └── verify-deployment.mjs / verify-media.mjs
├── .github/workflows/deploy-from-repository.yml
│ (site artifacts additionally carry dee-wan-build.yml, the managed
│ build contract; a `--model` artifact has no CMS-managed site build)
├── managed-renderer/
│ (only when the site had an active managed renderer release)
│ ├── package.json / package-lock.json / astro.config.mjs / tsconfig.json
│ ├── wrangler.jsonc / public/ / worker/ / src/
│ └── release/release.json / template.json / route-map.json / site-chrome.json
│ / managed-renderer.mjs / evidence/
└── apps/site/
├── package.json / wrangler.jsonc / astro.config.mjs / public-reads.json
├── public/.assetsignore
└── src/
├── lib/ / pages/ / components/
├── lib/image-endpoint.ts
├── owned/
└── pages/owned/

Файлы generated имеют контрольные отпечатки и регенерируются. Изменения останавливают следующую регенерацию. Файлы owned создаются как заготовки только при отсутствии и сохраняются побайтно. Файлы shared сохраняются, а манифесты пакетов дополняются слиянием. История миграций только дополняется. Точная матрица путей есть в руководстве по настройке.

Имена новых пакетов состоят из graduated-site- и ID сайта в UTF-8, записанного шестнадцатеричными цифрами в нижнем регистре. Они не меняются при перестановке, добавлении или удалении моделей. Существующие имена пакетов и README владельца сохраняются.

Hono обрабатывает HTTP. В API Worker Prisma использует компилятор запросов и адаптер D1. prisma-generator-express генерирует CRUD-конвейер Hono и OpenAPI, а prisma-guard проверяет структуру запросов. Локальный сервер использует адаптер better-sqlite3. eav-to-prisma преобразует принятые модели CMS при генерации. Генератор также поставляется как скрипт сборки с закреплённым хешем. Это не сервис, к которому CMS обращается во время работы.

Следующий перечень проверяется по ARTIFACT_TOOLCHAIN_PACKAGES, SITE_MANIFEST и константам установщика. Версии корневых зависимостей определяются по установке Dee Wan, в которой выполнялась генерация, и по graduation.manifest.json.toolchain. Версии установленных пакетов не задаются самой документацией. Все корневые версии закреплены точно. Для сайта допускаются указанные диапазоны версий; конкретные версии закреплены в его lock-файле.

Install root Package Package block Version authority
root @dee-wan/cache dependencies toolchain.responseCache (exact)
root @prisma/adapter-d1 dependencies toolchain.prismaAdapterD1 (exact)
root @prisma/client dependencies toolchain.prismaClient (exact)
root hono dependencies toolchain.hono (exact)
root yaml dependencies toolchain.yaml (exact)
root prisma-generator-express dependencies toolchain.prismaGeneratorExpress (exact)
root prisma-guard dependencies toolchain.prismaGuard (exact)
root jsonc-parser dependencies toolchain.jsoncParser (exact)
root @cloudflare/workers-types devDependencies toolchain.workersTypes (exact)
root eav-to-prisma devDependencies toolchain.eavToPrisma (exact)
root @prisma/adapter-better-sqlite3 devDependencies toolchain.prismaAdapterBetterSqlite3 (exact)
root @types/node devDependencies toolchain.nodeTypes (exact)
root better-sqlite3 devDependencies toolchain.betterSqlite3 (exact)
root esbuild devDependencies toolchain.esbuild (exact)
root prisma devDependencies toolchain.prisma (exact)
root typescript devDependencies toolchain.typescript (exact)
root wrangler devDependencies toolchain.wrangler (exact)
apps/site @astrojs/cloudflare dependencies ^12.6.3
apps/site @tailwindcss/vite dependencies ^4.1.14
apps/site astro dependencies ^5.14.1
apps/site tailwindcss dependencies ^4.1.14
apps/site @astrojs/check devDependencies ^0.9.5
apps/site @emnapi/runtime devDependencies ^1.11.3
apps/site typescript devDependencies ^5.9.3

Runtime-пакеты корня предоставляют адаптер/клиент D1, Hono, guard, вспомогательный код маршрутов и сериализацию OpenAPI в YAML. Пакеты разработки предоставляют типы Worker/Node, преобразование моделей, локальную SQLite, генерацию/миграции Prisma, проверку TypeScript, сборку esbuild и Wrangler. Пакеты локальной SQLite находятся в development-зависимостях, хотя локальный сервер использует их во время работы. Зависимости сайта включают Astro, адаптер Cloudflare, Tailwind и интеграцию Vite. Development-блок — проверку Astro и TypeScript.

Корень объявляет Node >=22. Генерируемый .nvmrc выбирает 24.18.1, а packageManager — npm@11.16.0. Существующие .nvmrc и выбор менеджера сохраняются, поэтому проверенный установщик всё равно сверяет выбранный npm с объявлением. CLI и клиент Prisma должны использовать поддерживаемую основную версию 6. Широкий диапазон Node сам по себе не доказывает совместимость.

Окно терминала
node scripts/install-dependencies.mjs
node scripts/install-dependencies.mjs apps/site

Проверенный установщик распаковывает зависимости с выключенными скриптами, проверяет lifecycle-скрипты, включая нативные сборки, и пересобирает только разрешённые пакеты. Разрешения allowScripts указывают точные версии. Запреты могут указывать имя пакета. Корневые разрешения выводятся из закреплённых Prisma engines/CLI, better-sqlite3, esbuild и известной пары Wrangler/workerd. Автоматическая генерация Prisma client и fsevents запрещены. У сайта свой проверенный список. Новая версия пакета со скриптами жизненного цикла требует отдельной проверки. Изменение зависимости не даёт разрешения выполнять её скрипты. Git hooks не устанавливаются.

Worker читает типы результатов D1 через src/d1-column-types.ts и файл сайта src/d1-schema-columns.ts. Адаптер D1 угадывает тип колонки по её первому значению, поэтому текст, похожий на дату, может превратить в даты всю колонку. Сырой запрос ($queryRaw*) получает такую колонку текстом, только если текст SQL доказывает, что колонка — String:

  • Каждый источник строк — таблица, объявленная в схеме. Источники читаются после FROM, любого вида JOIN, UPDATE, INTO и после каждой запятой списка FROM, в том числе после ON или USING, на любой глубине. Таблица может быть в кавычках, с префиксом main. и с псевдонимом. Подзапрос учитывается через таблицы внутри него.
  • Хотя бы один источник объявляет колонку, и каждый источник, который её объявляет, объявляет её как String.
  • Ни один список select или RETURNING не даёт это имя ничему, кроме той же колонки (x, t.x, x AS x).
  • В запросе нет UNION, INTERSECT или EXCEPT.

Всё остальное сохраняет тип, угаданный адаптером: таблица, которой нет в схеме, CTE, табличная функция, другая схема, список VALUES, псевдоним, выражение, составной select и запрос без таблицы. Проверка читает имена таблиц и колонок из текста SQL. Типы SQL-выражений она не выводит.

Элемент Источник или назначение
API Worker Собирается с nodejs_compat, развёртывается из выбранного коммита.
База D1 и привязка DB Определяется по идентичности сайта, создаётся при отсутствии. Миграции и загрузка снимка адресованы этой базе.
SITE_ID, ACCESS_CONFIG, GIT_SHA Переменные Worker: идентичность сайта, хеши/области read-токенов и коммит. ACCESS_CONFIG — объединение токенов, выданных CMS (access/read-tokens.json), и vars.ACCESS_CONFIG из вашего wrangler.jsonc, а если токенов нет — корректный пустой список токенов.
API_BEARER_TOKEN Не менее 32 символов. Секрет репозитория устанавливается как секрет записи API Worker. Никогда не коммитится.
CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID Секреты GitHub Actions для развёртывания, не runtime-учётные данные CMS.
PUBLIC_HOSTNAME Переменная репозитория с именем хоста без схемы/пути для проверки развёртывания.
Worker/ресурсы сайта Astro Собираются из apps/site, проходят бандлинг командой wrangler deploy --dry-run до того, как генерация примет сайт, и до первого изменения у провайдера, развёртываются после проверки API. Без nodejs_compat: изображения проходят без изменений (imageService: 'passthrough'), поэтому в Worker нет sharp. /_image отвечает 404 (src/lib/image-endpoint.ts): у сайта нет сервиса изображений. Сессии используют драйвер null, поэтому Worker не нужна KV-привязка SESSION. public/.assetsignore не пускает _worker.js и _routes.json в публичные ресурсы.
Хранилище медиа владельца При переносе медиа нужны декларации R2/хранилища и манифест переноса. MEDIA_PUBLIC_ORIGIN задаёт адрес назначения. Байты медиа не являются исходниками.
Полномочия управляемой сборки DEE_WAN_URL и токен, привязанный к сайту/версии, принадлежат управляемому CI. Репозиторное развёртывание не загружает определения CMS.

wrangler.jsonc — общая локальная конфигурация. wrangler.build.jsonc генерируется для сборки. Развёртывание выводит wrangler.deploy.jsonc из проверенных входных данных сайта/базы/медиа. Дополнительная привязка в общем локальном файле не создаётся автоматически и не переносится в конфигурацию развёртывания. Явно добавьте нужный шаг подготовки/развёртывания в свой workflow. Пример конфигурации не содержит учётных данных, рабочего идентификатора базы или production-маршрута.

Опубликованные версии становятся строками. Черновики, неопубликованные изменения, история, пользователи, роли, сессии, учётные данные и записи проверки/аудита остаются в Dee Wan. Slug модели создаёт Prisma-модель в PascalCase и соответствующие SQL-таблицы/столбцы. Идентификатор записи сохраняется в id.

Переводимые поля находятся в таблице переводов, связанной с основной строкой и языком. Связь к одному (oneToOne, manyToOne) хранит идентификатор цели в скалярном столбце <field>_id. Связь-список (manyToMany, oneToMany) использует явную связующую таблицу с позициями; ею владеет модель, объявившая поле. В связующей таблице one-to-many столбец цели уникален: у каждой цели через это поле один владелец. Поле, допускающее несколько моделей, имеет по одной связующей таблице для каждой допустимой модели, каждая с внешним ключом на свою модель; общий порядок элементов в списке владельца задаётся через position; смешанное поле «к одному» хранится так же. В целевую модель не добавляется столбец этого поля, и данные этого поля в неё не записываются. При генерации одной модели отсутствующие целевые модели и SQL-ограничения внешнего ключа не создаются, а связи-списки отклоняются. Иерархическая ссылка на родителя указывает на свою же модель, поэтому получает внешний ключ в обоих вариантах. Текст/rich text, числа, boolean и даты становятся соответствующими скалярными столбцами. JSON, multi-select и компоненты используют JSON. Варианты select остаются ограничениями манифеста, а не enum базы.

В результате для всего сайта каждый идентификатор связи — внешний ключ, а удаление ведёт себя как в CMS:

Ссылка При удалении целевой строки
Необязательная связь к одному <field>_id Столбец становится null.
Обязательная связь к одному <field>_id Удаление отклоняется с 409 relation_target_referenced, с моделью и столбцом.
Строка связующей таблицы Строка связи удаляется. Список может стать короче минимума, пока его не запишут снова.
Строка перевода Удаляется вместе с основной строкой.

Идентификатор связи внутри JSON-значения компонента не является внешним ключом. Обработчики чтения считают связь отсутствующей, если строки с указанным идентификатором нет, как и CMS.

Для медиа сохраняется столбец идентификатора и добавляется столбец URL. Перемещение репозитория не переносит байты медиа. Перенос переписывает адреса на хранилище владельца. Проверка развёртывания отклоняет результат, если в нём остались URL Dee Wan. Модели страниц создают маршруты Astro из заголовка и документа страницы. Обычные модели данных получают API без автоматически придуманных страниц сайта.

managed-renderer/ есть только тогда, когда генерация всего сайта шла для сайта с активным релизом управляемого рендерера. Это экспорт формата 2 в генерируемой зоне: генерация записывает его, вычисляет контрольные суммы и закрепляет в graduation.manifest.json в поле managedRenderer. Управляемая регенерация никогда его не переписывает. exportDigest закрепления — это SHA-256 файла release/release.json, который содержит канонический JSON и один перевод строки. Прежде чем принять содержимое каталога, генератор проверяет байты экспортированных файлов. Он отклоняет идентификатор, который не совпадает с вычисленным заново, release/managed-renderer.mjs, который не совпадает байт в байт с командой, которую создаёт этот продукт (managed_renderer_export_cli_mismatch), нечитаемое закрепление (managed_renderer_export_pin_invalid) и каталог managed-renderer, написанный в другом регистре. Совместимости со старыми форматами экспорта нет. См. управляемые шаблоны.

Генерация добавляет три корневых скрипта. Каждый добавляется, только если его нет, и никогда не перезаписывается.

Скрипт Что делает
managed-renderer:verify Проверяет закрепление в манифесте и запись релиза с фиксированным набором полей; длину и дайджест каждого файла в managed-renderer/, кроме результатов сборки в его корне, соблюдение лимита размера файла, безопасность и уникальность путей без учёта регистра; скомпилированные части; дайджесты входов, исходников, контракта и релиза; отчёт о проверках, привязанный к релизу, где каждая обязательная проверка пройдена по порядку; template.json, route-map.json, site-chrome.json и wrangler.jsonc; отсутствие ключа хранилища и локального пути в метаданных. Манифест шаблона, его происхождение и грамматику карты маршрутов заново не разбирает. Это делает инспектор генерации.
managed-renderer:build Проверяет, затем выполняет npm ci --ignore-scripts, проверку типов и сборку внутри managed-renderer/, на этой машине и с сетью для npm ci. Отклоняет изменённый lockfile или другой набор инструментов. Проверки контракта, доступности и viewport заново не запускает. Их результаты лежат в release/evidence/report.json.
managed-renderer:prepare Собирает, затем выполняет wrangler deploy --dry-run в managed-renderer/.wrangler/deploy. Ничего не разворачивает.

Запускайте их из корня репозитория. managed-renderer/ и managed-renderer/release/ должны быть настоящими каталогами. build и prepare сначала удаляют node_modules, dist, .astro и .wrangler на верхнем уровне managed-renderer/, а ссылку там удаляют, не переходя по ней. Команды задают ASTRO_TELEMETRY_DISABLED=1 и WRANGLER_SEND_METRICS=false. Ни одна из них не обращается к CMS.

Правка скриптов сохраняет все остальные байты package.json: переводы строк, отступы, метку порядка байтов и последний перевод строки. Когда предыдущий манифест закреплял экспорт, а следующая генерация его больше не записывает, она удаляет только скрипты, значение которых по-прежнему точно равно каноническому, так что ваша собственная команда под одним из этих имён остаётся. Добавление отклоняется с managed_renderer_scripts_unwritable, а удаление — с managed_renderer_scripts_unremovable, если package.json не в UTF-8 или не строгий JSON, его корень или scripts не объект, либо он дважды называет scripts или один из этих скриптов. Добавление также отклоняется, если package.json отсутствует. Удаление из package.json, который ни разу не называет ни один из этих скриптов, ничего не меняет.

Генерируемые workflow, deploy-from-repository.yml и вызывающий его dee-wan-build.yml, разворачивают только API Worker и apps/site. Они никогда не собирают и не разворачивают managed-renderer/. Его wrangler.jsonc не называет маршрута. Тот, кто его развёртывает, должен обслуживать его по адресу https:// плюс активный хост привязки сайта, потому что релиз закреплён за этим origin. Рендерер читает опубликованный контент из /v1 этого origin при каждом запросе, поэтому публикация не требует пересборки, а HTML он отдаёт с Cache-Control: no-store. Привязки сервиса у него нет: его wrangler.jsonc задаёт PUBLIC_API_TRANSPORT равным "public" и включает флаг совместимости global_fetch_strictly_public, поэтому это чтение уходит по публичному HTTPS и попадает в публичный Worker через его маршрут /v1/*.

Сгенерированный Worker использует опубликованный @dee-wan/cache@1.0.1. Чтение всех моделей кешируется по умолчанию. Общий переключатель находится в разделе Настройки → Код и публикация → Аутентификация, кэш и доступ standalone. Выбор для каждой модели — на странице модели → Отдельный API, хранится по id модели, как и её политика доступа. Чтение и запись общего переключателя требуют site:infra. Страница модели открывается с правом чтения этой модели, site:settings или site:infra. Сохранение на ней требует site:infra. Общий переключатель и отдельные значения моделей включаются во входные данные генерации, манифест и идентичность исходников. Изменение только кеша делает артефакт устаревшим: заново сгенерируйте и разверните его. Нечитаемые сохранённые настройки блокируют генерацию до исправления.

Отключение модели исключает из кеша её чтения, переводы и связанные таблицы отношений, а также запросы, затрагивающие исключённую модель. При включённом кеше записи по-прежнему обновляют барьеры. CACHE_ENABLED=false отключает и кеш чтения, и барьеры записей. Любое значение, кроме true и false, отклоняет каждый запрос с cache_switch_invalid. Локальный сервер SQLite не кеширует чтения.

Worker экспортирует CacheCoordinator. Пример, конфигурация сборки и конфигурация развёртывания Wrangler содержат привязку CACHE_COORDINATOR и миграцию SQLite Durable Object. Для каждой БД D1 артефакта нужно одно общее имя координатора для всех процессов записи и версий кода. Разные БД D1 не должны использовать одно имя. Скрипт развёртывания задаёт CACHE_COORDINATOR_NAME из идентичностей артефакта и найденного D1, сохраняя совместимые собственные привязки, миграции и переменные. При ручном развёртывании сохраняйте имя, а при клонировании в другой D1 задайте другое. Два координатора над одним D1 не видят записи друг друга.

В Prisma 6.19.3 расширение guard применяется первым, cache вторым. Guard выполняется и при попадании в кеш. Изменённые им аргументы входят в ключ. После авторизации клиент получает область кеша из principal, варианта доступа, URL, метода и влияющих на результат заголовков, включая язык и арендатора. Временные идентификаторы запросов исключены. В координатор передаётся дайджест области, а не исходные секреты. Проверки аутентификации читают без кеша. Права моделей и операций не расширяются, обращения к Dee Wan не нужны.

До записи D1 команда data:load требует служебный токен записи артефакта и PUBLIC_HOSTNAME. После подтверждённой фиксации она вызывает защищённый endpoint, который проверяет журнал snapshot и выполняет $cacheInvalidate({ all: true }). Ошибка инвалидирования означает ошибку команды даже после фиксации. Повтор той же загрузки повторяет инвалидирование. Для локального D1 также нужен loopback CACHE_INVALIDATION_ORIGIN, для файла SQLite координатор не нужен. Прямые SQL-записи должны использовать $cacheRawWrite или явное инвалидирование после фиксации. Между фиксацией и инвалидированием возможно старое чтение: остановите трафик на время массовой загрузки.

Поколение зависит от манифеста и необязательной эпохи CACHE_GENERATION. Разные поколения с включённым кешем сохраняют корректность, но постоянно сбрасывают кеш и вынуждают заполнять его заново. Нельзя одновременно обслуживать запросы версиями с включённым и отключённым кешем: операции записи при отключённом кеше не устанавливают защитные барьеры. Остановите поступление запросов, дождитесь завершения активных и одновременно обновите все процессы записи. Перед включением после некешированных записей инвалидируйте кеш или смените CACHE_GENERATION. Постепенное развёртывание для смены общего режима не подходит.

По умолчанию TTL свежести равен часу, лимит хранения — 1 GiB. Фоновое обновление использует ctx.waitUntil. Без контекста запроса его нет. Попадание в кеш требует одного обращения к DO; при промахе дополнительно выполняются чтение D1 и заполнение кеша. Операция записи обычно требует двух вызовов DO. Ориентировочный предел одного координатора — около 1 000 запросов в секунду; сам он находится в одной локации. Поэтому из-за сетевого расстояния попадание в кеш может быть медленнее чтения D1. При ошибке кеша чтение выполняется из D1. Если не удаётся получить временное право на запись (lease), запись блокируется. Если завершающий барьер потерян, после истечения lease старое значение может оставаться в кеше до конца TTL. При устранении сбоя учитывайте контракт пакета.

Миграции хранилища DO допускают только обновление, без возврата к прежнему формату. Старый код отклоняет операции с новым форматом хранилища, включая записи Prisma: при смене формата переходите на новый код. Откат кода не восстанавливает базу, хранилище кеша или медиа. Сохраняйте совместимую историю миграций и резервные копии владельца.

Вернитесь к странице Автономный бэкенд: обзор или продолжите со словарём продукта и остальными справочными страницами.


Стабильная версия ещё не выпущена (pre-MVP)
Коммит исходного кода: fe23db481c422b40e317b92fc98116b4c0c26a87 (с локальными изменениями)
Дата сборки (UTC): 2026-10-18T09:22:34.630Z
Коммит для обновления: не определён: в рабочей копии есть изменения. Примечания к выпуску