# Хранилище: 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` прирастает кодом на чужих типах с каждой задачей.
