chore: init monorepo with GPL-3.0 license, docs, backend skeleton, frontend wiring

This commit is contained in:
loki5512344 2026-09-06 14:34:57 +02:00
commit 43cf0e277d
Signed by: boba
GPG key ID: 253067914055423B
57 changed files with 5027 additions and 0 deletions

38
docs/README.md Normal file
View file

@ -0,0 +1,38 @@
# docs — Индекс документации Indexium
| Документ | Описание |
|----------|----------|
| [architecture.md](architecture.md) | Архитектура асинхронного событийного индексатора, компоненты, lifecycle, edge cases |
| [database-schema.md](database-schema.md) | Схема PostgreSQL, индексы FTS/pg_trgm, миграции |
| [api-spec.md](api-spec.md) | Public REST API v1, webhooks, auth, ошибки, rate limiting |
| [git-strategy.md](git-strategy.md) | Почему монорепо, workflow веток/коммитов |
| [deployment.md](deployment.md) | Деплой MVP на VPS, docker-compose, бэкапы, CI/CD |
| [adr/001-monorepo.md](adr/001-monorepo.md) | ADR-001: монорепо vs полирепо |
| [adr/002-async-indexer.md](adr/002-async-indexer.md) | ADR-002: асинхронный индексатор поверх GH Releases |
| [analytics.md](analytics.md) | bStats-аналог: SDK, ingestion, daily агрегаты, приватность |
| [adr/003-search-engine.md](adr/003-search-engine.md) | ADR-003: Postgres FTS + pg_trgm вместо Meilisearch |
| [adr/004-auth-strategy.md](adr/004-auth-strategy.md) | ADR-004: GitHub-only + PAT (+ Device Flow Phase 2) |
| [adr/005-analytics.md](adr/005-analytics.md) | ADR-005: bStats аналог — Postgres + daily_salt |
## Как добавлять доки
- Новые ADR: `docs/adr/NNN-kebab-title.md` по шаблону ниже.
- Диаграммы — Mermaid внутри markdown (рендерится в GitHub).
### Шаблон ADR
```markdown
# ADR-NNN: Заголовок
Дата: 2026-09-06
Статус: Принято | Отклонено | Отложено
## Контекст
...
## Решение
...
## Последствия
...
```

26
docs/adr/001-monorepo.md Normal file
View file

@ -0,0 +1,26 @@
# ADR-001: Монорепо vs Полирепо
Дата: 2026-09-06
Статус: Принято
## Контекст
В корне два пакета: `indexium-backend` (Rust/Axum) и `indexium-frontend` (SvelteKit). Нужно решить как организовать git: один репозиторий на всё или два отдельных. Backend уже имел пустой `.git` без коммитов, фронт без гита.
## Рассмотренные варианты
1. **Монорепо** — один `.git` в корне.
2. **Полирепо** — два независимых репозитория.
3. **Submodules** — корневой репо + сабмодули.
## Решение
Выбрать **монорепо**. Удалить `indexium-backend/.git`, инициализировать `Indexium/.git` в корне. См. `docs/git-strategy.md`.
Причины: атомарные изменения API+UI, один CI, проще onboarding, нет нужды в разных релизных каденсах на старте.
## Последствия
- Положительные: один clone, один PR для кросс-пакетных изменений, один issue tracker.
- Отрицательные: при росте команды >10 может потребоваться разрезание (план через `git filter-repo`).
- Submodules отклонены из-за сложности DX.
## Ссылки
- `docs/git-strategy.md`
- `todo.md` Phase 0

View file

@ -0,0 +1,26 @@
# ADR-002: Асинхронный событийный индексатор поверх GitHub Releases
Дата: 2026-09-06
Статус: Принято
## Контекст
Нужен дешёвый и устойчивый к лимитам GitHub способ индексировать моды. Хранить `.jar` у себя дорого, проксировать трафик — упрёмся в bandwidth и rate limits.
## Решение
Бэкенд не хранит артефакты. GitHub Releases CDN — источник правды для файлов. Мы только:
- принимаем webhook `release.published` (HMAC + queue + 202),
- воркер читает zip central directory через Range Request,
- парсит манифест (`fabric.mod.json` и т.д.),
- пишет метаданные в Postgres,
- отдаёт прямые `download_url` на `objects.githubusercontent.com`.
Подробнее в `docs/architecture.md`.
## Последствия
- Плюс: минимальный storage, нет egress costs, +5k–12.5k RPH через GitHub App.
- Минус: зависимость от доступности GitHub CDN (приемлемо — моды и так там).
- Вынесен malware-скан и SHA-256 сверка как обязательные.
## Альтернативы
- Хранить файлы у себя (S3) — отклонено: дорого, дублирование.
- Полный pull `.jar` на каждый релиз — отклонено: трафик, медленно.

View file

@ -0,0 +1,33 @@
# ADR-003: Использование PostgreSQL FTS и pg_trgm вместо Meilisearch/Elasticsearch
Дата: 2026-09-06
Статус: Принято
## Контекст
Для поиска модов по названию, описанию и авторам требуется полнотекстовый поиск и устойчивость к опечаткам (fuzzy search). Введение отдельного движка поиска (Meilisearch, OpenSearch, Elasticsearch) усложняет инфраструктуру и увеличивает потребление RAM (ещё один сервис в `docker-compose`, отдельный индекс, синхронизация).
На MVP ожидается <50k модов, поисковый трафик <100 RPS, VPS за $5–10.
## Решение
Использовать возможности PostgreSQL 16:
- `tsvector` + `GIN`-индексы для ранжированного полнотекстового поиска (`to_tsvector`, `ts_rank`, `plainto_tsquery`).
- Расширение `pg_trgm` для поиска с опечатками (Trigram Similarity, оператор `%`, `similarity()`).
- Материализованный `search_vector` с триггером на `INSERT/UPDATE` (см. `docs/database-schema.md`).
Запрос: `search_vector @@ plainto_tsquery` + фильтр по `mod_versions` (`GIN (game_versions, loaders)`) + `ORDER BY ts_rank DESC` + fallback `similarity()` при 0 результатах.
## Последствия
- **Плюсы:** Нет дополнительных сервисов в `docker-compose`, экономия памяти (~0 доп. RAM vs +500MB–1GB у Meilisearch), атомарные транзакции при обновлении индекса (нет лагосинка), проще бэкапы.
- **Минусы:** При объёме >500k записей или >500 RPS скорость FTS в Postgres начинает уступать специализированным движкам (latency >100ms, нет typo-tolerance из коробки как в Meilisearch).
- **План миграции:** Если p95 latency поиска превысит 100ms (метрика в `tracing` + Grafana), вынести индекс в Meilisearch: добавить воркер-синк `mods` → Meilisearch, переключить `GET /mods` на Meilisearch с fallback на Postgres. Схема БД не меняется.
## Альтернативы (отклонены на MVP)
- **Meilisearch** — отличный typo-tolerance, но +1 сервис, нужен отдельный деплой и синк.
- **Elasticsearch / OpenSearch** — оверхед по RAM/диску, нужен кластер даже для малого объёма.
- **SQLite FTS** — не подходит, уже Postgres как основной.
## Ссылки
- `docs/database-schema.md` — секция 2 (триггер `mods_search_vector_update`), секция 3 (примеры FTS + fuzzy).
- `docs/architecture.md` — секция 4 (Поиск без Elasticsearch).

View file

@ -0,0 +1,37 @@
# ADR-004: Стратегия авторизации и профилей — GitHub-only + PAT (+ Device Flow позже)
Дата: 2026-09-06
Статус: Принято (MVP) / Запроектировано (Phase 2)
## Контекст
Каталог open-source модов где 1 мод = 1 GitHub репо. Нужна минимальная, безопасная авторизация без паролей, но с поддержкой CLI/лаунчеров (Prism) и будущих Discord/коллекций. Пользователь предложил: GitHub App, PAT, Device Flow, Discord синк, дашборды, коллекции, геймификацию, SVG виджет.
## Решение
**MVP:**
- Вход только GitHub OAuth (`read:user`, `user:email`), JWT httpOnly cookie, без паролей. Читатели — anonymous.
- PAT с `SHA256` хранением и скоупами `read:mods|write:mods|webhooks:manage` для CI/лаунчеров.
- `verified` бейдж если репо публичное + лицензия + GitHub App установлен.
- Sponsors (GitHub/Patreon/Ko-fi) + star/follow (in-app) + SVG badges (`/v1/badges/:slug/downloads.svg`).
**Phase 2 (спроектировано, не кодим сейчас):**
- Device Code Flow (RFC 8628) — Indexium как OAuth2 Provider для лаунчеров (`/oauth/device/code` → `/activate`).
- Discord linked_account + бот роли `Verified Modder`.
- Collections/Modlists с экспортом Prism/packwiz, Activity Feed, аналитика по версиям/лоадерам, PGP проверка.
**Backlog:** краш-логи, лидерборды, Profile README.
## Альтернативы
- Google/email логин — отклонён для MVP (публикация всё равно требует GitHub, лишняя сложность).
- Сразу Device Flow — отклонён ( +2 недели, PAT покрывает 80% кейсов).
- Discord как логин — отклонён (только linked).
## Последствия
- Плюс: минимум GDPR, нет паролей, доказуемое владение репо, CLI готов через PAT.
- Минус: без GitHub аккаунта не опубликовать (осознанно, соответствует open-source философии).
- Миграция: таблицы `personal_access_tokens`, `linked_accounts`, `oauth_clients/device_codes` добавятся без breaking change.
## Ссылки
- `docs/auth-profiles.md` §7-10
- `docs/catalog-philosophy.md`
- `docs/api-spec.md` §Auth/Profiles

29
docs/adr/005-analytics.md Normal file
View file

