// статьи / статья
#статья #архитектура#sqlite#json#экспорт

> SQLite → JSON → сайт: как данные живут в двух мирах

Почему без API и фреймворков, как работает экспорт, где появляются «призраки» данных и как не потерять контент при переносе.

SkilinPur 25.10.2026 5 мин чтения серия: как это устроено 👁
SQLite → JSON → сайт: как данные живут в двух мирах

Витрина не ходит в базу на каждом запросе. Она читает JSON, который собрала админка. Это быстро, просто и иногда очень больно.

Почему без API

Когда я начинал, у меня не было цели построить «правильную» архитектуру. Была цель — сделать сайт, который я смогу поддерживать один, без DevOps-команды и без зоопарка сервисов.

Поэтому получилось так:

  • SQLite — только в админке. Одна файловая база, WAL, никакого отдельного сервера.
  • PHP — только там, где нужна запись или закрытая страница: кабинет, профили, статьи, проекты.
  • JSON — публичный слой. Сайт читает готовые файлы из data/, а не подключается к базе.
  • Vanilla JS — рендер карточек, фильтры, поиск, статьи. Без React, без сборки, без API-слоя.

Мне нравится эта схема. Статическая витрина не может уронить базу, потому что она к ней не подключена. JSON можно открыть, продиффрить, положить в git или отдать в кэш. Для небольшого портала этого достаточно.

Но у схемы есть цена: два мира. И между ними появляются призраки.

Как данные ходят по кругу

Путь такой:

  1. Я что-то меняю в админке: проект, статью, участника, бейдж.
  2. PHP пишет в SQLite.
  3. После изменения вызывается экспорт: export_all() собирает публичные срезы.
  4. В data/ появляются projects.json, articles.json, team.json, blog.json и остальные файлы.
  5. Сайт читает JSON и рисует интерфейс.

Внутри экспорта нет магии. Это обычные SELECT, сборка массива, json_encode. Например, для статей выбираются только опубликованные, для команды — только активные участники, для проектов — только публичные поля.

Условно:

$rows = $pdo->query("SELECT ... FROM posts WHERE status = 'published'")->fetchAll();
file_put_contents($root . '/data/articles.json', json_encode($rows, JSON_UNESCAPED_UNICODE));

Всё. Никакого брокера, очередей и «источника событий».

Главное правило: база — источник, JSON — проекция

Это звучит очевидно, но именно на этом ломаются.

SQLite — единственный источник правды. Всё, что я хочу сохранить, живёт в базе. JSON — производная. Его можно удалить и собрать заново. Его нельзя править руками «по-быстрому», потому что следующий экспорт затрёт правку. И его нельзя считать независимым хранилищем.

Если это правило нарушить, начинается:

  • В базе запись удалена, а в JSON она осталась — потому что экспорт не запустился.
  • В JSON поправили заголовок, а в админке он старый — потому что база не знает о правке.
  • На dev и prod разные JSON, потому что экспорт делали в разных местах.
  • Браузер держит старый файл, и сайт показывает то, чего уже нет.

Самый неприятный вариант — призрак данных: запись уже удалена, но всё ещё видна на сайте. Или наоборот: запись есть, но её нигде нет, потому что экспорт упал.

Где мы наступили на грабли

21.10.2026: миграция, которая затёрла прод

Это была не ошибка экспорта, а ошибка доверия к скрипту. Старый migrate-to-prod.sh скопировал базу с dev на prod. В теории — «синхронизация». На практике — потеря контента: репорты №11–13, проекты InINet/InINetWin, участники Flexin и Aevornyx.

Прод восстановили из архива /tmp/prod-before-migrate-20261021-0927.tar.gz, а dev-базу потом синхронизировали. Но осадок остался.

После этого появилось правило: перенос кода и перенос контента — разные операции. По умолчанию migrate-to-prod.sh копирует только код. Для базы нужен явный флаг --with-db, и перед ним печатается сравнение: счётчики, id, которых нет в другой базе. Для контента — --with-content.

Урок: скрипт, который «просто синхронизирует», опаснее скрипта, который требует подтверждения.

Бэкап не удаляет файлы

Ещё один призрак — откат. tar восстанавливает то, что было в архиве, но не удаляет файлы, созданные после. Если после бэкапа появился новый js/pjax.js, после отката он останется. И сайт будет вести себя странно: код старый, а кусок нового остался.

Поэтому после отката нужно руками смотреть, что появилось лишнего. Особенно если откат делается не в чистый каталог.

Кэш JSON

Даже если экспорт сработал, браузер может держать старый файл. Поэтому публичные запросы идут с ?t=Date.now() и заголовком no-cache. Это некрасиво, но работает.

Почему не API

Иногда спрашивают: «А почему не сделать нормальный API?»

Можно. Но для текущего масштаба API добавил бы:

  • ещё один слой, который нужно держать живым;
  • авторизацию для публичных чтений;
  • CORS;
  • кэширование;
  • обработку ошибок;
  • ещё одну причину, почему страница пустая.

JSON-файлы дают примерно то же самое, но их нельзя «уронить» запросом и они нормально кэшируются. Для витрины, которая меняется несколько раз в день, этого хватает.

API понадобится, когда:

  • появится мобильное приложение;
  • данные будут меняться в реальном времени;
  • понадобятся сложные фильтры на сервере;
  • несколько клиентов будут писать одновременно.

Пока этого нет — JSON честнее.

Правила, которые спасают

У меня есть небольшой чек-лист. Он скучный, но именно он держит схему живой:

  1. Любое изменение в админке заканчивается экспортом. Не «потом», не «в пятницу».
  2. JSON не редактируется руками. Только через базу.
  3. Перед переносом — сравнение баз. Не «на глаз».
  4. Контент живёт на проде. Dev — для кода. Если нужно перенести контент — только осознанно.
  5. После экспорта — проверить сайт с чистым кэшем. И с грязным тоже.
  6. Бэкап без восстановления — не бэкап. Хотя бы раз в месяц проверить, что архив разворачивается.
  7. Новый публичный файл — в sitemap или в исключения. Иначе он либо потеряется, либо попадёт туда, где не нужен.

Когда схема не подходит

Я не пытаюсь продать «SQLite + JSON» как универсальную архитектуру. Она не подходит, если:

  • пользователи пишут одновременно и нужны транзакции между сущностями;
  • данных столько, что JSON становится больше разумного;
  • нужен поиск по полнотексту на сервере;
  • несколько сервисов должны читать одни и те же данные;
  • без API уже нельзя.

Для портала команды, где контент меняется вручную и читателей немного, схема работает. Она предсказуемая. Я знаю, где лежит каждая запись и что произойдёт после нажатия кнопки.

Вместо вывода

«Два мира» — это не бесплатно. За простоту публичной части платишь дисциплиной в админке.

Если бы я начинал заново, я бы всё равно выбрал SQLite и JSON. Но сразу сделал бы три вещи: сравнение баз перед миграцией, обязательный экспорт после изменений и проверку восстановления бэкапа. Тогда призраков было бы меньше.

Сайт — это не только страницы. Это ещё и путь данных. И если этот путь не описан, рано или поздно данные пойдут не туда.

// реакции
SkilinPurучастник InIProject