От идеи до продакшена

Учебный маршрут: строим MVP с AI-агентами.

Модуль 13

Где живут данные

Проверяем слой хранения: JSON/localStorage, локальная база или PostgreSQL, фиксируем storage decision и export JSON.

К практике
1Разбор ситуации
Ситуация

Борис спрашивает, где живут данные

После m12 JobTrack выглядит прилично. Сценарий проходит, UI не мешает.

Я добавляю вакансии, они сохраняются — и тут ловлю неприятную мысль: где именно они сохраняются?

Вариантов несколько, и живут данные очень по-разному.

Массив в state — после перезагрузки всё исчезло. localStorage — MVP работает, но только на одном устройстве и без нормальных запросов, миграций, контроля формата. А маленькая локальная база живёт дольше, но всё ещё без командной работы и синхронизации.

Готовая чужая база, которую держишь не ты — Supabase или Firebase — приезжает вместе с сервером, свой backend писать не надо. А следом приезжают внешняя зависимость, лимиты и счёт за всё это.

PostgreSQL на своём backend — это уже нормальная серверная модель. Но тогда на тебя разом падает всё: auth, хостинг, бэкапы, миграции и деньги.

Короче, плохой prompt звучит так:

Добавь базу данных.

И это опасно.

Агент может притащить Prisma, PostgreSQL, auth, новый backend, миграции, Docker и деплой — всё сразу. На бумаге выглядит серьёзнее, а для текущего MVP это может быть грузовик бетона на табуретку.

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

И вот это я хочу решить сам, до агента: что пользователь потеряет при reload, смене браузера, очистке storage, переезде на другой девайс. Как вытащить данные наружу, если MVP пока живёт локально. В каком они формате и до какого размера дорастут, прежде чем всё придётся переделывать.

Он оказался перед storage decision, который нельзя заменить словом база. У данных должен быть дом, формат, export и понятная граница роста.

Одним словом база тут не отделаешься. Сначала решаю, где живут данные, — а потом уже говорю агенту, что делать.

2Принцип
Теория

Где живут данные

Модуль 13 — про то, где живут данные.

Не про большую архитектуру ради красивой схемы. К этому моменту у JobTrack уже есть рабочий slice, значит данные уже где-то живут. Вопрос простой: это осознанное решение или случайный побочный эффект первого кода?

Правильный порядок такой:

— В m8/m9 выбрать storage decision как часть stack/spec; — в m10 заложить задачи на модель данных, сохранение и export; — в m11 сделать первый executable slice на выбранном минимальном storage; — В m13 проверить границы хранения, export, риски и следующий слой роста.

Если m13 впервые поднимает вопрос, а где база данных, это нормально. В m8/m9 storage мог остаться неявным. Модуль 13 и существует, чтобы сделать выбор хранения явным.

Этот шаг закрывает скилл-чекпоинт data-boundaries в фазе 6. SPP держит память о проекте в docs/spp/pipeline-state.md: что за продукт, что уже сделано, какие решения приняты. Поэтому работу можно прервать и продолжить в новой сессии — следующий модуль это следующая фаза того же pipeline.

Короче, тут мы не выбираем базу ради базы. Мы фиксируем, где живут данные, почему именно там, что пользователь может потерять и когда это решение надо пересматривать.

3 слоя хранения

Я насчитал 3 нормальных слоя хранения для маленького продукта.

Первый — JSON или localStorage.

JSON/localStorage

Самый быстрый старт. Для JobTrack он нормален, если цель — проверить, помогает ли ручной список вакансий вести поиск работы одному человеку.

Плюсы: быстро сделать, не нужен backend, удобно для локального demo, легко добавить export JSON, меньше инфраструктуры.

Минусы начинаются там же: данные привязаны к браузеру или файлу. Одна очистка storage — и всё пропало. Нет нормальной синхронизации, тяжело жить на нескольких устройствах, миграции формата придётся продумывать руками.

Что обязательно: явное предупреждение о локальном хранении, export JSON, версия формата, время экспорта, понятная модель Job и никаких секретов в export.

Локальная база

Второй слой — локальная база.

SQLite для desktop/local app, IndexedDB для браузерного offline-first сценария, embedded database внутри локального инструмента.

Следующий шаг, если данных становится больше или нужен устойчивый формат на одном устройстве. Тут уже есть структура: индексы, запросы, проще мигрировать формат, удобнее жить в offline/local-first продукте.

Но цена есть. Сложнее дебажить, муторнее export/import, командной работы без синхронизации всё ещё нет, а агент может радостно притащить лишнюю библиотеку и миграции раньше времени.