@ -0,0 +1,29 @@
# ADR-005: Собственный bStats-аналог для Indexium Analytics
Дата: 2026-09-06
Статус: Принято (дизайн) / К реализации в Phase 3
## Контекст
Скачивания накручиваются CI, нужна честная метрика популярности — активные установки в рантайме. bStats де-факто стандарт для Minecraft модов: lightweight SDK → POST gzip JSON → агрегация. Пользователь предложил полный дизайн с `server_uuid`, daily_salt, `mod_telemetry_pings` + `mod_daily_stats`, opt-out и сортировкой `active_servers`.
## Решение
- **SDK:** MIT Java/Kotlin модуль `dev.indexium:analytics` ~15KB, `IndexiumMetrics(slug)` + `SimplePie`, уважает `-Dindexium.analytics.disable=true` и `config/indexium.json`.
- **Ingestion:** `POST /api/v1/analytics/submit` (gzip, без IP логов) → валидация allow-list → `server_hash = sha256(uuid + daily_salt)` → Redis 1/15мин → `mod_telemetry_pings` (TTL 30d).
- **Storage:** Postgres `mod_telemetry_pings` + `mod_daily_stats (breakdown_json)` + `analytics_salts`. Кроном `COUNT(DISTINCT server_hash)` раз в час. Хватает до 10M пингов/мес, далее TimescaleDB hypertable без смены схемы.
- **Serving:** `GET /mods/:slug/analytics?range=7d|30d|90d` (кэш 5м), `GET /badges/:slug/servers.svg`, `GET /mods?sort=active_servers`.
- **Приватность:** не храним IP, daily_salt ротация (не трекать сквозь дни), `custom_charts` ≤5 ключей, opt-out на клиенте.
## Альтернативы
- Сторонний bStats.org — отклонён (внешняя зависимость, нет контроля, нет breakdown по нашим лоадерам).
- ClickHouse сразу — отклонён (оверхед для MVP, Postgres хватает).
- Хранить сырые пинги навсегда — отклонён (раздувание, достаточно daily агрегата).
## Последствия
- Плюс: честная сортировка `active_servers`, графики для авторов, бейджи, без сторонних сервисов.
- Минус: +2 таблицы, крон-агрегация, SDK нужно публиковать в Maven Central.
- План: сначала Axum handler + агрегация, потом SDK (или наоборот — можно параллельно).
## Ссылки
- `docs/analytics.md`
- `docs/api-spec.md` §Analytics
- `docs/database-schema.md` §3

119
docs/analytics.md Normal file
View file

@ -0,0 +1,119 @@
# Indexium Analytics — собственный аналог bStats
> Даём мододелам встроенную аналитику рантайма (активные серверы/клиенты, MC/Java/OS) без сторонних сервисов. Indexium получает честную метрику популярности — не по скачиваниям (накручиваются CI), а по реальным установкам.
Основано на твоей схеме + правки под KISS/SOLID/приватность.
---
## 1. Как работает bStats (база)
1. **SDK в моде** — фоновый таймер каждые 30–60 мин собирает `mc_version, loader, java_version, os, player_count, server_uuid, custom_charts` → `POST` gzip JSON асинхронно, не блокируя главный поток.
2. **Ingestion** — бэкенд валидирует, rate-limit по `server_hash`, анонимизирует `server_uuid`.
3. **Aggregation** — сырые пинги → часовые/суточные агрегаты (Time Series), сырые удаляются по TTL.
---
## 2. Indexium реализация
### 2.1 Клиент — Lightweight Java/Kotlin модуль
```java
// Fabric/NeoForge initialize()
IndexiumMetrics metrics = new IndexiumMetrics("sodium-extra", 12345); // slug + projectId (опц)
metrics.addCustomChart(new SimplePie("config_type", () -> config.getType()));
// respects: -Dindexium.analytics.disable=true, config/indexium.json { enabled: false }
```
**Что собираем (allow-list, ничего лишнего):**
- `mc_version` (1.20.1), `loader` (fabric/neoforge/forge/quilt), `loader_version`
- `java_version` (21.0.2), `os` (linux/windows/macos — без детальной версии), `arch` (x64/arm64)
- `player_count` (0 на клиенте, N на сервере), `server_uuid` (генерим раз, храним в `config/indexium-uuid.txt`)
- `mod_version` (из `fabric.mod.json`), `custom_charts` (String→String, до 5 ключей, до 32 символов)
**Что НЕ собираем:** IP (не логируем), ник игрока, путь к файлам, список других модов (опционально по согласию, off по умолчанию).
SDK: ~15KB, без зависимостей, `CompletableFuture` + `HttpURLConnection`, gzip. Лицензия MIT, публикуем в Maven Central как `dev.indexium:analytics:1.0.0`.
### 2.2 API
- `POST /api/v1/analytics/submit` — пинг от мода (gzip JSON, `Content-Encoding: gzip` опционально)
- `GET /api/v1/mods/:slug/analytics?range=7d|30d|90d` — графики для SvelteKit
- `GET /api/v1/badges/:slug/servers.svg` — бейдж активных серверов
Пример payload (как в твоём ТЗ):
```json
{
"mod_slug": "sodium-extra",
"server_uuid": "e8d9a0f1-4b2c-...",
"metrics": {
"mc_version": "1.20.1",
"loader": "fabric",
"java_version": "21.0.2",
"os": "Linux",
"player_count": 12,
"custom_charts": { "gui_theme": "dark" }
}
}
```
### 2.3 Хранение — PostgreSQL (MVP) → TimescaleDB/ClickHouse при росте
На MVP хватает Postgres + daily агрегат (как ты предложил). Сырые пинги храним 30 дней, агрегаты — навсегда.
```sql
-- Полуагрегат: один пинг = одна строка, TTL 30 дней через cron
CREATE TABLE mod_telemetry_pings (
id BIGSERIAL PRIMARY KEY,
mod_id UUID NOT NULL REFERENCES mods(id) ON DELETE CASCADE,
server_hash CHAR(64) NOT NULL, -- sha256(server_uuid + daily_salt)
mc_version VARCHAR(16) NOT NULL,
loader VARCHAR(16) NOT NULL,
os VARCHAR(16) NOT NULL,
java_version VARCHAR(16) NOT NULL,
player_count INT NOT NULL DEFAULT 0,
pinged_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_telemetry_lookup ON mod_telemetry_pings (mod_id, pinged_at DESC);
CREATE INDEX idx_telemetry_hash ON mod_telemetry_pings (server_hash, pinged_at);
-- Суточный агрегат (хранится навсегда)
CREATE TABLE mod_daily_stats (
mod_id UUID NOT NULL REFERENCES mods(id) ON DELETE CASCADE,
date DATE NOT NULL,
active_servers INT NOT NULL DEFAULT 0, -- COUNT(DISTINCT server_hash)
active_players INT NOT NULL DEFAULT 0, -- SUM(player_count) по последним пингам сервера за день
breakdown_json JSONB NOT NULL, -- { mc_versions:{}, loaders:{}, os:{}, java:{}, custom:{gui_theme:{dark: 120}} }
PRIMARY KEY (mod_id, date)
);
```
**Агрегация:** воркер-кроном раз в час: `INSERT INTO mod_daily_stats ... ON CONFLICT DO UPDATE` группировкой по `server_hash` (последний пинг сервера за день). Через `pg_cron` или tokio `interval` в бэкенде.
**Масштаб:** при >10M пингов/мес — мигрируем на TimescaleDB hypertable (`create_hypertable('mod_telemetry_pings','pinged_at')`) или ClickHouse. Схема не меняется.
### 2.4 Защита и анонимность (критично)
- **Хеш + daily_salt:** `server_hash = sha256(server_uuid + salt_for_today)`. Соль ротируется в `analytics_salts(date, salt)`, храним 2 дня. Нельзя трекать сервер сквозь дни, но можно считать уникальные за день.
- **Не храним IP:** `tower_http::TraceLayer` без IP, `X-Forwarded-For` игнорируем, в логах — `/analytics/submit 200` без IP.
- **Opt-Out:** SDK проверяет в порядке: JVM флаг `-Dindexium.analytics.disable=true` → `global_privacy.json` (`.minecraft/config/indexium.json { enabled:false }`) → `config/<modid>/indexium.json`. Если любой `false` — не шлём.
- **Rate limit:** Redis `SET server_hash:mod_slug NX EX 900` — 1 пинг / 15 мин. Ответ `429` с `Retry-After`, SDK бэкофф 30 мин.
- **Валидация:** `mod_slug` должен существовать, `mc_version`/`loader` из allow-list, `custom_charts` ≤5 ключей, `player_count` 0–10000. Иначе `400`.
---
## 3. Фичи для профиля и карточки мода
1. **Live Charts (SvelteKit + LayerChart/Chart.js):** `GET /mods/:slug/analytics?range=30d` → `{ daily: [{date, active_servers, active_players}], breakdown: {mc_versions, loaders, os} }`. Графики: активные серверы (линия), разбивка по MC (пончик), лоадерам (бар).
2. **Badge:** `https://api.indexium.example.com/v1/badges/sodium-extra/servers.svg` → `Active Servers: 1.2k` (из `mod_daily_stats` за вчера, кэш 1h).
3. **Сортировка "Real-world Usage":** `GET /mods?sort=active_servers` — `ORDER BY (SELECT active_servers FROM mod_daily_stats WHERE date = CURRENT_DATE -1)`, а не по скачиваниям. Фильтр против накрутки CI.
---
## 4. Что не делаем (чтобы не стать spyware)
- Не собираем ник, чат, координаты, список всех модов без явного согласия (если включим — отдельный `custom_charts` с opt-in).
- Не fingerprint'им по железу.
- SDK открыт (MIT) — любой может проверить что шлём (как bStats — код на GitHub).
См. `adr/005-analytics.md`, `api-spec.md` §Analytics, `database-schema.md` §telemetry.

318
docs/api-spec.md Normal file
View file

