Передать проект — значит дать команде всё, чтобы она запустила, проверила и выпустила его без автора: README с запуском с нуля, .env.example со всеми переменными, одну команду проверки, список внешних сервисов с владельцами аккаунтов, чек-лист релиза и отката и честный список известных ограничений. Готовность проверяется репетицией: человек, который не писал код, клонирует репозиторий в чистую папку и выполняет README строго по шагам.
После материала вы сможете
- написать README с запуском с нуля и проверить его репетицией в чистом клоне
- сделать
.env.exampleчастью проверки: тест падает, если код читает переменную, которой нет в файле - собрать
npm run checkи закрепить версию Node.js черезenginesиengine-strict - составить таблицу внешних сервисов с владельцами и чек-лист релиза и отката с проверенными командами
pg_dumpиpg_restore
Что знать заранее
Проверено на
Node.js 24 LTS (24.18), npm 11.16Express 5.3, zod 4.6Prisma ORM 7.10.0PostgreSQL 17 (postgres:17-alpine), Docker Compose v2Vitest 5.0.4, supertest 7.3, TypeScript 6.0.3
Сверено с документацией: 12 октября 2026 г.
Содержание
Что входит в пакет передачи
MVP, собранный с AI-ассистентом, часто работает только на одном ноутбуке: там лежит .env, сгенерирован клиент базы, запущен нужный контейнер, а порядок команд помнит автор и история чата с агентом. Новая команда, подрядчик или инвестор на техническом аудите получают репозиторий без этого контекста. Пакет передачи возвращает контекст в репозиторий — в виде файлов, которые можно проверить.
README: запуск с нуля
- На какой вопрос отвечает
- Что поставить и какие команды выполнить на чистой машине
- Как проверить
- Репетиция: чистый клон, команды строго по README
.env.example и проверка при старте
- На какой вопрос отвечает
- Какие переменные нужны и где взять значения
- Как проверить
- Тест сверяет файл с кодом; приложение падает с понятным списком
npm run check
- На какой вопрос отвечает
- Как понять, что ничего не сломано
- Как проверить
- Та же команда в CI на каждый pull request
Внешние сервисы и владельцы
- На какой вопрос отвечает
- Где живут домен, хостинг, база, боты и кто владеет аккаунтами
- Как проверить
- Каждый аккаунт открывается под учётной записью компании
Чек-лист релиза и отката
- На какой вопрос отвечает
- Как выпустить версию и как вернуть предыдущую
- Как проверить
- Команды резервной копии и восстановления выполнены хотя бы раз
Известные ограничения
- На какой вопрос отвечает
- Что не сделано или сделано упрощённо
- Как проверить
- Тесты, которые фиксируют это поведение
Учебный проект — API заявок с сайта: Express 5, Prisma 7.10, PostgreSQL 17 в Docker Compose и необязательные уведомления в Telegram. Все файлы ниже взяты из него, а команды выполнены.
README: запуск с нуля
Первая версия README, которую часто пишет ассистент, выглядит убедительно и не работает. Вот что получилось, когда мы выполнили её дословно в чистом клоне:
## Quick start
```bash
git clone <repository-url> leads-mvp
cd leads-mvp
npm install
npm run dev
```$ npm install
4 high severity vulnerabilities
$ npm run dev
> tsx watch --env-file=.env src/server.ts
node: .env: not found
$ cp .env.example .env && npx tsx --env-file=.env src/server.ts # next guess
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '…\src\generated\prisma\client.js' imported from …\src\db.tsУ автора всё это уже было на машине, поэтому шаги выпали незаметно. По документации GitHub, README отвечает на вопросы, что делает проект, зачем он нужен, как начать с ним работать, где получить помощь и кто его поддерживает. Для передачи главный — «как начать»: требования с версиями, каждая команда по порядку и проверка результата.
# Leads MVP
API for the website contact form: stores leads in PostgreSQL and can notify
a Telegram chat about each new lead.
## Requirements
- Node.js 24 and npm 11 (`node -v`, `npm -v`)
- Docker with Compose v2 (`docker compose version`) for the local database
- Free ports: 3080 for the API and 55439 for PostgreSQL. To use other ports,
change `PORT`, `DB_PORT` and the port in `DATABASE_URL` in `.env`
## Quick start from zero
Commands are for bash: macOS, Linux, WSL or Git Bash on Windows.
```bash
git clone <repository-url> leads-mvp
cd leads-mvp
cp .env.example .env
docker compose up -d --wait db
npm ci
npx prisma generate
npm run db:migrate
npm run db:seed
npm run check
npm run dev
```
In another terminal: `curl http://localhost:3080/health` returns
`{"status":"ok"}`.
## Commands
| Command | What it does |
| --- | --- |
| `npm run dev` | API with reload on file changes |
| `npm run check` | Type check and tests. Must pass before every merge |
| `npm run db:migrate` | Applies pending migrations (`prisma migrate deploy`) |
| `npm run db:seed` | Adds a demo lead. Local database only |
| `docker compose down` | Stops the database; add `-v` to delete its data |
## Project structure
- `src/app.ts` — HTTP routes and input validation
- `src/env.ts` — environment variables, checked at startup
- `src/db.ts` — database access (Prisma)
- `prisma/` — schema, migrations, seed
- `tests/` — API tests without a real database
- `docs/HANDOFF.md` — external services and account owners
- `docs/RELEASE.md` — release and rollback checklist
## Configuration
Every variable is listed with a comment in `.env.example`; a test fails
if the code reads a variable that is not there. Production values are set
in the hosting provider's settings and are never committed.
## Known limitations
- `POST /api/leads` has no rate limiting or captcha: spam is possible.
- Admin access is one shared `ADMIN_TOKEN`; there are no users or roles.
- If Telegram is unreachable, the request returns 500 after the lead
is already saved, and a retry by the user creates a duplicate.
- `GET /api/leads` returns the latest 100 leads, without pagination.
- Tests use an in-memory store; the database is checked only by the
quick start above.
- On 12 Oct 2026 `npm audit` reports 4 high-severity advisories in the
dependencies of the `prisma` CLI (`mysql2`, `deepmerge-ts`). The suggested
fix downgrades Prisma to 6.x, so it is not applied. Re-check after
Prisma updates.
## Contacts
- Product owner: `<name, email>`
- Technical contact: `<name, email>`npm ci, а неnpm install: по документации npmnpm ciставит ровно то, что вpackage-lock.json, падает при расхождении сpackage.jsonи никогда не переписывает lock-файл.docker compose up -d --wait dbждёт, пока сервис станет healthy, поэтому следующая команда не упадёт на неготовой базе. Для этого вcompose.yamlнуженhealthcheck.- Миграции применяет
prisma migrate deploy— та же команда, что на сервере.migrate devдокументация Prisma называет командой разработки, которую нельзя использовать в продакшене. - Последняя строка — проверка результата:
/healthотвечает{"status":"ok"}. Без неё непонятно, получилось ли.
services:
db:
image: postgres:17-alpine
environment:
POSTGRES_USER: leads
POSTGRES_PASSWORD: leads_local_only
POSTGRES_DB: leads
ports:
- "${DB_PORT:-55439}:5432"
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U leads -d leads"]
interval: 2s
timeout: 3s
retries: 15
volumes:
db-data:Переменные окружения: .env.example и проверка при старте
.env.example — единственное место, где новая команда узнаёт, какие переменные нужны. В нём нет секретов: только имена, безопасные локальные значения, комментарии, как получить настоящие, и пометка, какие переменные необязательны. Как хранить сами секреты и что делать при утечке, разобрано в материале о секретах.
# Copy to .env for local development: cp .env.example .env
# Production values live in the hosting provider's settings, never in git.
# PostgreSQL. The local value matches compose.yaml.
DATABASE_URL="postgresql://leads:leads_local_only@localhost:55439/leads"
# HTTP port of the API.
PORT=3080
# Token for GET /api/leads (Authorization: Bearer ...), at least 32 characters.
# Generate one: node -e "console.log(require('node:crypto').randomBytes(32).toString('hex'))"
ADMIN_TOKEN="replace-with-a-random-string-of-32-plus-chars"
# Optional: Telegram notifications about new leads. Leave empty to disable.
TELEGRAM_BOT_TOKEN=""
TELEGRAM_CHAT_ID=""Приложение проверяет переменные при старте и сразу перечисляет, чего не хватает, — вместо падения через полчаса на первом запросе к базе:
import { z } from "zod";
const Env = z.object({
DATABASE_URL: z.url(),
PORT: z.coerce.number().int().positive().default(3080),
ADMIN_TOKEN: z.string().min(32, "must be at least 32 characters"),
TELEGRAM_BOT_TOKEN: z.string().optional(),
TELEGRAM_CHAT_ID: z.string().optional(),
});
export type Env = z.infer<typeof Env>;
export const ENV_KEYS = Object.keys(Env.shape);
// Fails fast at startup with a list of what is missing, instead of a crash later.
export function loadEnv(source: NodeJS.ProcessEnv = process.env): Env {
const parsed = Env.safeParse(source);
if (!parsed.success) {
const problems = parsed.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`);
console.error(`Invalid environment. Compare .env with .env.example:\n${problems.join("\n")}`);
process.exit(1);
}
return parsed.data;
}$ npx tsx src/server.ts # started without --env-file, .env not loaded
Invalid environment. Compare .env with .env.example:
- DATABASE_URL: Invalid input: expected string, received undefined
- ADMIN_TOKEN: Invalid input: expected string, received undefinedЧтобы .env.example не отставал от кода, его сверяет тест. Агент добавил переменную в src/env.ts, но забыл про пример — проверка падает до слияния:
import { readFileSync } from "node:fs";
import { expect, it } from "vitest";
import { ENV_KEYS } from "../src/env.js";
// .env.example is part of the handoff: it must list every variable the code reads.
it(".env.example documents every environment variable", () => {
const documented = readFileSync(".env.example", "utf8")
.split("\n")
.map((line) => line.match(/^([A-Z0-9_]+)=/)?.[1])
.filter(Boolean);
expect(documented.sort()).toEqual([...ENV_KEYS].sort());
});$ npx vitest run tests/env-example.test.ts # SENTRY_DSN added to src/env.ts only
FAIL tests/env-example.test.ts > .env.example documents every environment variable
- Expected
+ Received
[
"ADMIN_TOKEN",
"DATABASE_URL",
"PORT",
- "SENTRY_DSN",
"TELEGRAM_BOT_TOKEN",
"TELEGRAM_CHAT_ID",
]Одна команда проверки и закреплённые версии
Команда, которая принимает проект, должна одной командой понять, цел ли он. npm run check объединяет проверку типов и тесты; её запускают человек, агент и CI. Как сделать её обязательной в pull request, показано в уроке о тестах для сгенерированного кода и в уроке по GitHub Actions.
{
"scripts": {
"dev": "tsx watch --env-file=.env src/server.ts",
"start": "tsx --env-file=.env src/server.ts",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"check": "npm run typecheck && npm test",
"db:migrate": "prisma migrate deploy",
"db:seed": "tsx --env-file=.env prisma/seed.ts"
},
"engines": {
"node": ">=24 <25"
}
}engine-strict=trueПоле engines по документации npm носит рекомендательный характер, пока не включён engine-strict. С ним npm ci на неподходящей версии Node.js останавливается сразу, а не через непонятную ошибку сборки. Проверили на копии проекта, где требование подняли до Node.js 26:
$ node -v
v24.18.0
$ npm ci # test copy: engines.node ">=26", .npmrc engine-strict=true
npm error code EBADENGINE
npm error notsup Not compatible with your version of node/npm: leads-mvp@1.0.0
npm error notsup Required: {"node":">=26"}
npm error notsup Actual: {"node":"v24.18.0","npm":"11.16.0"}npm ci в чистом клоне показал ещё одну вещь, которую стоит передать, а не прятать: предупреждения аудита зависимостей. Здесь они приходят из CLI Prisma, а предложенное исправление откатывает Prisma на 6.x. Такое решение принимает команда, поэтому оно записано в README в разделе Known limitations с датой проверки.
$ npm audit
deepmerge-ts <8.0.0
Severity: high
mysql2 <=3.23.0
Severity: high
fix available via `npm audit fix --force`
Will install prisma@6.19.3, which is a breaking change
4 high severity vulnerabilitiesВнешние сервисы и владельцы аккаунтов
Код — только часть продукта. Домен, хостинг, база, Telegram-бот, платёжный кабинет, аккаунты AI-ассистентов с ключами API часто зарегистрированы на личную почту того, кто собирал MVP. При передаче это самый болезненный пункт: без доступа к DNS или к базе новая команда не выпустит даже исправление опечатки.
# Handoff
## External services and account owners
| Service | Used for | Variables | Account owner | Others with access |
| --- | --- | --- | --- | --- |
| Git hosting | Code, CI | — | `<company account>` | `<names, roles>` |
| App hosting | Runs the API | `DATABASE_URL`, `ADMIN_TOKEN`, `TELEGRAM_*` | `<company account>` | `<names, roles>` |
| Managed PostgreSQL | Production data, backups | `DATABASE_URL` | `<company account>` | `<names, roles>` |
| Domain and DNS | `<api domain>` | — | `<company account>` | `<names, roles>` |
| Telegram bot | Lead notifications | `TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_ID` | `<company account>` | `<names, roles>` |
| AI assistant accounts and API keys | Development only | — | `<who pays>` | `<names>` |
## Access transfer
- [ ] Every account above is registered to a company email, not a personal one.
- [ ] The receiving team has admin access to each service.
- [ ] Access of people who leave the project is revoked or reduced.
- [ ] Secrets are rotated after the transfer: `ADMIN_TOKEN`, the bot token,
the database password.
- [ ] Two-factor recovery codes are in the company password manager.
- [ ] Personal API keys used by AI assistants during development are revoked.
## Context
- Commands and rules for people and AI assistants: `AGENTS.md`.
- Limitations that are known and accepted: README, section Known limitations.Релиз, откат и известные ограничения
Чек-лист релиза нужен не для бюрократии: он убирает вопросы, которые иначе решаются в спешке. Перед релизом — зелёный CI, прочитанные миграции и резервная копия; после — проверка /health; на случай проблем — заранее понятный откат.
# Release and rollback
## Before the release
- [ ] `npm run check` is green in CI on the release commit.
- [ ] New migrations are reviewed. A release only adds columns and tables;
removing or renaming happens in a later release, when no running version
uses the old structure.
- [ ] A database backup is made and its file name is written in the release notes:
```bash
pg_dump --format=custom --file="leads-$(date +%Y%m%d-%H%M).dump" "$DATABASE_URL"
```
- [ ] The release commit is tagged: `git tag v1.4.0 && git push origin v1.4.0`.
## Release
1. CI/CD deploys the tag and runs `npm run db:migrate` once.
2. `GET /health` returns `{"status":"ok"}`.
3. A test lead from the staging form reaches the database and Telegram.
## Rollback
1. Deploy the previous tag. This is safe when the release only added
columns and tables: the old version ignores them.
2. If a migration must be undone, write a new migration that reverses it
and release it as usual. Applied migrations are never edited.
3. Restore the backup only as a last resort: data written after the backup
is lost.
```bash
pg_restore --clean --if-exists --no-owner --dbname="$DATABASE_URL" leads-YYYYMMDD-HHMM.dump
```Команды резервной копии и восстановления в чек-листе выполнены на учебной базе: сделали дамп, добавили заявку, восстановили — лишняя заявка исчезла. Последний шаг отката поэтому и стоит последним: восстановление возвращает базу к моменту копии вместе со всеми данными, записанными после неё.
$ pg_dump --format=custom --file="leads-$(date +%Y%m%d-%H%M).dump" "$DATABASE_URL"
leads-20261012-1410.dump
leads after an extra insert: 2
$ pg_restore --clean --if-exists --no-owner --dbname="$DATABASE_URL" leads-20261012-1410.dump
exit code: 0
leads after restore: 1Откат кода предыдущим тегом безопасен, только если релиз лишь добавлял колонки и таблицы: старая версия их просто не использует. Поэтому удаление и переименование делают в следующем релизе — это подход «сначала расширить, потом сузить», подробно описанный в материале о деплое. Как ревьюить миграции, которые пишет агент, — в материале об архитектурных границах и миграциях.
Раздел «Известные ограничения» в README — признак зрелости, а не слабости. Он экономит новой команде дни на поиск того, что автор и так знал. Ограничение полезно закрепить тестом: если кто-то его исправит, тест упадёт и напомнит обновить README.
// Documents a known limitation from README: the lead is saved, the response is 500.
it("known limitation: a failed notification returns 500 after saving", async () => {
const { app, notify, saved } = setup();
notify.mockRejectedValueOnce(new Error("telegram unreachable"));
const res = await request(app).post("/api/leads").send({ name: "Aigerim", phone: "+7 701 123 45 67" });
expect(res.status).toBe(500);
expect(saved).toHaveLength(1);
});Репетиция передачи: чистый клон строго по README
Чистая папка и чистое окружение
Клонируйте репозиторий в новую папку, без вашего
.env,node_modulesи сгенерированных файлов. Лучше всего — на машине или в аккаунте человека, который не писал код.Только команды из README
Выполняйте их по порядку и дословно. Любая команда «из головы» — это пропущенный шаг, его нужно дописать.
Проверка результата
npm run checkзелёный,/healthотвечает, тестовая заявка сохраняется.Журнал расхождений
Каждую заминку записывайте и исправляйте в репозитории, затем повторяйте репетицию с начала.
Мы автоматизировали репетицию скриптом: он берёт блок команд из раздела Quick start и выполняет его в новой папке. Итоговый README прошёл от git clone до сохранённой заявки:
$ git clone …/project leads-mvp
$ cd leads-mvp
$ cp .env.example .env
$ docker compose up -d --wait db
$ npm ci
4 high severity vulnerabilities
$ npx prisma generate
✔ Generated Prisma Client (7.10.0) to .\src\generated\prisma in 25ms
$ npm run db:migrate
Applying migration `20261012140759_init`
All migrations have been successfully applied.
$ npm run db:seed
seed: 1 lead(s) in the database
$ npm run check
Test Files 2 passed (2)
Tests 6 passed (6)
$ npm run dev
[dev] leads-mvp listening on http://localhost:3080
$ curl http://localhost:3080/health
{"status":"ok"}
POST /api/leads → 201 {"id":2}
rehearsal finished in 27 sРепетиция находит и ошибки в коде. На порту 3000 нашей машины уже работало другое приложение, и первая версия сервера написала «listening» и тихо завершилась с кодом 0. В Express 5, по руководству по миграции, ошибка запуска передаётся в колбэк app.listen и не выбрасывается сама. Исправленная версия падает с понятной ошибкой:
import { createApp } from "./app.js";
import { createDb, createLeadStore } from "./db.js";
import { loadEnv } from "./env.js";
import { createNotify } from "./notify.js";
const env = loadEnv();
const db = createDb(env.DATABASE_URL);
const app = createApp({ leads: createLeadStore(db), notify: createNotify(env), adminToken: env.ADMIN_TOKEN });
// Express 5 passes a listen error (for example, a busy port) to this callback.
app.listen(env.PORT, (error) => {
if (error) throw error;
console.info(`leads-mvp listening on http://localhost:${env.PORT}`);
});$ PORT=3000 npx tsx --env-file=.env server.before.ts # first version, port 3000 is busy
leads-mvp listening on http://localhost:3000
exit code: 0
$ PORT=3000 npx tsx --env-file=.env src/server.ts # fixed version
Error: listen EADDRINUSE: address already in use :::3000
exit code: 1- Репетиция на одной машине не заменяет проверку на сервере: деплой, домен и доступы проверяются отдельно по чек-листу релиза.
- README устаревает с каждым изменением. Повторяйте репетицию перед каждой передачей и после изменений в установке.
- Пакет передачи не делает код хорошим. Он делает проект проверяемым: новая команда быстрее поймёт, что оставить, а что переделать.
Как проверено9 · пройдено1 · не запускалосьсверено 12 октября 2026 г.
- запущено, работает
учебный проект leads-mvp: src/*.ts, prisma/schema.prisma, prisma/seed.ts, compose.yaml, prisma.config.ts
Node.js 24.18, npm 11.16, Express 5.3, zod 4.6.6, Prisma 7.10.0, PostgreSQL 17 (postgres:17-alpine в Docker Compose, проекты kb-g-handoff-*, после проверки docker compose down -v): migrate dev init, generate, tsc --noEmit (TypeScript 6.0.3), vitest — 2 файла, 6 тестов
- запущено, работает
README.md (итоговый): Quick start from zero
rehearse.mjs: git clone в новую папку и дословное выполнение блока команд README в bash (подстановка только <repository-url> → локальный путь, COMPOSE_PROJECT_NAME=kb-g-handoff-clean): npm ci, prisma generate, db:migrate, db:seed, check (6 тестов), dev → /health {"status":"ok"}, POST /api/leads → 201 (log-07)
- запущено, работает
README первой версии (npm install && npm run dev)
та же репетиция: «node: .env: not found», /health — ECONNREFUSED (log-09); после cp .env.example .env — ERR_MODULE_NOT_FOUND для src/generated/prisma/client.js (log-10)
- запущено, работает
src/env.ts, tests/env-example.test.ts
запуск без .env — список недостающих переменных, exit 1 (log-02); добавление SENTRY_DSN только в src/env.ts — тест падает с диффом (log-12), затем изменение отменено
- запущено, работает
package.json engines + .npmrc engine-strict=true
копия проекта с engines.node ">=26": npm ci на Node 24.18 — EBADENGINE, exit 1 (log-06)
- запущено, работает
npm audit
в чистом клоне 12.10.2026: 4 high (deepmerge-ts через @prisma/config, mysql2 через prisma CLI 7.10.0), исправление npm audit fix --force ставит prisma@6.19.3 (log-08)
- запущено, работает
docs/RELEASE.md: pg_dump / pg_restore
docker run --rm postgres:17-alpine против базы из compose.yaml (host.docker.internal): дамп custom-формата создан, лишняя заявка после дампа исчезла после pg_restore --clean --if-exists --no-owner (log-05)
- запущено, работает
src/server.ts (обработка ошибки app.listen)
порт 3000 занят другим приложением: первая версия вывела «listening» и вышла с кодом 0 (log-11), исправленная — EADDRINUSE, exit 1 (log-03)
- запущено, работает
README.md, AGENTS.md, docs/HANDOFF.md, docs/RELEASE.md
markdownlint-cli2 0.23.3 с .markdownlint.jsonc (MD013 без таблиц и кода): 0 issues (log-04)
- не запускалось
docs/HANDOFF.md (таблица сервисов), шаги релиза на хостинге
шаблон без реальных аккаунтов; деплой на хостинг, DNS и передача доступов не выполнялись — нет реальной инфраструктуры
«Запущено» — код выполнен во временном проекте или контейнере, вывод в статье взят из прогона. «Частично» — проверено с заглушками вместо внешних сервисов. «Не запускалось» — нужны устройство, аккаунт или ключ; шаги сверены с официальной документацией. Файлы проверочного проекта и логи запусков сохранены в архиве InterLAB.