# Хранилище: PocketBase против голого SQLite с каталогом файлов Отвечает на вопрос владельца от 2026-08-22: не окажется ли голый SQLite с каталогом аудиозаписей гибче встроенной PocketBase. Записи в каталоге задач у вопроса нет — он поднят по ходу работы, и разведка идёт от него, а не от постановки. Мерилось **дерево репозитория**, а не внешний сервис: вопрос о том, что библиотека уже держит в этом коде и чем за это плачено. Замеров на живом потоке нет. ## Как снималось Дата — 2026-08-22, коммит `a8fb479`, дерево чисто. Всё считано в самом репозитории, боевые данные не участвовали. - **Пакеты в сборке:** `go list -deps ./... | wc -l` → **584**; из них с `pocketbase` в пути — `go list -deps ./... | grep -c pocketbase` → **40**. - **Кто тянет тяжёлый модуль:** `go mod why -m <модуль>` по семи модулям. - **Объём кода:** `find <каталог> -name '*.go' [! -name '*_test.go'] | xargs wc -l`. - **Протечка библиотеки за адаптер:** `grep -l "pocketbase/" internal/controller/http/*.go`. - **Узлы для варианта с монтированием** — чтением кэша модулей: `router.Router.BuildMux()` отдаёт `http.Handler` (`tools/router/router.go:61`), а `apis.Serve` присваивает `e.Server.Handler` (`apis/serve.go:223`). Прототипа на этих двух узлах не собирал — проверено только их существование. ## PocketBase здесь — фреймворк приложения, а не хранилище Разрез «хранилище против хранилища» вопроса не покрывает: библиотека держит шесть ролей сразу, и только две из них про хранение. | Роль | Где | Чем заменяется | | --- | --- | --- | | SQLite без CGO, пул записи одним соединением | `pbrepo.New` | `modernc.org/sqlite` напрямую, свои WAL, `busy_timeout` и единственный писатель | | Схема и шаги миграций | `internal/adapter/repo/pocketbase/migrations`, 1039 строк | свой раннер либо `goose`, который тут уже был | | Файлы записей на диске и отдача `/api/files/…` по токену | `file_repo.go`, `FileTokenPath` | каталог и свой обработчик отдачи | | Маршрутизатор, цепочка слоёв с приоритетами, ограничитель частоты | `internal/controller/http` целиком | `net/http` и счётчик по ключу | | Учётная запись значением `e.Auth` | `identity.go` | строка своей таблицы | | Панель `/_/` | покупалась ради неё | ничем | **Библиотека вышла за адаптер.** Из пяти не-тестовых файлов `internal/controller/http` её импортируют **все пять**, из шести тестовых — **все шесть**: 1497 строк кода контроллера и 3075 строк его проверок написаны на `*core.RequestEvent`. Адаптер хранилища — ещё 1787 строк без шагов схемы. ## Три довода прежнего решения: один умер 2026-08-22 [ADR-2026-08-11-pocketbase-storage-with-admin-panel](../adr/ADR-2026-08-11-pocketbase-storage-with-admin-panel.md) покупал библиотеку за три вещи разом, и отвергал половинчатые пути тем, что порознь ни одна перевода не оправдывает. 1. **Правка записей в панели** — работает. Единственная роль, которой сегодня нет замены. 2. **Файлы видны в панели, и копия накрывает их вместе с базой** — работает; тот же ADR оговаривает, что копии сервер делает своими средствами. 3. **Вход через её провайдер OIDC** — **отпал**. Цитата, которой ADR отверг вариант «взять библиотеку только хранилищем»: > Панель показывает свою коллекцию пользователей и ничего больше. Отсюда > следствие для целевого входа: **пользователи Authelia в панели не появятся, > если вход делает само приложение**. Вход делает само приложение с 2026-08-22 ([ADR-2026-08-22-login-by-trusted-header](../adr/ADR-2026-08-22-login-by-trusted-header.md)): пришедшего называет заголовок прокси, а учётную запись заводит наш `EnsureUser`. Пользователи в коллекции есть **потому, что их пишет наш код**, а не провайдер библиотеки. Довод, которым отвергнут отвергнутый вариант, перестал быть верным — и вместе с ним отпало основание держать вход в библиотеке. ## Что оплачено и не работает - **Захват записи идёт сырым запросом мимо записей коллекции** (`record_repo.go`, `FindAndAcquire`): `app.DB().NewQuery` с `UPDATE … RETURNING`. На самом горячем месте абстракция не помогает, а её ограничения действуют — хуки коллекции не срабатывают, время изменения проставляет наш запрос, времена сравниваются строками побайтово. - **Налог соседства двух периметров в одном процессе и на одном порту.** Появился секрет, которого не было, — пароль суперпользователя. Панель опубликована в интернет, и её барьер обходится подменой знака: `/%5f/` попадает в ту же группу, что `/_/`, а правило прокси написано на литерал ([../security.md](../security.md)); дефект не закрыт. Узнавание нельзя повесить на всю поверхность хранилища, иначе узнанный перепишет себе `provider_login` и заберёт чужой архив — область слоя сужена, и причина стоит абзацем в `identity.go`. - **Пространство `/api/` нельзя закрыть на прокси**, потому что за файлами ходит туда браузер пользователя. Своя отдача файла снимает это ограничение: закрыть можно всё пространство хранилища разом. ## Чем платит уход - **Панель теряется целиком.** Правка записи, возврат остановленной в работу, просмотр очереди фильтром, прослушивание файла — всё это сегодня живёт только там. - **Пишем сами** шаги схемы, отдачу файла, ограничитель частоты и настройку базы. Последняя — не формальность: у `modernc.org/sqlite` запись идёт единственным соединением, и библиотека держит это за нас двумя пулами. - **Раскладка каталога данных** названа необратимой в `../../CLAUDE.md`. На стройке цена нулевая: на сервере пусто, переносить нечего. ## Что уходит из сборки, а что остаётся `go mod why -m` показал, что шесть модулей достижимы **только** через PocketBase: `disintegration/imaging` (через `tools/filesystem`), `domodwyer/mailyak/v3` и `golang-jwt/jwt/v5` (через `core`), `ganigeorgiev/fexpr` (через `apis`), `spf13/cobra` (набор команд), `go-sql-driver/mysql` (через `pocketbase/dbx`). `modernc.org/sqlite` остаётся: он нужен и без библиотеки, и требование обходиться без CGO с ним сохраняется. ## Варианты и что выбрано | Вариант | Что делает | Чем платит | | --- | --- | --- | | Оставить как есть | ноль работы | долг растёт с каждой задачей на `RequestEvent` | | **Уйти целиком** | SQLite напрямую, каталог файлов, свои шаги схемы и маршруты | панель теряется сразу, работа одной порцией | | Уйти в два шага | сначала снять с периметра HTTP, панель оставить; потом с хранилища | панель жива в промежутке, работа та же, но двумя порциями | | Локализовать протечку | библиотека не выходит за `internal/adapter/repo` | решение откладывается, панель остаётся | **Решением владельца от 2026-08-22 взят уход целиком, и панель не заменяется ничем**: пока стройка не кончилась, остановленную запись возвращают в работу запросом к базе. Решение записано в [ADR-2026-08-22-storage-without-pocketbase](../adr/ADR-2026-08-22-storage-without-pocketbase.md). Обстоятельство, которое решило дело: на сервере данных нет и сервис остановлен, поэтому смена стоит только кода. Дешевле она не станет никогда — каталог `internal/controller/http` прирастает кодом на чужих типах с каждой задачей.