@ -0,0 +1,318 @@
# Public REST API — Indexium v1
Base URL: `https://api.indexium.example.com/api/v1` (локально `http://localhost:3000/api/v1`)
Все ответы — `application/json`. Пагинация — `page`/`limit` (MVP) → cursor позже. Кэш — `Cache-Control: public, max-age=60`, `ETag`.
---
## Health
### `GET /health`
Проверка живости + БД + Redis.
**200**
```json
{ "status": "ok", "db": "up", "redis": "up", "version": "0.1.0" }
```
---
## Webhooks (internal)
### `POST /webhooks/github`
Принимает GitHub Webhook `release`.
Headers:
- `X-GitHub-Delivery: uuid`
- `X-Hub-Signature-256: sha256=...`
- `X-GitHub-Event: release`
Body: raw JSON от GitHub.
**202** — принято в очередь
```json
{ "status": "accepted", "delivery_id": "..." }
```
**401** — неверная подпись
**409** — уже обработано (идемпотентность)
Логика: HMAC проверка → дедуп по `delivery_id` → push в Redis Streams → 202.
---
## Mods
### `GET /mods`
Query params:
| param | type | описание |
|-------|------|----------|
| `query` | string | FTS по name/summary/README |
| `gameVersion` | string | фильтр `1.20.1` |
| `loader` | string | `fabric` \| `quilt` \| `neoforge` \| `forge` |
| `page` | int | default 1 |
| `limit` | int | default 20, max 50 |
| `sort` | string | `relevance` \| `newest` \| `popular` \| `active_servers` |
**200**
```json
{
"data": [
{
"slug": "sodium-extra",
"name": "Sodium Extra",
"summary": "Extra optimizations",
"author": "flashy",
"icon_url": "https://...",
"game_versions": ["1.20.1"],
"loaders": ["fabric"],
"latest_version": "1.2.3",
"download_url": "https://github.com/.../releases/download/...",
"updated_at": "2026-09-01T12:00:00Z"
}
],
"pagination": { "page": 1, "limit": 20, "total": 142, "pages": 8 }
}
```
Кэшируется в Redis по ключу `mods:query=...:gv=...:loader=...:page=...` TTL 60s.
### `GET /mods/:slug`
**200**
```json
{
"slug": "sodium-extra",
"name": "Sodium Extra",
"summary": "...",
"description": "... (markdown)",
"github_repo": "owner/repo",
"author": { "login": "flashy", "avatar_url": "https://..." },
"icon_url": "https://...",
"verified": true,
"versions": [
{
"version_number": "1.2.3",
"game_versions": ["1.20.1"],
"loaders": ["fabric"],
"download_url": "https://github.com/.../releases/download/v1.2.3/sodium-extra-1.2.3.jar",
"file_sha256": "abc...",
"file_size": 123456,
"published_at": "2026-09-01T12:00:00Z"
}
]
}
```
**404** — `{"error":"mod_not_found"}`
### `GET /mods/:slug/icon`
Отдаёт иконку мода. Воркер при индексации извлекает `assets/<modid>/icon.png` (или `icon` из `fabric.mod.json` → путь внутри jar) → сохраняет в кэш/проксирует.
- **200** — `image/png` / `image/webp` с `Cache-Control: public, max-age=86400`, `ETag`. Если иконки нет → `302` на `raw.githubusercontent.com` fallback или дефолтная заглушка.
- **404** — мод не найден.
> Альтернатива на MVP: не хранить иконку у себя, а отдавать `icon_url` как прямую ссылку `https://raw.githubusercontent.com/<owner>/<repo>/<branch>/src/main/resources/assets/...`. Эндпоинт `/icon` тогда — 302 редирект + кэш заголовков.
### `POST /mods/resolve` — пакетный резолв для лаунчеров
Принимает список модов + окружение, возвращает дерево прямых скачиваний и зависимостей (для Prism / Modrinth-compatible клиентов).
**Request**
```json
{
"game_version": "1.20.1",
"loader": "fabric",
"mods": [
{ "slug": "sodium-extra", "version": "1.2.3" },
{ "slug": "lithium", "version": "latest" }
]
}
```
**200**
```json
{
"resolved": [
{
"slug": "sodium-extra",
"version_number": "1.2.3",
"download_url": "https://github.com/.../releases/download/.../sodium-extra-1.2.3.jar",
"file_sha256": "abc...",
"file_size": 123456,
"dependencies": [{ "slug": "sodium", "version_range": ">=0.5.0", "resolved_version": "0.5.8" }]
},
{
"slug": "sodium",
"version_number": "0.5.8",
"download_url": "https://github.com/.../sodium-0.5.8.jar",
"file_sha256": "def...",
"file_size": 654321,
"dependencies": []
}
],
"unresolved": []
}
```
- `version: "latest"` → резолвит последнюю совместимую с `game_version` + `loader`.
- Транзитивные зависимости резолвятся рекурсивно (BFS, max depth 20, защита от циклов).
- **422** — несовместимая комбинация `game_version`/`loader`.
- Кэшируется по ключу `resolve:gv:loader:hash(mods)` TTL 60s.
### `GET /mods/:slug/versions/:version`
Детали конкретной версии. Аналогично элементу массива выше + зависимости:
```json
{
"mod_slug": "sodium-extra",
"version_number": "1.2.3",
"game_versions": ["1.20.1"],
"loaders": ["fabric"],
"dependencies": [{ "mod_id": "sodium", "version_range": ">=0.5.0" }],
"download_url": "https://github.com/...",
"file_sha256": "...",
"file_size": 123456,
"published_at": "..."
}
```
### `POST /mods/import` (auth required)
Импорт репозитория по GitHub OAuth.
Headers: `Authorization: Bearer <github_token>`
Body:
```json
{ "repo": "owner/repo" }
```
Логика: проверить что токен имеет доступ к репо → fetch `fabric.mod.json` из default branch → создать запись `mods` → повесить webhook.
**201** — создан
**409** — уже импортирован
**422** — манифест не найден
---
## Auth
### `GET /auth/github` → 302 redirect на GitHub OAuth
### `GET /auth/github/callback?code=...` → обмен code→token, установка httpOnly cookie / JWT
### `POST /auth/tokens` (auth) — PAT creation
Body: `{ "name": "ci-token", "scopes": ["read:mods","write:mods"], "expires_in_days": 30 }` → `201 { token: "idx_...", id, expires_at }` (токен показывается 1 раз, храним hash). `Authorization: Bearer idx_...` для API.
### `GET /auth/tokens` / `DELETE /auth/tokens/:id` — список/отзыв.
### Device Flow (Phase 2, RFC 8628)
- `POST /oauth/device/code` → `{ device_code, user_code: "ABCD-1234", verification_uri: "https://indexium.example.com/activate", expires_in: 600 }`
- `GET /activate` (frontend) — ввод `user_code` → consent → `POST /oauth/device/verify { user_code }`
- `POST /oauth/token` grant_type=`urn:ietf:params:oauth:grant-type:device_code` → `{ access_token, refresh_token }`
- Лаунчер поллит `/oauth/token` до получения токена.
### `GET /auth/discord` → линк Discord (linked_account, не логин). `GET /auth/discord/callback` → запись в `linked_accounts`.
## Profiles & Social
### `GET /u/:login` / `GET /org/:login` — публичный профиль (кэш 60s, ISR)
### `POST /mods/:slug/star` / `DELETE /mods/:slug/star` — звезда (auth)
### `POST /u/:login/follow` / `DELETE /u/:login/follow` — подписка на автора с опционально `?game_version=1.20.1&loader=fabric`
### `GET /collections` / `POST /collections` (auth, body: `{ title, description, mods: [{slug, version}] }`)
### `GET /collections/:slug` / `GET /collections/:slug/export?format=prism|packwiz`
### `GET /v1/badges/:slug/downloads.svg` / `GET /v1/badges/:slug/version.svg` — SVG виджет для README (public, кэш 1h)
## Analytics (bStats аналог, см. docs/analytics.md)
### `POST /api/v1/analytics/submit` — пинг от мода (gzip опционально)
Headers: `Content-Type: application/json`, `Content-Encoding: gzip` (optional)
Body:
```json
{
"mod_slug": "sodium-extra",
"server_uuid": "e8d9a0f1-4b2c-...",
"metrics": {
"mc_version": "1.20.1",
"loader": "fabric",
"java_version": "21.0.2",
"os": "Linux",
"player_count": 12,
"custom_charts": { "gui_theme": "dark" }
}
}
```
- Валидация: `mod_slug` exists, allow-list версий/лоадеров, `custom_charts` ≤5 ключей.
- Анонимизация: `server_hash = sha256(server_uuid + daily_salt)` — IP не храним.
- Rate limit: 1 пинг / 15 мин per `server_hash+mod_slug` (Redis `SET NX EX 900`) → `429`.
- Opt-Out: respect `-Dindexium.analytics.disable=true` на клиенте.
- **200** `{ "status": "ok" }` **400** validation **429** rate_limited
### `GET /api/v1/mods/:slug/analytics?range=7d|30d|90d`
Public, кэш `public, max-age=300`.
```json
{
"mod_slug": "sodium-extra",
"range": "30d",
"daily": [
{ "date": "2026-09-01", "active_servers": 450, "active_players": 3200 },
{ "date": "2026-09-02", "active_servers": 470, "active_players": 3400 }
],
"breakdown": {
"mc_versions": { "1.20.1": 400, "1.21": 50 },
"loaders": { "fabric": 420, "neoforge": 30 },
"os": { "Linux": 200, "Windows": 250 },
"java": { "21": 300, "17": 150 },
"custom": { "gui_theme": { "dark": 120, "light": 30 } }
}
}
```
**404** mod_not_found. Источник: `mod_daily_stats`.
### `GET /api/v1/badges/:slug/servers.svg` — бейдж активных серверов (как downloads.svg, кэш 1h)
SVG `Active Servers: 1.2k` из `mod_daily_stats` за вчера.
## Sponsors & Badges
- `GET /u/:login` отдаёт `sponsors: { github, patreon, kofi, bmc }` и `badges: ["early_adopter","verified"]`
- Бейджи выдаются воркером (`badges` таблица), SVG — динамически.
---
## Ошибки
Единый формат:
```json
{ "error": "validation_error", "message": "gameVersion must be semver", "details": {...} }
```
Коды:
- `400` validation_error
- `401` unauthorized
- `404` not_found
- `429` rate_limited (headers `Retry-After`)
- `500` internal_error
---
## Rate limiting
- Public API: 60 req/min per IP (Redis).
- Webhook: без лимита, но HMAC обязателен.
---
## Версионирование
- URL версионирование `/api/v1`.
- Breaking changes → `/api/v2` + 6 мес поддержка v1.
---
## OpenAPI
Спека будет жить в `indexium-backend/openapi.yaml` (генерировать из Axum через `utoipa` когда созреет). На MVP — этот markdown как источник правды.

