# PocketBase: умолчания, которые ломают штатный сценарий

**Записка о прошлом.** PocketBase ушла из проекта целиком 2026-08-22 —
[ADR-2026-08-22-storage-without-pocketbase](../adr/ADR-2026-08-22-storage-without-pocketbase.md).
Умолчания ниже принадлежат ушедшей библиотеке и ни на что в сервисе не влияют.
Живое из записки переехало в [../database.md](../database.md), «Настройки с
числовым значением», — потолок размера одной записи, потолок тела запроса и
снятый таймаут чтения, — и в
[ADR-2026-08-15-owner-required-by-schema](../adr/ADR-2026-08-15-owner-required-by-schema.md).

Наблюдения, снятые по ходу задачи `pocketbase-storage` уже на своём коде. От
[записки разведки](pocketbase.md) отличаются предметом: та мерила, **что даёт
панель**, эта — **что библиотека делает молча**, если её не переубедить.

Наблюдения нашлись ревью, а не чтением документации, и все об одном роде промаха:
объявление библиотеки выглядит как «ограничения нет» либо «ограничение есть», а
значит обратное.

## Как снималось

Версия **0.39.10**, та же, что у первой записки. Прогоны — на пустом каталоге
данных во временном каталоге и на поднятом сервере `127.0.0.1:18099`; боевые
данные и ключи не участвовали. Числа сняты 2026-08-11 и 2026-08-12, последнее
наблюдение — 2026-08-15.

## Нулевой потолок у поля файла значит 5 МиБ, а не «без предела»

`&core.FileField{MaxSize: 0}` читается библиотекой как её собственное умолчание:

```
core/field_file.go:28   const DefaultFileFieldMaxSize int64 = 5 << 20
core/field_file.go:310  if f.MaxSize <= 0 { return DefaultFileFieldMaxSize }
```

Проверено укладкой: файл в 6 МиБ отвергается на сохранении записи —
`the maximum allowed file size is 5242880 bytes`. Прогон через боевой роутер дал
границу дословно:

| тело запроса | ответ |
| --- | --- |
| 4 194 304 байта | `201` |
| 5 238 784 байта | `201` |
| 5 246 976 байт | `500` |
| 34 603 008 байт | `413` |

**Цена для сервиса:** 5 МиБ — это примерно 5,5 минут mp3 при 128 кбит/с. Отвергалась
бы не только длинная запись на приёме: результат конвертации в ogg переваливает
тот же порог примерно на пятой минуте, и **уже принятая** задача исчерпывала бы
попытки на шаге конвертации.

## Тело запроса режется на 32 МиБ раньше обработчика

`apis/base.go:36` вешает `BodyLimit(DefaultMaxBodySize)` на **корневой** роутер,
то есть и на чужие маршруты; `apis/middlewares_body_limit.go:14` —
`const DefaultMaxBodySize int64 = 32 << 20`. Ответ `413` уходит мимо обработчика,
без строки в журнале приёма (последняя строка таблицы выше).

Снимается на маршруте: `.Bind(apis.BodyLimit(<своё число>))`.

## Таймаут чтения запроса — пять минут

`apis/serve.go:151` ставит `ReadTimeout: 5 * time.Minute`. Заливка шестичасовой
записи по медленному каналу его переживает: соединение рвётся на середине.
Снимается в хуке `OnServe` — `se.Server.ReadTimeout = 0`.

## Суффикс к имени файла дописывает конструктор, а не укладка

Первая записка наблюдала `sample.ogg → sample_uztrv6wvz3.ogg` и читала это как
свойство хранилища. Наблюдение верно **только когда имя строит сама библиотека**:
десять случайных знаков добавляет `normalizeName`, вызываемый из
`filesystem.NewFileFrom*`. Имя, положенное в поле `File.Name` после
конструктора, ложится на диск дословно:

```
задано  11111111-2222-3333-4444-555555555555.mp3
на диске 11111111-2222-3333-4444-555555555555.mp3
```

**Цена:** тот, кто задаёт имя сам, не получает от суффикса никакой
неугадываемости — и защищать ссылку на файл ему приходится другим.

## Хук правки записи не различает, кто пишет

`app.OnRecordUpdate(<коллекция>)` — событие **модели**: оно срабатывает на каждом
`app.Save`, включая сохранение из собственного кода. Хук, написанный «для
панели», правил записи конвейера: проверено прогоном — задержка, поставленная
шагом вместе со сменой состояния, обнулялась тем же сохранением.

Различает источник `app.OnRecordUpdateRequest(<коллекция>)`: оно поднимается
только на правку запросом, а код, пишущий мимо HTTP-слоя, под него не попадает.

## Приглашение завести владельца панели живёт полчаса

`apis/installer.go:31` — `systemSuperuser.NewStaticAuthToken(30 * time.Minute)`;
печатается только пока владельца нет (`needInstallerSuperuser`). Проверено
прогоном: при первом запуске строка со ссылкой в журнале есть, после заведения
владельца при следующем запуске её нет.

## Обязательность связи проверяется у записи, а не у колонки

`Required` у поля связи — правило **проверки записи при сохранении**, а не
ограничение таблицы. Шаг схемы, объявляющий колонку обязательной на базе, где уже
лежат строки с пустым значением, проходит **зелёным** и такие строки оставляет:

```
core/field_relation.go:156   ColumnType отдаёт TEXT DEFAULT '' NOT NULL — от Required не зависит
core/collection_validate.go  ни одной проверки, читающей существующие строки
```

Проверено прогоном 2026-08-15 на копии хранилища во временном каталоге: строка с
пустым владельцем заведена до шага, шаг применён тем же кодом, что и на подъёме,
и вывод:

```
STEP 003 (Required=true) поверх ничьей записи: err=<nil>
ПОСЛЕ ШАГА: строка на месте, owner=""
Save остановленной ничьей записи: err=failed to update audio record: owner: cannot be blank.
```

Следствие для нас: оставленная строка становится **незакрываемой**. Захват идёт
сырым запросом мимо проверки и выдаёт её воркеру, а всякое сохранение отказывает —
включая то, которым ставится признак остановки. Искать такие строки надо запросом
до выкладки, а не прогоном самого шага: прогон чистую базу от грязной не
отличает. Цена решения записана в
[adr/ADR-2026-08-15-owner-required-by-schema.md](../adr/ADR-2026-08-15-owner-required-by-schema.md).

## Чего эта записка не узнала

- **Во что обходится потолок в 8 ГиБ на диске.** Число выбрано расчётом из
  шестичасовой записи с запасом на видео, а не замером: настоящего распределения
  длин у сервиса нет.
- **Как ведёт себя укладка файла в несколько гигабайт.** Самая длинная проверенная
  запись — 9,6 МБ (десять минут mp3). Потоковую укладку это подтверждает, предел
  — нет.
- **Поведение под одновременной правкой панели и конвейера в бою.** Проверено
  тестом на одной машине, не живой нагрузкой.
- **Сколько строк с пустой связью выдерживает смена признака обязательности.**
  Проверено на одной строке: суть наблюдения — сам факт отсутствия проверки, а не
  её цена на объёме.