Для JobTrack локальная база нужна не в первом MVP, а когда появятся реальные причины: много вакансий, история изменений, offline-first, desktop app или сложные фильтры.

PostgreSQL

Третий слой — PostgreSQL.

Нормальный выбор, когда продукт становится серверным: аккаунты, доступ с нескольких устройств, командная работа, серверные фильтры и отчёты, стабильные бэкапы, боевая эксплуатация, интеграции с внешними сервисами.

Но PostgreSQL не приходит один. Он приносит backend/API, auth, migrations, hosting, backups, секреты и env, права доступа, тестовую базу и продовую базу.

Если пользователь один и проверяет гипотезу руками, PostgreSQL преждевременен. Не потому что PostgreSQL плохой. Просто он тянет за собой кучу решений, которых в MVP ещё никто не проверял.

Что должно быть в data model

Storage layer не заменяет модель данных. Модель должна быть описана явно. Для JobTrack минимум выглядит так:

type Job = {
  id: string;
  title: string;
  company: string;
  url?: string;
  source: string;
  status: string;
  nextStep?: string;
  notes?: string;
  createdAt: string;
  updatedAt: string;
};

Поля source и status могут опираться на справочники/config. Но справочник — это часть модели, а не весь модуль. Настоящая граница другая: где хранится Job, как он меняется, как экспортируется, как переживает reload и как будет мигрировать.

Export JSON

Export JSON для локального MVP обязателен. Человек вносит вакансии руками — он должен уметь их забрать, иначе одна очистка браузера и весь его труд в мусорке. Export должен содержать version, timestamp, jobs и реальные поля Job, которые уже есть в MVP. Без токенов, ключей, session data, дампов и stack trace: no secrets.

Проверка простая: создать 2–3 вакансии, сделать export, открыть файл руками, убедиться, что JSON парсится и содержит ожидаемые данные. Никакой магии, просто нормальная инженерная гигиена.

Как выбрать storage decision

Prompt для m13 может быть таким:

Проверь storage decision для JobTrack.

Контекст:
- есть accepted docs/design.md;
- есть docs/plan.md;
- есть working slice после m11/m12;
- нужно понять, где сейчас живут данные и соответствует ли это MVP.

Сравни 3 слоя:
1. JSON/localStorage;
2. локальная база: SQLite/IndexedDB/embedded DB;
3. PostgreSQL.

Для каждого слоя дай:
- что пользователь получает;
- что пользователь теряет;
- какие файлы/части приложения меняются;
- какие риски;
- когда слой станет недостаточным;
- какой минимальный next step.

Не предлагай переписать приложение без доказанной причины.

Как принять решение в m13

Результат m13 — не обязательно новая база. Чаще так:

Storage decision:
- Сейчас: localStorage + JSON export.
- Почему: один пользователь, ручная проверка гипотезы, нет auth и синхронизации.
- Риски: данные только в текущем браузере, нужен явный export.
- Следующий слой: PostgreSQL, если появятся аккаунты или доступ с нескольких устройств.
- Не сейчас: auth, backend API, migrations, production DB.

Это сильнее, чем молча воткнуть PostgreSQL — тут видно решение, его границы и когда его пересматривать.

Diff review

Diff m13 должен быть storage/data slice: data model или storage adapter, JSON export или проверка существующего export, явное описание storage decision, минимальная подпись про локальное хранение, если нужна, tests или описанная ручная проверка.

Чего там быть не должно: UI redesign, auth, search, import, tags, новый backend без принятого решения и миграция на PostgreSQL просто потому что звучит взросло.

PostgreSQL не делает MVP взрослее. Чаще просто тяжелее.

Работаем от продукта, а не от желания поставить взрослую базу и преисполниться в своём познании)

Дополнительный материал

Для ознакомления перед практикой

Короткие разборы, которые помогают пройти упражнение без лишнего контекста в основном тексте.

3Практика
Практика

Проверить и оформить хранение данных

Цель практики — понять, где живут данные твоего проекта, зафиксировать storage decision и проверить export JSON. Не надо автоматически добавлять PostgreSQL. Надо выбрать слой хранения под текущий MVP, а не ставить взрослую базу ради ощущения взрослости.

Шаг 0. Запусти /spp

Это уже рабочий продуктовый slice, поэтому storage нельзя менять prompt-ом добавь базу. Сначала попроси агента выбрать workflow и повторить границы задачи:

Используй /spp.