219
docs/architecture.md Normal file
View file

@ -0,0 +1,219 @@
# Архитектура Indexium — Асинхронный событийный индексатор
> Цель: сделать сервис максимально лёгким, дешёвым в обслуживании и устойчивым к ограничениям GitHub.
> Принцип: бэкенд **не хранит** тяжёлые файлы — артефакты отдаются с GitHub Releases CDN, мы валидируем, индексируем метаданные и выдаём быстрые JSON-ответы.
---
## 1. Обзор системных компонентов
```
┌───────────────────────────────────────────────┐
│ GitHub Ecosystem │
└───────┬───────────────────────────────▲───────┘
│ │
│ 1. Webhook (release.published)│ 4. Read Assets / Metadata
▼ │
┌─────────────────────────────────────────────────────────┴────────────────────────────────┐
│ Ваш Бэкенд (Event-Driven Indexer) │
│ │
│ ┌───────────────────────┐ ┌──────────────────────┐ ┌─────────────────────┐ │
│ │ Webhook Ingestion API │ ────> │ Async Job Queue │ ────> │ Worker / Indexer │ │
│ │ (Fast Signature Check)│ │ (Redis / NATS) │ │ (Jar Parser & AST) │ │
│ └───────────────────────┘ └──────────────────────┘ └──────────┬──────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────┐ ┌──────────────────────┐ ┌─────────────────────┐ │
│ │ Edge Public REST/v1 │ <──── │ Read-Heavy Cache │ <──── │ Relational DB │ │
│ │ (High Throughput API) │ │ (Redis Key-Value) │ │ (PostgreSQL + FTS) │ │
│ └───────────────────────┘ └──────────────────────┘ └─────────────────────┘ │
└─────────────────────────────────────────────▲────────────────────────────────────────────┘
│
│ 5. Query Mods / Index
│
┌───────────┴───────────┐
│ Web UI / Launchers │
└───────────────────────┘
```
### Потоки данных
1. **Ingestion** — GitHub шлёт webhook → Ingestion API валидирует HMAC → кладёт job в очередь → отвечает `202`.
2. **Indexing** — Worker читает job → делает `Range Request` к `.jar` → парсит манифест → пишет в Postgres → инвалидирует кэш.
3. **Serving** — Лаунчер / Web UI дергает `GET /api/v1/mods` → читаем Redis → miss → Postgres FTS → кэшируем 60s → отдаём JSON.
---
## 2. Выбор технологического стека
| Слой | Технология | Почему именно это? |
| --- | --- | --- |
| **Backend Core** | **Rust (Axum)** | Минимальный memory footprint, высокий throughput, быстрый async I/O при парсинге. Альтернатива Go (Fiber) — допустима на Phase 2. |
| **Main Database** | **PostgreSQL** | Нативный FTS, `JSONB` для зависимостей, `pg_trgm` для fuzzy, `pgvector` опционально для семантики. |
| **Cache & Queue** | **Redis / Valkey** | Одновременно брокер очередей (Streams/PubSub) и L2-кэш популярных эндпоинтов. |
| **Worker Engine** | **Rust Background Worker (tokio)** | Потребляет webhook-события, скачивает только zip-header stream, валидирует байткод. |
| **Auth & Security** | **GitHub App / OAuth 2.0** | Вход только через GitHub, без локальных паролей. |
| **Frontend** | **SvelteKit + TypeScript** | SSR, маленький бандл, быстрый dev. |
> Обоснование против Elasticsearch/Meilisearch на старте: `to_tsvector` + `pg_trgm` переваривают десятки тысяч модов с <5ms без отдельного кластера. Миграция на внешний поиск — только если FTS упрётся.
---
## 3. Жизненный цикл релиза (Release Lifecycle)
### 3.1 Регистрация мода (Onboarding)
1. Разработчик логинится через GitHub OAuth.
2. Жмёт «Импортировать репозиторий» → сервис проверяет наличие манифеста (`fabric.mod.json`, `neoforge.mods.toml`, `quilt.mod.json`) в default branch.
3. Устанавливается GitHub App + Webhook на события `release` и `push`.
### 3.2 Обработка webhook (Ingestion & Job Dispatch)
1. GitHub шлёт `release.published`.
2. **Ingestion API** валидирует `X-Hub-Signature-256` (HMAC SHA-256 с `WEBHOOK_SECRET`), проверяет идемпотентность по `X-GitHub-Delivery`.
3. Кладёт задачу в Redis Queue, отвечает `202 Accepted` за <50ms (чтобы не висеть по таймауту GitHub — 10s).
### 3.3 Работа воркера-индексатора (Worker Execution) — детальный алгоритм `jar_parser.rs`
> Цель: не скачивать весь `.jar` (может быть 20–50MB), а прочитать только нужный манифест через 2–3 Range-запроса.
```
download_url = "https://github.com/owner/repo/releases/download/v1.2.3/mod-1.2.3.jar"
│
Step 1: HEAD ─────┤
▼
Content-Length: 12345678
Accept-Ranges: bytes
(если нет Content-Length → GET Range: bytes=0-0 + парс Content-Range)
│
Step 2: GET tail ─┤ Range: bytes=-65536 (последние 64KB)
▼
Найти EOCD (End of Central Directory) = 0x06054b50
Из EOCD: central_dir_offset, central_dir_size, num_entries
│
Step 3: GET central dir ─┤ Range: bytes=central_dir_offset..central_dir_offset+size
▼
Парс Central Directory headers (0x02014b50)
Найти entry: fabric.mod.json | quilt.mod.json | neoforge.mods.toml | mcmod.info
+ icon (assets/<modid>/icon.png если указан в манифесте)
→ получить local_header_offset, compressed_size
│
Step 4: GET manifest ─┤ Range: bytes=local_header_offset.. + compressed_size + header
▼
Распаковать (DEFLATE/STORE), парс JSON/TOML, валидация
+ SHA-256 сверка, file_size = Content-Length
```
**Детали реализации:**
1. `HEAD` — обязателен, чтобы получить `Content-Length` и убедиться `Accept-Ranges: bytes`. Таймаут 5s, retry 2.
2. Последние 64KB достаточно для EOCD даже для jar с 10k файлов (EOCD в конце). Если не найден — fallback к последним 128KB.
3. Central Directory читается одним запросом (обычно 5–30KB). Парсим `central_dir_offset/size` из EOCD.
4. Манифест — 4-й запрос только если нужен (часто 1–3KB). Иконка — опционально 5-й запрос, кэшируется и отдаётся через `GET /mods/:slug/icon`.
5. Все `GET` — `reqwest` с `header("Range", ...)`, проверка `206 Partial Content`, иначе fallback к полному скачиванию с лимитом 10MB.
**Ошибки:** `412` если `Accept-Ranges` != bytes → full download; `404` на Range → retry без Range; повреждённый ZIP → помечаем `suspicious` и DLQ.
Код: `indexium-backend/src/worker/jar_parser.rs` — чистые функции `find_eocd()`, `parse_central_dir()`, `fetch_manifest()` без I/O в тестах.
### 3.4 Агрегация и индексация (Storage & Cache Invalidation)
1. Сохраняет версию в `mod_versions` (см. `database-schema.md`).
2. `download_url` = прямая ссылка `https://github.com/.../releases/download/...` (CDN `objects.githubusercontent.com`).
3. Инвалидирует / обновляет Redis-кэш для поиска и карточки мода.
---
## 4. Обход ключевых ограничений (Edge Cases)
### GitHub Rate Limits
- Запросы воркеров — от имени **GitHub App Installs** (5k–12.5k RPH на инсталл vs 60 RPH анонимных).
- Скачивание не проксируем — выдаём клиентам прямые CDN-ссылки, трафик не идёт через нас.
### Безопасность (Malware Protection)
- Сверка `SHA-256` ассета с `.sha256` если есть.
- Сканирование байткода: флаг на `Runtime.getRuntime().exec()`, `ProcessBuilder`, `URLClassLoader`, сетевые вызовы в `<clinit>` / `FabricModInitializer`.
- Карантин: помечаем версию `suspicious = true`, не показываем в публичном поиске до ручной проверки.
### Поиск без Elasticsearch
- Postgres `to_tsvector('russian'|'english', name || summary)` + `GIN`.
- `pg_trgm` (`similarity()`, `%` оператор) для опечаток.
- Материализованный `search_vector` + триггер на update.
### Надёжность очереди
- Retry с exponential backoff (3 попытки), DLQ (dead-letter) для ручного разбора.
- Идемпотентный воркер: `ON CONFLICT (mod_id, version_number) DO UPDATE`.
### Телеметрия (bStats аналог, см. docs/analytics.md)
- **Ingestion:** `POST /api/v1/analytics/submit` (gzip JSON, без IP логов) → валидация allow-list → `server_hash = sha256(uuid + daily_salt)` → Redis rate limit 1/15мин → `INSERT mod_telemetry_pings`.
- **Aggregation:** кроном раз в час `COUNT(DISTINCT server_hash)` + `breakdown_json` → `mod_daily_stats`, TTL 30 дней для сырых пингов (`DELETE WHERE pinged_at < NOW()-30d`).
- **Serving:** `GET /mods/:slug/analytics?range=30d` (кэш 5 мин) + `GET /badges/:slug/servers.svg` + `sort=active_servers` для честной сортировки по реальным установкам, а не накрученным скачиваниям.
- **Масштаб:** Postgres хватает до 10M пингов/мес, далее TimescaleDB hypertable без смены схемы.
---
## 5. Схема структуры БД (Core Entity Relation)
См. детально в [`database-schema.md`](database-schema.md). Коротко:
```sql
-- Таблица модов
CREATE TABLE mods (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
github_repo_id BIGINT UNIQUE NOT NULL,
slug VARCHAR(64) UNIQUE NOT NULL,
name VARCHAR(128) NOT NULL,
summary TEXT,
author_github_id BIGINT NOT NULL,
default_branch VARCHAR(32) DEFAULT 'main',
created_at TIMESTAMPTZ DEFAULT now()
);
-- Таблица версий (релизов)
CREATE TABLE mod_versions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
mod_id UUID REFERENCES mods(id) ON DELETE CASCADE,
version_number VARCHAR(32) NOT NULL,
game_versions VARCHAR(32)[] NOT NULL, -- e.g. ['1.20.1', '1.20.2']
loaders VARCHAR(16)[] NOT NULL, -- e.g. ['fabric', 'quilt']
download_url TEXT NOT NULL, -- GitHub Release Direct Asset URL
file_sha256 CHAR(64) NOT NULL,
published_at TIMESTAMPTZ NOT NULL,
UNIQUE(mod_id, version_number)
);
CREATE INDEX idx_versions_lookup ON mod_versions USING GIN (game_versions, loaders);
```
Дополнительно: `authors`, `webhook_deliveries` (идемпотентность), `search_vector`, `mod_telemetry_pings`/`mod_daily_stats` (аналитика).
---
## 6. Масштабирование и эволюция
| Этап | Нагрузка | Действие |
|------|----------|----------|
| MVP | <10k модов, <100 RPS | Один инстанс Axum + Postgres + Redis, воркер в том же бинаре (tokio spawn) |
| Growth | 10k-100k модов, 1k RPS | Вынос воркера в отдельный деплой, реплика Postgres RO, Redis Cluster |
| Scale | 100k+ модов, 10k RPS | NATS JetStream вместо Redis Streams, read-replica + шардирование по `slug`, CDN перед API (Cloudflare) |
---
## 7. Нефункциональные требования
- **Latency**: `GET /mods` p95 < 80ms (cache hit), < 200ms (cache miss + FTS).
- **Availability**: 99.9% (допустим 43 мин downtime/мес на MVP).
- **Cost**: < $20/мес на MVP (1 VPS Hetzner + managed Postgres free tier).
- **Security**: HMAC, GitHub App, no local passwords, malware scan.
---
## 8. Диаграмма деплоя (MVP)
```
[GitHub] --webhook--> [Axum Ingestion :3000] --> [Redis :6379] --> [Worker]
| |
v v
[Postgres :5432] <--+
|
[Browser/Launcher] --> [Axum Public API :3000] --> [Redis Cache]
\
--> [SvelteKit :5173] (SSR, fetch API)
```
Все сервисы — `docker compose` локально, один VPS в проде.

