chore: init monorepo with GPL-3.0 license, docs, backend skeleton, frontend wiring
This commit is contained in:
commit
43cf0e277d
57 changed files with 5027 additions and 0 deletions
38
docs/README.md
Normal file
38
docs/README.md
Normal 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
26
docs/adr/001-monorepo.md
Normal 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
|
||||
26
docs/adr/002-async-indexer.md
Normal file
26
docs/adr/002-async-indexer.md
Normal 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` на каждый релиз — отклонено: трафик, медленно.
|
||||
33
docs/adr/003-search-engine.md
Normal file
33
docs/adr/003-search-engine.md
Normal 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).
|
||||
37
docs/adr/004-auth-strategy.md
Normal file
37
docs/adr/004-auth-strategy.md
Normal 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
29
docs/adr/005-analytics.md
Normal 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
119
docs/analytics.md
Normal 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
318
docs/api-spec.md
Normal 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
219
docs/architecture.md
Normal 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
232
docs/auth-profiles.md
Normal 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. Пример: `` — **делаем в 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 юзеров?
|
||||
|
||||
82
docs/catalog-philosophy.md
Normal file
82
docs/catalog-philosophy.md
Normal 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
250
docs/database-schema.md
Normal 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
119
docs/deployment.md
Normal 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
86
docs/git-strategy.md
Normal 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`.
|
||||
Loading…
Add table
Add a link
Reference in a new issue