- разведка storage-without-pocketbase: шесть ролей библиотеки в этом коде, отпавший довод перевода, шесть модулей достижимы только через неё - ADR-2026-08-22-storage-without-pocketbase заменяет три записи: перевод в PocketBase, очередь коллекцией, файл за защищённым полем - задача storage-without-pocketbase встала первой строкой плана стройки
11 KiB
Хранилище: 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 оговаривает, что копии сервер делает своими средствами.
-
Вход через её провайдер OIDC — отпал. Цитата, которой ADR отверг вариант «взять библиотеку только хранилищем»:
Панель показывает свою коллекцию пользователей и ничего больше. Отсюда следствие для целевого входа: пользователи Authelia в панели не появятся, если вход делает само приложение.
Вход делает само приложение с 2026-08-22 (ADR-2026-08-22-login-by-trusted-header): пришедшего называет заголовок прокси, а учётную запись заводит наш
EnsureUser. Пользователи в коллекции есть потому, что их пишет наш код, а не провайдер библиотеки. Довод, которым отвергнут отвергнутый вариант, перестал быть верным — и вместе с ним отпало основание держать вход в библиотеке.
Что оплачено и не работает
- Захват записи идёт сырым запросом мимо записей коллекции
(
record_repo.go,FindAndAcquire):app.DB().NewQueryсUPDATE … RETURNING. На самом горячем месте абстракция не помогает, а её ограничения действуют — хуки коллекции не срабатывают, время изменения проставляет наш запрос, времена сравниваются строками побайтово. - Налог соседства двух периметров в одном процессе и на одном порту.
Появился секрет, которого не было, — пароль суперпользователя. Панель
опубликована в интернет, и её барьер обходится подменой знака:
/%5f/попадает в ту же группу, что/_/, а правило прокси написано на литерал (../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.
Обстоятельство, которое решило дело: на сервере данных нет и сервис остановлен,
поэтому смена стоит только кода. Дешевле она не станет никогда — каталог
internal/controller/http прирастает кодом на чужих типах с каждой задачей.