232
docs/auth-profiles.md Normal file
View file

@ -0,0 +1,232 @@
# Профили, аккаунты и авторизация — дизайн Indexium
> Цель: максимально лёгкая, но крутая система профилей без паролей, где GitHub — источник правды.
---
## 1. TL;DR — рекомендуем для MVP
**Авторизация: только GitHub OAuth / GitHub App.** Никаких паролей, email+пароль, Google и т.д. на старте.
**Почему именно GitHub-only:**
| Плюс | Минус |
|---|---|
| 1 клик, нет форм регистрации | Отсекаем тех у кого нет GitHub (но они и моды не публикуют) |
| Доказуемое владение репозиторием (`GET /repos` с токеном) | Зависимость от GitHub OAuth (но у нас и так всё на GitHub) |
| Аватар, ник, био подтягиваются автоматически | Нет anon-публикаций (и это хорошо для open source) |
| Один токен — и публикация, и вебхуки, и профиль | Если GitHub лежит — логин не работает (редкость) |
| Нет хранения паролей, нет утечек | |
> **Вывод:** для каталога где `1 мод = 1 GitHub репо` — GitHub-only это не ограничение, а фича. Пользователи-читатели (игроки) могут смотреть каталог **без логина вообще**. Логин нужен только авторам.
---
## 2. Роли и модель аккаунта
### Роли
- **Reader (anonymous)** — ищет, качает по прямым ссылкам, смотрит профили. Без аккаунта.
- **Author** — залогинен через GitHub, импортировал хотя бы один репо. Может публиковать релизы (через `git push` + webhook, без кнопки "загрузить jar").
- **Contributor** — указан в `mod_authors` с `role=contributor`, не обязательно owner репо. Получает бейдж на карточке мода.
- **Moderator / Admin** — ручная выдача, может ставить `verified` / `suspicious`, банить.
### Что храним (минимум GDPR)
```sql
-- уже есть authors, расширяем (см. database-schema.md)
CREATE TABLE authors (
github_id BIGINT PRIMARY KEY,
login VARCHAR(39) NOT NULL UNIQUE, -- github login
display_name VARCHAR(128), -- from GitHub name
avatar_url TEXT,
bio TEXT, -- from GitHub bio (кэш, обновляем раз в день)
company VARCHAR(128),
location VARCHAR(128),
website TEXT, -- blog
created_at TIMESTAMPTZ DEFAULT now(),
last_synced_at TIMESTAMPTZ
);
CREATE TABLE mod_authors (
mod_id UUID REFERENCES mods(id) ON DELETE CASCADE,
github_id BIGINT REFERENCES authors(github_id) ON DELETE CASCADE,
role VARCHAR(16) NOT NULL DEFAULT 'owner', -- owner | maintainer | contributor
PRIMARY KEY (mod_id, github_id)
);
```
Никаких email в открытом виде (берём только для JWT, не показываем), никаких паролей. `github_id` — неизменяемый PK, `login` может смениться — обновляем по webhook `user.renamed` или при следующем логине.
### Сессии
- **JWT (httpOnly cookie)**: `sub: github_id`, `login`, `exp: 7d`. Подпись `HS256` с `JWT_SECRET` или `RS256` если хотим ротацию.
- **Не храним сессии в Redis на MVP** — stateless JWT достаточно. Позже — refresh token в `author_sessions`.
- **CSRF**: `SameSite=Lax` + `Origin` check для `POST /mods/import`.
---
## 3. Флоу авторизации (GitHub OAuth)
```
[User] → GET /auth/github → 302 https://github.com/login/oauth/authorize?client_id=...&scope=read:user,repo
→ GitHub login → 302 /auth/github/callback?code=...
→ Backend: POST https://github.com/login/oauth/access_token (code → access_token)
→ GET https://api.github.com/user (с токеном) → { id, login, avatar_url, name, bio }
→ UPSERT authors
→ Set-Cookie: indexium_token=<JWT>; HttpOnly; Secure; SameSite=Lax; Max-Age=604800
→ 302 /me или /?welcomed=1
```
**Для публикации модов нужен `repo` scope** только если хотим ставить webhook автоматически. На MVP можно `read:user` + `public_repo` (только публичные). Токен GitHub не храним долго — меняем на JWT и забываем (или храним encrypted `github_access_token` для будущих API вызовов, с возможностью revoke).
**GitHub App (альтернатива OAuth):**
- Плюс: `5k–12.5k RPH`, управление webhooks через App, `installation_id` per org.
- Минус: сложнее флоу установки.
- Рекомендация: старт с **OAuth** (проще), миграция на **GitHub App** когда упрёмся в rate limits или захотим `checks` API.
---
## 4. Профили — как сделать круто и по open source
### URL структура
- `/u/:login` — профиль пользователя (зеркало GitHub, но с модами)
- `/org/:login` — профиль организации (если `type: Organization`)
- `/mod/:slug` — карточка мода (показывает авторов с ролями)
Все профили **публичны и кэшируются** (ISR в SvelteKit).
### Что показываем на `/u/:login`
```
[avatar] flashy (@flashy) — "Minecraft modder"
bio | 📍 Berlin | 🔗 flashy.dev | Joined 2024
Stats: 12 mods · 48 releases · 12k downloads (aggregated) · 342 stars (from GH)
Mods:
[sodium-extra] 1.20.1 fabric — ★ 42 — MIT
[lithium-fork] ...
Contributions: контрибьютил в 5 чужих модов (через mod_authors)
Activity: последние релизы таймлайн (из webhook_deliveries)
Links: GitHub → github.com/flashy | Indexium RSS → /u/flashy/feed.xml
```
Фишки:
- **Верификация:** бейдж `✓ Verified` если `mods` >0 и все репо публичные + лицензия. `✦ Staff` для модераторов.
- **Граф вклада:** как GitHub contributions, но по релизам модов.
- **Open Source score:** % модов с OSI лицензией, наличие `CONTRIBUTING.md`, `issues` открыты.
- **Не показываем email**, только то что уже публично на GitHub.
### Крутые идеи (backlog, но заложим)
- **Profile README** — рендерим `https://github.com/:login/:login/blob/main/README.md` если есть (как GitHub profile README).
- **Achievements:** `First Mod`, `10k Downloads`, `GPL Defender` (все моды GPL).
- **Follow:** подписка на автора (email / webhook) — `POST /u/:login/follow` → уведомляем о новых релизах (через `author_follows` таблицу).
- **Organizations:** группируем моды по `owner` (из `mods.owner`), страница `/org/:owner` агрегирует всех авторов организации.
- **Sponsors:** кнопка `Sponsor` → ссылка на `github.com/sponsors/:login` если у автора включён Sponsors.
---
## 5. Альтернативы — когда добавлять второй провайдер
| Провайдер | Когда добавлять | Как |
|---|---|---|
| **Discord OAuth** | Если заведём Discord сервер и хотим связать роли | `GET /auth/discord` → линк к `authors.discord_id`, не как замена GitHub, а как `linked_accounts` |
| **Google / Email magic link** | Если появятся читатели-комментаторы без GitHub | Только для `Reader` роли, без права публикации. Публикация всё равно требует GitHub линк (`GET /link/github`) |
| **Passkeys / WebAuthn** | Если хотим passwordless для модераторов | Избыточно на MVP |
| **Gitea / Codeberg / GitLab** | Если хотим тру-децентрализацию | Добавляем `provider: github|gitlab|codeberg` в `authors`, но каждый — отдельный OAuth. На MVP — только GitHub |
**Архитектура на будущее (не делаем сейчас, но не блокируем):**
```sql
CREATE TABLE linked_accounts (
github_id BIGINT REFERENCES authors(github_id),
provider VARCHAR(16) NOT NULL, -- discord | google
provider_id VARCHAR(128) NOT NULL,
PRIMARY KEY (provider, provider_id)
);
-- Публикация мода всё равно требует linked GitHub с доступом к репо
```
**Рекомендация:** MVP — **только GitHub**. Второй провайдер — Discord линк **после** первых 500 пользователей, если попросят.
---
## 6. Безопасность и приватность
- Никаких паролей — нечего утекать.
- `access_token` GitHub храним только в памяти/JWT, не в БД (или encrypted at rest).
- Rate limit на `/auth/*` — 10 req/min per IP.
- Удаление аккаунта: `DELETE /me` → удаляем `authors` + `mod_authors`, но `mods` остаются ( orphan → показываем `by @deleted` ), т.к. код уже open source и на GitHub.
- GDPR: `GET /me/export` → JSON со всеми данными, `DELETE` — право на забвение (кроме публичных модов).
---
## 7. Расширенная авторизация — твои идеи (оценка)
### 7.1 API Keys / PAT — **да, делаем в Phase 1**
Генерация в `/settings/tokens` с кастомными скоупами `read:mods`, `write:mods`, `webhooks:manage`.
- Хранение: `personal_access_tokens (id, github_id, token_hash, scopes[], expires_at)` — храним только `SHA256(token)` как у GitHub.
- Зачем: CI/CD (`github actions: indexium publish --token $INDEXIUM_TOKEN`), лаунчеры без браузера.
- Риск: утечка → лимит скоупов + `expires_at` 30/90 дней + `last_used_at` + revoke.
- **Вердикт:** берём в MVP — 1 таблица + 2 эндпоинта, без OAuth сервера.
### 7.2 OAuth2 Provider / Device Code Flow (RFC 8628) — **круто, но Phase 2**
Ты предлагаешь сделать Indexium IdP для лаунчеров: лаунчер показывает `ABCD-1234` → юзер на `indexium.example.com/activate` подтверждает.
- Плюс: идеален для Prism на Linux/TV/без браузера, как у GitHub CLI (`gh auth login --web`).
- Минус: нужно реализовать полноценный Authorization Server (`/oauth/authorize`, `/oauth/token`, `/oauth/device/code`, `/oauth/device/verify`) + consent screen + refresh tokens. Это +2-3 недели.
- Альтернатива на MVP: **PAT** — лаунчер просит вставить токен вручную (как `gh` с PAT). UX хуже, но без IdP.
- **Вердикт:** проектируем сейчас (закладываем `oauth_clients`, `device_codes`), реализуем после PAT когда попросят лаунчеры.
### 7.3 Discord линк — **да, но как linked_account, не как логин**
- Флоу: `GET /auth/discord` → `linked_accounts (github_id, provider='discord', provider_id)` → бот выдаёт `Verified Modder` на сервере Indexium, шлёт DM о релизах.
- Не делаем Discord как замену GitHub — публикация всё равно требует GitHub. Это синк ролей, не вход.
- **Вердикт:** делаем после MVP, когда заведём Discord сервер.
---
## 8. Фичи профиля — разбор твоих идей
### 8.1 Для разработчиков (оценка)
| Идея | Оценка | Комментарий |
|---|---|---|
| **Дашборд аналитики** (скачивания по версиям/лоадерам/OS, краш-логи) | **Phase 2** | Скачивания считаем агрегатом `downloads_daily` (без IP), OS — из `User-Agent` лаунчера если пришлёт. Краш-логи — отдельный `POST /telemetry/crash` с анонимизацией, опционально. |
| **Организации/команды** (Team CoFH) | **MVP-лайт** | Уже есть `mod_authors` + `mods.owner` (org). Делаем `/org/:login` как агрегатор, `role=maintainer` для команды. Без отдельного `teams` на старте. |
| **Спонсорство** (GitHub Sponsors, Patreon, Ko-fi) | **MVP** | Поле `authors.sponsors: JSONB { github, patreon, kofi, bmc }` + кнопки в шапке профиля/мода. Парсим из GitHub `sponsors` API или ручной ввод. |
| **Verified + PGP/GPG подпись** | **MVP / Phase 2** | `verified` уже в `mods` — ставим если репо через GitHub App и `license` ok. PGP — показываем `gpg_keys` из GitHub API (`GET /users/:login/gpg_keys`), проверка `.asc` рядом с `.jar` — Phase 2. |
### 8.2 Для игроков
| Идея | Оценка |
|---|---|
| **Коллекции / Модпаки** (`My OptiFine Alternatives`) с экспортом в Prism/CurseForge | **Phase 2, хит** | `collections (id, author_id, slug, title, mods[] JSONB, visibility)` + `collection_stars`. Экспорт — `GET /collections/:slug/export?format=prism|packwiz`. Виральная фича. |
| **Star / Follow + подписки** (уведомления о релизе под `1.20.1+fabric`) | **MVP-лайт** | `stars (github_id, mod_id)`, `follows (github_id, author_id)` + фильтр `notify_game_version/loader`. Уведомления — сначала in-app + Discord DM, email позже. |
| **Activity Feed** | **Phase 2** | Лента из `webhook_deliveries` + `collections` + `stars` по подпискам. |
### 8.3 Геймификация и виджет
- **Бейджи:** `Early Adopter` (id <1000), `Bug Hunter` (репорты), `Top Contributor` (N релизов/мес), `Open Source Veteran` (GitHub age >5 лет через `created_at` из API). Храним `badges (github_id, badge_id)` — выдаём воркером раз в день. Показываем на `/u/:login`.
- **Showcase Widget SVG:** `GET /v1/badges/:slug/downloads.svg` и `GET /v1/badges/:slug/version.svg` — генерируем SVG на лету (как `shields.io`), кэш 1h, без JS. Пример: `![Indexium](https://api.indexium.example.com/v1/badges/sodium-extra/downloads.svg)` — **делаем в MVP**, это маркетинг.
---
## 9. Итоговая приоритизация (что берём когда)
**MVP (следующие 2 недели):** GitHub OAuth only + PAT + `verified` + sponsors + star/follow (без email) + SVG badges + `/u/:login` + `/org/:login`
**Phase 2 (после 100 модов):** Device Flow + Discord linked + Collections + Activity Feed + аналитика + PGP
**Backlog:** краш-логи, `Top Contributor` лидерборд, Profile README
См. также: `catalog-philosophy.md`, `api-spec.md` (раздел Auth), `database-schema.md` (authors/mod_authors), `adr/004-auth-strategy.md` (создать).
## 10. Что решить сейчас
1. Подтверди: **PAT в MVP — да?** (я заложил, это быстро).
2. Device Flow — **проектируем сейчас, код позже** — ок?
3. Коллекции — делать сразу после MVP или откладываем до 500 юзеров?