Мы на модуле 13. Нужно проверить storage decision для моего проекта.

Цель:
- понять, где данные хранятся сейчас;
- сравнить JSON/localStorage, локальную базу и PostgreSQL;
- зафиксировать storage decision;
- добавить или проверить JSON export;
- не делать auth, import, search, UI redesign или production DB без причины.

Перед кодом повтори expected diff, checks и rollback boundary.

Шаг 1. Подними решения из spec и plan

Открой:

  • docs/design.md;
  • docs/plan.md;
  • docs/todo.md;
  • текущий код storage/save/export.

Найди, где раньше было принято решение о хранении. Если его нет, это нормально: в m8/m9 storage мог остаться неявным. Запиши его явно сейчас — для этого и нужен модуль 13.

Шаг 2. Проверь текущий storage

Prompt:

Inspect current project storage.

Find:
- where jobs are stored now;
- what happens after page reload;
- what happens in an incognito window or another browser/device (a fresh state without your localStorage);
- whether there is JSON export;
- what fields are persisted;
- whether source/status are part of a clear data model;
- whether there are secrets/debug fields in persisted/exported data.

Do not change code yet.
Return facts with file paths.

Шаг 3. Сравни 3 слоя хранения

Попроси агента сравнить варианты:

Compare storage options for current MVP:
1. JSON/localStorage;
2. local database: SQLite/IndexedDB/embedded DB;
3. PostgreSQL.

Use current docs/design.md and docs/plan.md.

For each option explain:
- why it fits or does not fit current MVP;
- what user gets;
- what user risks;
- implementation cost;
- hosting/deploy impact;
- when we should move to the next layer.

Recommend one option for now.

Шаг 4. Прими storage decision

Запиши решение в docs/design.md или docs/todo.md, в зависимости от структуры проекта.

Формат:

## Storage decision

Current layer:
- <JSON/localStorage | local DB | PostgreSQL>

Why now:
- ...

User-visible limits:
- ...

Required safeguards:
- export JSON;
- format version;
- timestamp;
- no secrets;
- manual export check.

Next layer trigger:
- move to PostgreSQL when ...

Not now:
- auth;
- backend API;
- production DB;
- migrations beyond current format.

Шаг 5. Проверь модель данных

Опиши минимальную модель Job:

  • id;
  • title;
  • company;
  • url;
  • source;
  • status;
  • nextStep;
  • notes;
  • createdAt;
  • updatedAt.

Если source/status захардкожены в 3 местах, вынеси их в config. Но не превращай модуль в рефактор всех справочников. Справочники здесь только часть data model.

Шаг 6. Добавь или проверь JSON export

Если export уже есть, проверь его. Если нет, добавь минимальный export под выбранный storage.

Export должен содержать:

  • version;
  • timestamp;
  • jobs;
  • поля Job, которые есть в MVP;
  • no secrets.

Prompt:

Add or verify JSON export for current storage layer.

Constraints:
- no auth;
- no import;
- no search;
- no UI redesign;
- no PostgreSQL migration unless storage decision explicitly requires it.

Export must include:
- version;
- timestamp;
- jobs;
- no secrets/debug fields.

Before code, show expected diff and checks.

Шаг 7. Проверь руками

Сделай manual check:

  1. Добавь 2–3 вакансии.
  2. Перезагрузи страницу и проверь, что данные ведут себя так, как обещает storage decision.
  3. Сделай export JSON.
  4. Открой JSON руками.
  5. Проверь version, timestamp, jobs.
  6. Проверь, что source/status сохранились.
  7. Проверь, что секретов и debug dump нет.

Если выбранный storage не сохраняет данные после reload, это допустимо только если storage decision честно говорит об этом. Но для JobTrack такой MVP слабый: пользовательские вакансии не должны исчезать незаметно.

Шаг 8. Зафиксируй следующий слой роста

Запиши trigger для перехода:

  • на локальную базу, если нужен offline/local-first и больше структурированных запросов;
  • на PostgreSQL, если нужны аккаунты, несколько устройств, команды, серверные фильтры или production backups.

Не делай переход сейчас без причины. Запиши условие, при котором он нужен.

Шаг 9. Проверь diff и commit boundary

Diff должен быть storage/data slice:

  • storage decision в docs;
  • data model/config, если нужен;
  • JSON export;
  • minimal UI copy о локальном хранении, если нужно;
  • tests или documented manual fallback.

Запрещено:

  • UI redesign;
  • auth;
  • search;
  • import;
  • tags;
  • новая production DB без принятого решения;
  • переписывание всего state management.