View file

@ -0,0 +1,82 @@
# Философия каталога Indexium — Open Source Only, Zero Storage
> **Тезис:** Indexium — не хостинг файлов. Ты даёшь свой GitHub, мы даём индексацию, поиск и доверие. Все моды в каталоге обязаны быть open source.
---
## 1. Принцип Zero Storage
| Храним у себя | НЕ храним у себя |
|---|---|
| Метаданные `fabric.mod.json` / `mods.toml` | `.jar` / `.zip` артефакты |
| `README.md`, `LICENSE`, иконка (кэш) | Скомпилированный байткод |
| `SHA256`, `file_size`, `game_versions`, `loaders` | Исходники (берём с GitHub) |
| `search_vector` для FTS | Логи скачиваний с IP |
**Как работает:**
- Релиз публикуется в `github.com/<owner>/<repo>/releases` → webhook → воркер делает 2-3 `Range Request` к CDN (`objects.githubusercontent.com`) → парсит только центральную директорию ZIP → сохраняет метаданные в Postgres → отдаёт клиенту **прямую ссылку** `https://github.com/.../releases/download/...`
- Трафик не идёт через нас. Мы не платим за egress, не упираемся в лимиты хранения.
**Почему это круто:**
- Дешёво: VPS $5 + managed Postgres, без S3.
- Честно: автор контролирует файлы, может удалить релиз — он пропадёт и у нас (через webhook `release.deleted`).
- Устойчиво к DMCA: мы — индексатор, а не дистрибьютор (как `crates.io` vs `GitHub`).
---
## 2. Open Source Only — честь и правило
### Что значит "обязан быть open source"
Мод принимается в каталог только если:
1. **Репозиторий публичный** (`private: false` через GitHub API).
2. **Есть файл лицензии** в корне: `LICENSE` / `COPYING` / `LICENSE.md`. Проверяем через `GET /repos/{owner}/{repo}/license` — поле `license.spdx_id != null` и `license.spdx_id != "NOASSERTION"`.
3. **Лицензия из allow-list OSI:** `MIT`, `Apache-2.0`, `GPL-2.0`, `GPL-3.0`, `LGPL-2.1`, `LGPL-3.0`, `MPL-2.0`, `BSD-2/3-Clause`, `CC0-1.0`, `Unlicense`, `EUPL-1.2`, `AGPL-3.0`. Список расширяется через ADR.
4. **Исходники соответствуют артефакту** (best-effort): проверяем что в репо есть `fabric.mod.json` / `gradle.properties` с тем же `mod_id`/`version` что и в `.jar`. Полная reproducible-build проверка — в backlog.
5. **Нет обфускации/шифрования** в релизе без исходников: если воркер находит `Runtime.exec` без открытого кода — флаг `suspicious`.
> **На MVP** достаточно п.1 + п.2 (любая распознанная лицензия GitHub). Строгий OSI allow-list включаем после первых 100 модов.
### Как проверяем при импорте
```
POST /mods/import { repo: "owner/repo" }
→ GitHub API: GET /repos/{repo} → private? reject 422
→ GET /repos/{repo}/license → null? reject 422 "LICENSE required — open source only"
→ GET /repos/{repo}/contents/fabric.mod.json?ref=main → not found? reject
→ Создаём mods + ставим webhook
```
При каждом `release.published` повторно проверяем лицензию — если автор сменил на `NOASSERTION`/сделал приватным → мод помечается `deprecated`, скрывается из поиска, но старые версии доступны (кэш).
### Что показываем пользователю
- Бейдж `OSI: MIT` на карточке мода, ссылка на `LICENSE` на GitHub.
- Фильтр `license:MIT` в поиске.
- Страница `/manifesto` — манифест: "Почему только open source" (прозрачность, безопасность, форки, обучение).
### Edge cases
- **Форки:** разрешены, но `slug` уникален, показываем `fork_of: owner/repo`. Оригинал помечается `upstream`.
- **Мульти-мод репо (монорепо):** на MVP 1 репо = 1 мод. Позже — поддержка `mods.toml` с несколькими `modId`.
- **Организация vs личный акк:** оба ок, если репо публичное и лицензия есть.
- **Что если автор закрыл репо?** Webhook `repository.privatized` → скрываем мод, чистим кэш, храним метаданные 30 дней для восстановления.
---
## 3. Что это даёт экосистеме
- **Доверие:** любой может `git clone`, проверить код, собрать самому — нет "левый jar с майнером".
- **Долговечность:** даже если Indexium умрёт, моды живут на GitHub.
- **Культура:** стимулируем PR'ы, а не "скачал и забыл". Профили показывают контрибьюторов, а не только owner.
---
## 4. Что НЕ делаем
- Не принимаем бинарники без исходников (даже если автор "обещает" открыть позже).
- Не зеркалируем закрытые репозитории, даже с токеном.
- Не храним `.jar` у себя даже кэшем (кроме 64KB хвоста для парсинга — эфемерно).
См. также: `docs/auth-profiles.md` — как профили усиливают open source (контрибьюторы, верификация), `docs/adr/004-open-source-only.md`.

250
docs/database-schema.md Normal file
View file

@ -0,0 +1,250 @@
# Схема БД Indexium
> PostgreSQL 16+ с расширениями `pg_trgm`, `pgvector` (опционально), `uuid-ossp`/`pgcrypto`.
---
## 1. Расширения
```sql
CREATE EXTENSION IF NOT EXISTS "pgcrypto"; -- gen_random_uuid()
CREATE EXTENSION IF NOT EXISTS "pg_trgm"; -- fuzzy search
-- CREATE EXTENSION IF NOT EXISTS vector; -- pgvector, когда нужен семантический поиск
```
## 2. Таблицы
### `authors` — авторы (зеркало GitHub users)
```sql
CREATE TABLE authors (
github_id BIGINT PRIMARY KEY, -- GitHub user ID
login VARCHAR(39) NOT NULL UNIQUE, -- GitHub login
avatar_url TEXT,
created_at TIMESTAMPTZ DEFAULT now()
);
```
### `mods` — моды (один репозиторий = один мод на MVP)
```sql
CREATE TABLE mods (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
github_repo_id BIGINT UNIQUE NOT NULL,
slug VARCHAR(64) UNIQUE NOT NULL, -- URL-friendly, напр. "sodium-extra"
name VARCHAR(128) NOT NULL,
summary TEXT, -- короткое описание из манифеста
description TEXT, -- README.md (markdown, кэшируем)
author_github_id BIGINT NOT NULL REFERENCES authors(github_id),
github_repo_name VARCHAR(128) NOT NULL, -- "owner/repo"
default_branch VARCHAR(32) DEFAULT 'main',
icon_url TEXT,
verified BOOLEAN DEFAULT FALSE,
suspicious BOOLEAN DEFAULT FALSE,
search_vector TSVECTOR, -- материализованный FTS вектор
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX idx_mods_search ON mods USING GIN (search_vector);
CREATE INDEX idx_mods_trgm ON mods USING GIN (name gin_trgm_ops, summary gin_trgm_ops);
CREATE INDEX idx_mods_author ON mods (author_github_id);
```
Триггер для `search_vector`:
```sql
CREATE OR REPLACE FUNCTION mods_search_vector_update() RETURNS trigger AS $$
BEGIN
NEW.search_vector :=
setweight(to_tsvector('english', coalesce(NEW.name,'')), 'A') ||
setweight(to_tsvector('english', coalesce(NEW.summary,'')), 'B') ||
setweight(to_tsvector('english', coalesce(NEW.description,'')), 'C');
RETURN NEW;
END $$ LANGUAGE plpgsql;
CREATE TRIGGER trg_mods_search_vector
BEFORE INSERT OR UPDATE OF name, summary, description ON mods
FOR EACH ROW EXECUTE FUNCTION mods_search_vector_update();
```
### `mod_versions` — версии / релизы
```sql
CREATE TABLE mod_versions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
mod_id UUID NOT NULL REFERENCES mods(id) ON DELETE CASCADE,
version_number VARCHAR(32) NOT NULL, -- semver из манифеста/тега
game_versions VARCHAR(32)[] NOT NULL, -- e.g. ['1.20.1', '1.21']
loaders VARCHAR(16)[] NOT NULL, -- e.g. ['fabric','quilt','neoforge']
download_url TEXT NOT NULL, -- https://github.com/.../releases/download/...
file_name VARCHAR(128) NOT NULL, -- sodium-1.2.3.jar
file_sha256 CHAR(64) NOT NULL,
file_size BIGINT,
published_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ DEFAULT now(),
UNIQUE(mod_id, version_number)
);
CREATE INDEX idx_versions_mod ON mod_versions (mod_id, published_at DESC);
CREATE INDEX idx_versions_lookup ON mod_versions USING GIN (game_versions, loaders);
CREATE INDEX idx_versions_sha ON mod_versions (file_sha256);
```
### `webhook_deliveries` — идемпотентность webhook'ов (<50ms ответ)
```sql
CREATE TABLE webhook_deliveries (
delivery_id VARCHAR(64) PRIMARY KEY, -- X-GitHub-Delivery (UUID от GitHub)
event VARCHAR(32) NOT NULL, -- "release"
action VARCHAR(32), -- "published"
repo_id BIGINT,
payload JSONB,
processed_at TIMESTAMPTZ DEFAULT now()
);
-- Уникальный индекс уже есть как PK, но явно для дедупа:
-- INSERT INTO webhook_deliveries (...) VALUES (...) ON CONFLICT (delivery_id) DO NOTHING
-- В handler: если affected_rows == 0 → 409 Already Processed, иначе push в Redis Streams.
```
> **Почему так:** Ingestion API должен ответить `202` за <50ms. Сначала `INSERT ... ON CONFLICT DO NOTHING` в `webhook_deliveries`, только потом `XADD` в Redis. Если `delivery_id` уже есть — сразу `409` без очереди.
### `mod_authors` — M2M авторы/контрибьюторы
На MVP `mods.author_github_id` достаточно (1 репо = 1 owner). Для организаций и соавторов — нормализуем сразу, чтобы не мигрировать болезненно:
```sql
CREATE TABLE mod_authors (
mod_id UUID NOT NULL REFERENCES mods(id) ON DELETE CASCADE,
github_id BIGINT NOT NULL REFERENCES authors(github_id) ON DELETE CASCADE,
role VARCHAR(16) NOT NULL DEFAULT 'owner', -- 'owner' | 'contributor' | 'maintainer'
created_at TIMESTAMPTZ DEFAULT now(),
PRIMARY KEY (mod_id, github_id)
);
CREATE INDEX idx_mod_authors_github ON mod_authors (github_id);
-- На MVP можно оставить mods.author_github_id как денормализованный owner
-- и дублировать его в mod_authors при создании мода (триггер или код).
```
> **MVP стратегия:** оставляем `mods.author_github_id` (как сейчас в `migrations/20260906000000_init_schema.sql`) для простых запросов, но добавляем `mod_authors` когда появится первый кейс организации. В `GET /mods/:slug` отдаём `authors: [{login, role}]` вместо одиночного `author`.
### `mod_versions.file_size` — откуда берётся
В `api-spec.md` поле `file_size` возвращается клиентам. Заполняется воркером из HTTP-заголовка:
```sql
-- уже в mod_versions: file_size BIGINT — bytes из Content-Length
```
Алгоритм воркера (`jar_parser.rs`):
1. `HEAD download_url` → `Content-Length` + `Accept-Ranges: bytes`.
2. Если `Content-Length` отсутствует — fallback на `GET` с `Range: bytes=0-0` и парсинг `Content-Range`.
3. Значение пишется в `mod_versions.file_size` при `INSERT`.
> GitHub CDN (`objects.githubusercontent.com`) всегда отдаёт `Content-Length` и поддерживает `Range` для release assets — проверено для `.jar` до 50MB.
### `dependencies` (опционально, нормализованная)
На MVP храним зависимости как `JSONB` в `mod_versions` или отдельной таблицей:
```sql
CREATE TABLE mod_dependencies (
version_id UUID REFERENCES mod_versions(id) ON DELETE CASCADE,
depends_on_mod_id UUID REFERENCES mods(id), -- nullable если внешний мод не в индексе
mod_id_str VARCHAR(64) NOT NULL, -- id из fabric.mod.json depends
version_range VARCHAR(64), -- ">=1.0.0"
PRIMARY KEY (version_id, mod_id_str)
);
```
## 3. Телеметрия — аналог bStats (см. docs/analytics.md)
```sql
-- Полуагрегат: один пинг = одна строка, TTL 30 дней (DELETE via cron)
CREATE TABLE mod_telemetry_pings (
id BIGSERIAL PRIMARY KEY,
mod_id UUID NOT NULL REFERENCES mods(id) ON DELETE CASCADE,
server_hash CHAR(64) NOT NULL, -- sha256(server_uuid + daily_salt)
mc_version VARCHAR(16) NOT NULL,
loader VARCHAR(16) NOT NULL,
os VARCHAR(16) NOT NULL,
java_version VARCHAR(16) NOT NULL,
player_count INT NOT NULL DEFAULT 0,
pinged_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_telemetry_lookup ON mod_telemetry_pings (mod_id, pinged_at DESC);
CREATE INDEX idx_telemetry_hash ON mod_telemetry_pings (server_hash, pinged_at);
-- Суточный агрегат — хранится навсегда
CREATE TABLE mod_daily_stats (
mod_id UUID NOT NULL REFERENCES mods(id) ON DELETE CASCADE,
date DATE NOT NULL,
active_servers INT NOT NULL DEFAULT 0, -- COUNT(DISTINCT server_hash) за день
active_players INT NOT NULL DEFAULT 0,
breakdown_json JSONB NOT NULL, -- { mc_versions:{}, loaders:{}, os:{}, java:{}, custom:{...} }
PRIMARY KEY (mod_id, date)
);
-- Соль для анонимизации (ротация daily)
CREATE TABLE analytics_salts (
date DATE PRIMARY KEY,
salt CHAR(64) NOT NULL
);
-- Хеш: server_hash = sha256(server_uuid || salt_for_today) — позволяет считать уникальные за день, но не трекать сквозь дни.
-- Rate limit: Redis SET server_hash:mod_id NX EX 900 (1 пинг / 15 мин)
-- TTL: DELETE FROM mod_telemetry_pings WHERE pinged_at < NOW() - INTERVAL '30 days' (cron hourly)
-- Агрегация: кроном раз в час INSERT INTO mod_daily_stats ... ON CONFLICT DO UPDATE COUNT(DISTINCT server_hash)
```
> Postgres хватает до ~10M пингов/мес. При росте — `SELECT create_hypertable('mod_telemetry_pings','pinged_at')` (TimescaleDB) или ClickHouse без смены схемы.
## 4. Пример запросов
### Поиск с FTS + фильтры
```sql
SELECT id, name, summary, ts_rank(search_vector, query) AS rank
FROM mods, plainto_tsquery('english', $1) query
WHERE search_vector @@ query
AND suspicious = false
AND EXISTS (
SELECT 1 FROM mod_versions v
WHERE v.mod_id = mods.id
AND v.game_versions && ARRAY[$2]::varchar[]
AND v.loaders && ARRAY[$3]::varchar[]
)
ORDER BY rank DESC
LIMIT 20 OFFSET $4;
```
### Fuzzy (опечатки)
```sql
SELECT name, similarity(name, 'sodim') AS sml
FROM mods
WHERE name % 'sodim' -- оператор pg_trgm
ORDER BY sml DESC LIMIT 10;
```
## 5. Миграции
Хранятся в `indexium-backend/migrations/` (sqlx):
```
migrations/
20260906000000_init_schema.sql -- mods, mod_versions
20260907000000_telemetry.sql -- mod_telemetry_pings, mod_daily_stats, analytics_salts
```
Запуск: `sqlx migrate run` / `cargo sqlx migrate run`.
## 6. Сиды
Для дев-окружения: `migrations/seeds/dev.sql` — 5 фейковых модов + версии, чтобы фронт сразу имел данные.
## 7. Будущие расширения
- `pgvector` колонка `embedding vector(1536)` для семантического поиска по README.
- Партиционирование `mod_versions` по `published_at` если >1M строк.
- Материализованное представление `popular_mods` (top по скачиваниям).

119
docs/deployment.md Normal file
View file

@ -0,0 +1,119 @@
# Деплой Indexium
## MVP (один VPS, <$20/мес)
### Инфра
- **VPS**: Hetzner CX22 (2 vCPU, 4GB) или аналог.
- **Postgres**: Supabase Free / Neon Free / или Docker на том же VPS.
- **Redis**: Valkey/Redis в Docker.
- **Reverse proxy**: Caddy (авто TLS) или Nginx.
### Компоновка
```
VPS
├─ Caddy :80/:443 → :3000 (Axum) + :5173 (SvelteKit SSR)
├─ indexium-backend (systemd / docker)
├─ indexium-frontend (adapter-node)
├─ postgres:5432
└─ redis:6379
```
### Docker Compose (prod)
```yaml
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: indexium
POSTGRES_USER: indexium
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes: [pgdata:/var/lib/postgresql/data]
healthcheck: { test: ["CMD-SHELL", "pg_isready -U indexium"] }
redis:
image: valkey/valkey:8-alpine
command: ["valkey-server", "--save", ""]
healthcheck: { test: ["CMD", "valkey-cli", "ping"] }
backend:
build: ./indexium-backend
env_file: ./indexium-backend/.env
depends_on: [postgres, redis]
ports: ["3000:3000"]
frontend:
build: ./indexium-frontend
environment: { PUBLIC_API_URL: "https://api.indexium.example.com" }
ports: ["5173:3000"]
volumes: { pgdata: {} }
```
### Env (backend)
```
DATABASE_URL=postgres://indexium:***@postgres:5432/indexium
REDIS_URL=redis://redis:6379
GITHUB_APP_ID=...
GITHUB_APP_PRIVATE_KEY=...
WEBHOOK_SECRET=...
RUST_LOG=info
```
### Деплой шаги
```bash
# на VPS
git pull origin main
docker compose -f docker-compose.prod.yml build
docker compose -f docker-compose.prod.yml up -d
# миграции
docker compose exec backend sqlx migrate run
# проверка
curl https://api.indexium.example.com/api/v1/health
```
### Бэкапы
- Postgres: ежедневный `pg_dump` + WAL (PITR если managed).
- Хранить 7 дней в S3/R2.
- Тест восстановления раз в месяц.
### Мониторинг (MVP минимум)
- `/health` + Uptime Kuma / Hetrix.
- Логи: `journalctl -u indexium-backend` или `docker logs`.
- Позже: Prometheus + Grafana + Loki.
### CI/CD (GitHub Actions)
```yaml
# .github/workflows/ci.yml
on: [push, pull_request]
jobs:
backend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- run: cargo fmt --check
- run: cargo clippy -- -D warnings
- run: cargo test
frontend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install --cwd indexium-frontend
- run: bun run check --cwd indexium-frontend
```
Деплой: `on: push: branches: [main]` → SSH в VPS → `git pull && docker compose up -d` (или через Watchtower).
### Масштабирование (когда >1k RPS)
- Вынести Worker в отдельный сервис/реплики.
- Postgres read-replica.
- Redis Cluster / NATS JetStream.
- Cloudflare перед API (кэш GET).

86
docs/git-strategy.md Normal file
View file

@ -0,0 +1,86 @@
# Git-стратегия — почему монорепо
## Решение (ADR-001)
**Выбрано: монорепо в корне `/` с двумя пакетами `indexium-backend/` и `indexium-frontend/`.**
Альтернатива — полирепо (два отдельных git) — отклонена на старте.
## Почему монорепо
| Критерий | Монорепо | Полирепо |
|----------|----------|----------|
| Onboarding нового разработчика | `git clone` один раз, `docker compose up` | 2 clone, синхронизация версий |
| Атомарные изменения API+UI | Один коммит/PR меняет `api-spec` + фронт-клиент | Два PR, риск рассинхрона |
| CI | Один pipeline, один статус | Два pipeline, дублирование |
| Версионирование контрактов | Фронт всегда соответствует бэку в `main` | Нужен отдельный версионинг |
| Стоимость поддержки | Минимальна для 1-3 человек | Оверхед: 2 набора настроек, 2 issue-треккера |
Монорепо оправдан пока команда <10 человек и релизный цикл единый. Если в будущем бэкенд и фронт разойдутся по командам/каденсу — легко разрезать через `git filter-repo` или `git subtree`.
## Что было сделано
1. Удалён пустой `.git` из `indexium-backend/` (коммитов не было — безопасно).
2. `git init --initial-branch=main` в корне `Indexium/`.
3. Корневой `.gitignore` + локальные.
4. Весь код теперь трекается как:
```
Indexium/
.git/
.gitignore
README.md
todo.md
docs/
indexium-backend/
indexium-frontend/
```
## Workflow
### Ветки
- `main` — защищённая, только через PR.
- `feat/<scope>-<short>` — фичи, напр. `feat/webhook-hmac`.
- `fix/<scope>-<short>`.
### Коммиты (Conventional Commits)
```
feat(api): add GET /mods with FTS
fix(worker): handle missing quilt.mod.json
docs(arch): describe queue retry
chore(frontend): bump svelte 5.56 → 5.57
```
### PR
- Один PR = одна фича/фикс.
- Если меняется API — в том же PR обновляется `docs/api-spec.md` и фронт-клиент.
- CI должен пройти: `cargo fmt --check`, `cargo clippy`, `cargo test`, `svelte-check`.
### Локально
```bash
git clone <url> Indexium && cd Indexium
git checkout -b feat/my-feature
# ... код ...
cargo fmt && cargo clippy -- -D warnings
git add -A && git commit -m "feat(scope): message"
git push -u origin feat/my-feature
# → создать PR
```
## Когда резать на полирепо
Сигналы что пора:
- >10 активных контрибьюторов, частые конфликты в `main`.
- Фронт деплоится 10× в день, бэк — 1× в неделю (разный каденс).
- Нужны разные права доступа (внешние контрибьюторы только к фронту).
Как резать: `git subtree split -P indexium-backend -b backend-only` и аналогично для фронта, либо `git filter-repo --path`.
## Альтернативы (для справки)
- **Git submodules** — не рекомендуется: сложны, легко сломать, плохой DX.
- **Polyrepo + shared package** — имеет смысл если выносить `openapi`/`types` в отдельный npm/crate.
## ADR
- ADR-001: Монорепо vs полирепо — принято монорепо (этот документ).
- Следующие ADR складывать в `docs/adr/NNN-title.md`.