Потоки запросов (Request Flows)¶
Детальное описание всех потоков данных между Panel, API-шлюзом, Zitadel, Lago и Typesense.
Содержание¶
- Общая цепочка middleware
- Аутентификация
- 2.1 Panel Login (V2 Session)
- 2.2 Panel Register
- 2.3 Bearer JWT (SDK / M2M)
- 2.4 Scoped API Key (клиентский JS)
- 2.5 Auth.js OIDC (legacy)
- 2.6 Session Check (Panel)
- Поиск
- 3.1 POST /api/v1/search
- 3.2 Scoped Search Key (генерация)
- Typesense Engine Proxy
- 4.1 Создание коллекции
- 4.2 Запрос через Engine Proxy
- 4.3 Feature Detection в прокси
- Документы
- Биллинг
- 6.1 Billing Gate (проверка подписки)
- 6.2 Feature Gate (проверка фич)
- 6.3 Rate Limiting
- 6.4 Lago Webhooks
- 6.5 Usage Tracking
- 6.6 Dashboard (использование)
- Data Sources (краулинг)
- Reindex (переиндексация)
- AI / RAG
- Cron (планировщик)
- Panel → API маппинг
1. Общая цепочка middleware¶
Каждый HTTP-запрос к api.aacsearch.com проходит через цепочку в строгом порядке. Шаг может прервать цепочку (throw HTTPException) или добавить данные в контекст.
graph TD
REQ["Запрос → Cloudflare Workers Edge"]
S0["<b>0. resolveSecrets(env)</b><br/>SecretBinding → строки (Promise.all 9)<br/>Результат: c.set('secrets', ResolvedSecrets)<br/>⚠ ДОЛЖЕН быть первым"]
S1["<b>1. CORS</b><br/>Origins: app/platform/billing/docs.aacsearch.com<br/>Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS<br/>Headers: Content-Type, Authorization, Cookie, X-AACSEARCH-API-KEY<br/>Credentials: true • OPTIONS → 204"]
S2["<b>2. authConfig()</b><br/>Инициализация Auth.js + Zitadel OIDC<br/>AUTH_SECRET, ZITADEL_CLIENT_SECRET"]
S3{"<b>3. Маршрутизация</b>"}
PUB1["/api/auth/v2/* → authProxyRoutes"]
PUB2["/api/auth/* → Auth.js OIDC"]
PUB3["/webhooks/lago → без auth"]
PUB4["/health, /doc → публичные"]
S4["<b>4. tenantResolver()</b><br/>acme.aacsearch.com → slug='acme'<br/>tenantId из JWT/сессии на шаге 5"]
S5["<b>5. apiAuth() — 4-факторная</b><br/>① X-AACSEARCH-API-KEY → verifyScopedSearchKey<br/>② Bearer JWT → verifyBearer (JWKS)<br/>③ Cookie aac_session → verifyV2Session<br/>④ Auth.js session → verifyAuth<br/>Результат: c.set('tenantId')<br/>⚠ 401 если ни один не сработал"]
S6["<b>6. billingGate()</b><br/>search, documents, engine, ai, data-sources, reindex<br/>НЕ применяется к: billing/*, api-keys<br/>Результат: c.set('planCode')<br/>⚠ 402 нет подписки • 502 Lago offline"]
S7["<b>7. rateLimit()</b><br/>Все /api/v1/*<br/>⚠ 429 если лимит превышен"]
S8["<b>8. featureGate(feature)</b><br/>/conversations → rag • /synonym_sets → synonyms<br/>/curation_sets → curations • /analytics → analytics_advanced<br/>/data-sources → data_sources • /ai/nl-search → nl_search<br/>⚠ 403 если фича недоступна"]
HANDLER["<b>Route Handler</b>"]
REQ --> S0 --> S1 --> S2 --> S3
S3 -->|"/api/v1/*"| S4
S3 --> PUB1 & PUB2 & PUB3 & PUB4
S4 --> S5 --> S6 --> S7 --> S8 --> HANDLER
style REQ fill:#e3f2fd,color:#000
style HANDLER fill:#e8f5e9,color:#000
style S3 fill:#fff3e0,color:#000
style PUB1 fill:#fce4ec,color:#000
style PUB2 fill:#fce4ec,color:#000
style PUB3 fill:#fce4ec,color:#000
style PUB4 fill:#fce4ec,color:#000
2. Аутентификация¶
2.1 Panel Login (V2 Session)¶
sequenceDiagram
participant P as Panel SPA
participant A as API-шлюз
participant Z as Zitadel
P->>A: POST /api/auth/v2/login<br/>{email, password}
Note over A: Извлечь email, password<br/>PAT = secrets.ZITADEL_SERVICE_ACCOUNT_TOKEN
A->>Z: POST /v2/sessions<br/>Authorization: Bearer {PAT}<br/>checks: { user: { loginName: email } }
Z-->>A: sessionId
A->>Z: POST /v2/sessions/{sessionId}<br/>checks: { password: { password } }
Z-->>A: sessionToken
Note over A: Set-Cookie: aac_session={sessionId}:{sessionToken}<br/>Domain=.aacsearch.com; Secure; HttpOnly; SameSite=Lax
A-->>P: 200 + Set-Cookie
Note over P: Сохранить cookie → Dashboard
2.2 Panel Register¶
sequenceDiagram
participant P as Panel SPA
participant A as API-шлюз
participant Z as Zitadel
participant L as Lago
P->>A: POST /api/auth/v2/register<br/>{email, password, givenName, familyName, orgName}
A->>Z: POST /v2/users/human<br/>{username, profile, email, password, organization}
Z-->>A: userId, orgId
Note over A: Авто-логин (как в §2.1)
A->>Z: POST /v2/sessions → sessionId
A->>Z: POST /v2/sessions/{id} → check password
Z-->>A: sessionToken
Note over A: waitUntil (неблокирующе)
A-)L: POST /customers<br/>{external_id: orgId, name: orgName, email}
A-->>P: 200 + Set-Cookie: aac_session=...
2.3 Bearer JWT (SDK / M2M)¶
sequenceDiagram
participant S as SDK / Machine User
participant A as API-шлюз
participant Z as Zitadel
S->>A: GET /api/v1/engine/collections<br/>Authorization: Bearer jwt
Note over A: apiAuth() → обнаружен Bearer<br/>verifyBearer():
alt In-memory cache (15 мин TTL) HIT
Note over A: Использовать кешированный JWKS
else KV cache "zitadel:jwks" HIT
Note over A: createLocalJWKSet()
else MISS — Remote fetch
A->>Z: GET {ZITADEL_JWKS_URI}
Z-->>A: JWKS JSON
end
Note over A: jwtVerify(token, jwks, {issuer, audience})<br/>⚠ 401 если подпись невалидна
Note over A: tenantId = jwt[resourceowner:id] ?? org_id<br/>⚠ 403 если tenantId отсутствует<br/><br/>planCode = jwt.plan_code (если есть)<br/>⚠ Machine users: plan_code ОТСУТСТВУЕТ<br/>→ определяется в billingGate
Note over A: c.set("tenantId"), c.set("planCode")<br/>→ продолжение цепочки
2.4 Scoped API Key (клиентский JS)¶
sequenceDiagram
participant B as Браузер (JS SDK)
participant A as API-шлюз
B->>A: GET /api/v1/search?q=hello<br/>X-AACSEARCH-API-KEY: scopedKey
Note over A: apiAuth() → обнаружен X-AACSEARCH-API-KEY<br/>verifyScopedSearchKey(parentKey, scopedKey):
Note over A: 1) base64 → digest + prefix + JSON<br/>2) HMAC-SHA256(parentKey, JSON) == digest<br/>3) prefix == parentKey[0:4]<br/>4) expires_at > Date.now()<br/>⚠ 401 если невалидный / просрочен
Note over A: JSON: {collection: "t_12345__products",<br/>filter_by: "tenant_id:=12345", expires_at: ...}
Note over A: tenantId = "12345" (из collection)<br/>c.set("scopedApiKey"), c.set("scopedCollection")<br/>→ запрос ограничен одной коллекцией
2.5 Auth.js OIDC (legacy)¶
sequenceDiagram
participant B as Браузер
participant A as API-шлюз
participant Z as Zitadel
B->>A: GET /api/auth/signin/zitadel
A-->>B: Redirect → {issuer}/oauth/v2/authorize<br/>?response_type=code&scope=openid resourceowner
B->>Z: Redirect to Zitadel Login UI
Note over Z: Ввод email/пароль<br/>Zitadel Action "inject-plan-code":<br/>ctx.v1.org.getMetadata()<br/>→ plan_code → JWT claim
Z-->>B: Redirect → callback?code=auth_code
B->>A: GET /api/auth/callback/zitadel?code=...
A->>Z: POST /oauth/v2/token<br/>grant_type=authorization_code
Z-->>A: access_token (JWT с plan_code)
Note over A: JWT callback:<br/>token.org_id = profile[resourceowner:id]<br/>token.plan_code = profile["plan_code"]
A-->>B: Set-Cookie: auth.js session
2.6 Session Check (Panel)¶
sequenceDiagram
participant P as Panel SPA
participant A as API-шлюз
participant Z as Zitadel
Note over P: authProvider.checkAuth()<br/>(каждые 30 сек, кеш на клиенте)
P->>A: GET /api/auth/v2/session<br/>Cookie: aac_session={sessionId}:{sessionToken}
Note over A: Извлечь sessionId из cookie
alt KV cache "session:{sessionId}" HIT (TTL 5 мин)
A-->>P: 200 {userId, orgId, displayName}
else KV MISS
A->>Z: GET /v2/sessions/{sessionId}<br/>Authorization: Bearer {PAT}
alt Zitadel 200
Z-->>A: session data
Note over A: tenantId = session.factors.user.organizationId<br/>userId = session.factors.user.id<br/>KV PUT "session:{sessionId}" TTL=300
A-->>P: 200 {userId, orgId, displayName, email}
else Zitadel 401/403
Z-->>A: ошибка авторизации
Note over A: ⚠ Ответ 503 (НЕ 401!)<br/>чтобы Panel не сделал logout
A-->>P: 503
Note over P: Тихая ошибка, stale кеш
else Zitadel 404
Z-->>A: сессия не найдена
Note over A: Сессия реально истекла
A-->>P: 401
Note over P: authProvider.logout()
end
end
3. Поиск¶
3.1 POST /api/v1/search¶
sequenceDiagram
participant C as Клиент
participant A as API-шлюз
participant T as Typesense
participant L as Lago
C->>A: POST /api/v1/search<br/>Authorization: Bearer jwt<br/>{q, limit, page, filters}
Note over A: [apiAuth] → tenantId = "org123"<br/>[billingGate] → planCode = "professional_v1"<br/>[rateLimit] → OK (< 500 req/min)
Note over A: 1) Zod валидация<br/>q: string, limit: 1-100, page: ≥1<br/>filters: SAFE_FILTER_KEYS only
Note over A: 2) buildFilterBy(tenantId, filters)<br/>→ "tenant_id:=org123 && status:=active && category:=news"<br/>⚠ Whitelist предотвращает инъекцию
A->>T: POST /collections/documents/documents/search<br/>{q, query_by: "title,content", filter_by, per_page, page}
T-->>A: Результаты поиска
Note over A: 4) Маппинг: hit → {id, title, snippet, score}<br/>highlights → snippet с <mark> тегами
A-)L: waitUntil: trackSearchRequest()<br/>(неблокирующе)
A-->>C: {total: 150, hits: [{id, title, snippet, score}]}
3.2 Scoped Search Key (генерация)¶
sequenceDiagram
participant P as Panel / SDK
participant A as API-шлюз
P->>A: POST /api/v1/api-keys/scoped<br/>Authorization: Bearer jwt<br/>{collection: "products", filters, ttlSeconds: 600}
Note over A: [apiAuth: JWT only, scoped keys запрещены]<br/>tenantId = "org123"<br/>prefixedCollection = "t_org123__products"
Note over A: generateScopedSearchKey():<br/>params = {collection: "t_org123__products",<br/>filter_by: "tenant_id:=org123 && status:=active",<br/>expires_at: now + 600}<br/>digest = HMAC-SHA256(parentKey, JSON)<br/>prefix = parentKey[0:4]<br/>key = base64(digest + prefix + JSON)
A-->>P: {key: "base64...", expiresAt: "..."}
4. Typesense Engine Proxy¶
4.1 Создание коллекции¶
sequenceDiagram
participant P as Panel
participant A as API-шлюз
participant T as Typesense
P->>A: POST /api/v1/engine/collections<br/>Cookie: aac_session=...<br/>{name: "products", fields: [...]}
Note over A: [apiAuth] → tenantId = "org123"<br/>[billingGate] → planCode<br/>[rateLimit] → OK
Note over A: 1) Перезапись тела:<br/>body.name = "t_org123__products"
A->>T: POST /collections<br/>{name: "t_org123__products", fields: [...]}
T-->>A: Коллекция создана
Note over A: 3) Перезапись ответа:<br/>response.name = "products"<br/>(удаление префикса)
A-->>P: {name: "products", fields: [...], num_documents: 0}
4.2 Запрос через Engine Proxy¶
Любой /api/v1/engine/* запрос проходит через 5 этапов:
| Этап | Действие | Пример |
|---|---|---|
| 1. URL rewrite | Добавление префикса t_{tenantId}__ |
/engine/collections/products → {TYPESENSE_URL}/collections/t_org123__products |
| 2. Body rewrite | POST/PUT/PATCH: body.name, body.collection, body.collection_name, multi_search: searches[].collection |
"products" → "t_org123__products" |
| 3. Blocked paths | /keys, /debug, /stats.json, /metrics.json, /operations, /analytics/flush |
403 "Access denied" |
| 4. Response rewrite | Удаление t_{tenantId}__ из имён + фильтрация по тенанту |
"t_org123__products" → "products" |
| 5. Error rewrite | Typesense 401/403 → 502 | Предотвращает logout Panel |
4.3 Feature Detection в прокси¶
При каждом запросе через engine proxy определяются используемые фичи:
| URL / Query / Body | Фича | Lago событие |
|---|---|---|
_geoloc, field:(lat,lng,r km) |
geo_search |
geo_query |
vector_query |
semantic_search |
semantic_query |
image_query, image_embedding |
image_search |
image_query |
voice_query |
voice_search |
voice_query |
join, join_as |
joins |
join_query |
group_by |
grouping |
— |
/multi_search |
multi_search |
multi_search_query |
/conversations |
rag |
rag_query |
/nl_search_models |
nl_search |
— |
Note
Фичи проверяются featureGate middleware ДО прокси. Usage events отправляются ПОСЛЕ успешного ответа через waitUntil().
5. Документы¶
Эндпоинты: POST /api/v1/documents (единичный), POST /api/v1/documents/bulk (до 1000, JSONL), DELETE /api/v1/documents/{id}
sequenceDiagram
participant P as Panel / SDK
participant A as API-шлюз
participant T as Typesense
participant L as Lago
P->>A: POST /api/v1/documents<br/>{id, title, content, category}
Note over A: [apiAuth] → tenantId<br/>[billingGate] → planCode<br/>[rateLimit] → OK
Note over A: 1) Zod валидация<br/>2) Авто-поля: +tenant_id, +created_at,<br/>+type="document", +status="active", +lang="en"
A->>T: POST /collections/t_org123__documents/documents?action=upsert
T-->>A: OK
A-)L: waitUntil: trackDocumentsIndexed(1)
A-->>P: Документ создан
DELETE /api/v1/documents/{id}
- GET документ по ID
- Проверить
tenant_id == tenantId(404 если чужой документ) - DELETE из Typesense
6. Биллинг¶
6.1 Billing Gate (проверка подписки)¶
graph TD
START["billingGate()<br/>tenantId = c.get('tenantId')"]
subgraph L0["Уровень 0: Entitlement Cache"]
KV0{"KV GET<br/>billing:{tenantId}:entitlement"}
KV0_HIT_OK["HIT + ok:true<br/>planCode из кеша<br/>TTL 300с (активные)"]
KV0_HIT_FAIL["HIT + ok:false<br/>TTL 60с (неактивные)"]
end
subgraph L1["Уровень 1: Webhook Marker"]
KV1{"KV GET<br/>billing:{tenantId}:subscription_active"}
KV1_HIT["HIT → JSON<br/>{external_id, plan_code, started_at}<br/>TTL 30 дней"]
end
subgraph L2["Уровень 2: Lago API (fallback)"]
LAGO["lago.customers.<br/>findAllCustomerSubscriptions(tenantId)<br/>Таймаут: 5 сек"]
LAGO_ACTIVE["Есть active подписка<br/>→ записать маркер + кеш"]
LAGO_NONE["Нет active<br/>→ кеш negative (60с)"]
LAGO_FAIL["Lago offline / таймаут"]
end
PASS["✅ PASS<br/>c.set('planCode')<br/>+ X-Billing-Warning headers"]
E402["❌ 402<br/>Active subscription required"]
E502["❌ 502<br/>Billing provider unavailable"]
START --> KV0
KV0 -->|"HIT ok:true"| KV0_HIT_OK --> PASS
KV0 -->|"HIT ok:false"| KV0_HIT_FAIL --> E402
KV0 -->|"MISS"| KV1
KV1 -->|"HIT"| KV1_HIT --> PASS
KV1 -->|"MISS"| LAGO
LAGO --> LAGO_ACTIVE --> PASS
LAGO --> LAGO_NONE --> E402
LAGO --> LAGO_FAIL --> E502
style PASS fill:#c8e6c9,color:#000
style E402 fill:#ffcdd2,color:#000
style E502 fill:#ffcdd2,color:#000
style L0 fill:#e3f2fd22
style L1 fill:#fff3e022
style L2 fill:#fce4ec22
6.2 Feature Gate (проверка фич)¶
graph TD
START["featureGate('synonyms')<br/>tenantId = c.get('tenantId')"]
subgraph C["1. KV Cache"]
KV{"KV GET<br/>entitlements:{tenantId}"}
KV_HIT["HIT → features JSON<br/>TTL 5 мин<br/>10% early refresh в последние 60с"]
end
subgraph S["2. Subscription lookup"]
SUB["KV billing:{tenantId}:subscription_active<br/>→ external_id<br/>ИЛИ Lago API fallback"]
end
subgraph L["3. Lago Entitlements"]
LAGO["findAllSubscriptionEntitlements(id)<br/>→ [{code, privileges}]"]
PARSE["privileges.find(p => p.code == 'enabled')<br/>override_value ?? value → boolean<br/>Записать KV TTL=300"]
end
CHECK_FEAT{"features[synonyms]?"}
PASS["✅ PASS"]
E403["❌ 403 Feature unavailable"]
E402["❌ 402 No subscription"]
START --> KV
KV -->|"HIT"| KV_HIT --> CHECK_FEAT
KV -->|"MISS"| SUB --> LAGO --> PARSE --> CHECK_FEAT
CHECK_FEAT -->|"true"| PASS
CHECK_FEAT -->|"false"| E403
LAGO -->|"Нет entitlements"| E402
style PASS fill:#c8e6c9,color:#000
style E403 fill:#ffcdd2,color:#000
style E402 fill:#ffcdd2,color:#000
6.3 Rate Limiting¶
graph TD
START["rateLimit()"]
TID{"tenantId?"}
SKIP["PASS<br/>(health, webhooks)"]
PLAN["planCode = c.get('planCode')<br/>Fallback: KV entitlement → KV subscription<br/>Default: starter_v1"]
LIMITS["RATE_LIMITS:<br/>starter_v1: 100/мин<br/>professional_v1: 500/мин<br/>enterprise_v1: unlimited<br/>payg_v1: 1000/мин"]
UNL{"enterprise?"}
PASS_UNL["PASS"]
COUNT["KV GET ratelimit:{tenantId}:{minute}<br/>→ current count"]
CHECK{"current >= limit?"}
BLOCK["❌ 429<br/>X-RateLimit-Remaining: 0"]
PASS_OK["✅ PASS<br/>KV PUT current+1 (TTL 60с)<br/>X-RateLimit-Remaining: N"]
START --> TID
TID -->|"Нет"| SKIP
TID -->|"Да"| PLAN --> LIMITS --> UNL
UNL -->|"Да"| PASS_UNL
UNL -->|"Нет"| COUNT --> CHECK
CHECK -->|"Да"| BLOCK
CHECK -->|"Нет"| PASS_OK
style BLOCK fill:#ffcdd2,color:#000
style PASS_OK fill:#c8e6c9,color:#000
style PASS_UNL fill:#c8e6c9,color:#000
style SKIP fill:#e0e0e0,color:#000
6.4 Lago Webhooks¶
sequenceDiagram
participant L as Lago
participant A as API-шлюз
participant KV as KV Store
participant Z as Zitadel
L->>A: POST /webhooks/lago<br/>X-Lago-Signature: base64(HMAC)<br/>X-Lago-Unique-Key: {key}
Note over A: 1) HMAC верификация<br/>base64(HMAC-SHA256(key, body))<br/>⚠ 401 если не совпадают
A->>KV: GET "wh:{uniqueKey}"
alt Дубль
KV-->>A: HIT
A-->>L: 200 "Already processed"
else Новый
KV-->>A: MISS
A->>KV: PUT "wh:{uniqueKey}" TTL=7 дней
end
Note over A: 3) Обработка по типу
tenantId= subscription.external_customer_idplanCode= subscription.plan_code- KV PUT
billing:{tenantId}:subscription_active→{external_id, plan_code, started_at}TTL=30 дней - KV DELETE
billing:{tenantId}:entitlement,entitlements:{tenantId} - waitUntil: POST
{issuer}/management/v1/metadata/plan_codeсx-zitadel-orgid→ синхронизация plan_code в Zitadel
- KV DELETE
billing:{tenantId}:subscription_active - KV DELETE
billing:{tenantId}:entitlement,billing:{tenantId}:plan_code,entitlements:{tenantId} - waitUntil: DELETE
{issuer}/management/v1/metadata/plan_code
- Очистка всех billing KV ключей
- Обновление
plan_codeв KV и Zitadel metadata
| Событие | KV ключ | TTL |
|---|---|---|
invoice.paid_credit_added |
billing:{tenantId}:last_paid_credit |
1 день |
wallet.depleted |
billing:{tenantId}:wallet_depleted |
1 час |
payment_request.payment_failure |
billing:{tenantId}:payment_failure |
7 дней |
invoice.payment_overdue |
billing:{tenantId}:payment_overdue |
7 дней |
6.5 Usage Tracking¶
Все usage events отправляются неблокирующе через executionCtx.waitUntil().
sequenceDiagram
participant H as Route Handler
participant A as trackUsageEvent()
participant KV as KV Store
participant L as Lago
H-)A: waitUntil(trackSearchRequest(env, secrets, tenantId, {query, results_count}))
A->>KV: GET "billing:{tenantId}:subscription_active"
alt HIT
KV-->>A: external_id
else MISS
A->>L: Lago API fallback → external_id
end
A->>L: POST /events<br/>{transaction_id: UUID, external_subscription_id,<br/>code: "search_request", timestamp: Unix epoch,<br/>properties: {tenant_id, query, results_count}}
Важно
- Lago timestamps ДОЛЖНЫ быть Unix epoch integer (
Math.floor(Date.now() / 1000)), НЕ ISO 8601 - Ошибки проглатываются — не блокируют основной ответ
6.6 Dashboard (использование)¶
sequenceDiagram
participant P as Panel
participant A as API-шлюз
participant KV as KV Store
participant L as Lago
P->>A: GET /api/v1/billing/dashboard/usage<br/>Cookie: aac_session=...
Note over A: [apiAuth] → tenantId<br/>[rateLimit] → OK<br/>⚠ НЕТ billingGate — billing доступен всегда
A->>KV: subscription, entitlements, rate limit status, billing warnings
A->>L: current_usage (фильтр по периоду)
L-->>A: usage data
A-->>P: {planCode, usage, features, rateLimit, billing, subscription}
Пример ответа
{
"planCode": "professional_v1",
"planName": "Professional",
"usage": {
"search_request": { "current": 1234, "limit": null },
"documents_stored": { "current": 5000, "limit": 100000 }
},
"features": ["synonyms", "curations", "geo_search"],
"rateLimit": { "limit": 500, "remaining": 423, "unlimited": false },
"billing": {
"paymentFailed": false,
"paymentOverdue": false,
"walletDepleted": false
},
"subscription": { "status": "active", "startedAt": "..." },
"nextBillingDate": "2026-03-01"
}
Info
Panel authProvider.canAccess() использует features из этого ответа для скрытия/показа ресурсов в сайдбаре (кеш 60 сек).
7. Data Sources (краулинг)¶
sequenceDiagram
participant P as Panel
participant A as API-шлюз
participant D as D1
participant Q as Queue
participant T as Typesense
Note over P,A: Создание Data Source
P->>A: POST /api/v1/data-sources<br/>{name, feed_url, feed_type, collection, schedule, field_mapping}
Note over A: [apiAuth] → tenantId<br/>[billingGate] → planCode<br/>[featureGate("data_sources")] → OK<br/>SSRF protection: блок localhost, 127.*, 10.*, 192.168.*
A->>D: INSERT INTO data_sources
D-->>A: id: 1
A-->>P: {id: 1, name: "Tech News", ...}
Note over P,A: Ручной запуск синхронизации
P->>A: POST /api/v1/data-sources/1/sync
A->>D: INSERT INTO crawl_runs (status: "queued")
A->>Q: CRAWL_QUEUE.send({type: "crawl", dataSourceId: 1, runId: 42, tenantId})
A-->>P: {runId: 42, status: "queued"}
Note over Q,T: Queue Consumer (async)
Note over Q: resolveSecrets(env)<br/>executeCrawl():
Q->>D: SELECT data_source
D-->>Q: feed_url, field_mapping
Note over Q: Fetch feed_url (etag/last-modified)<br/>Parse (RSS/Atom/JSON/CSV)<br/>Apply field_mapping<br/>Add tenant_id prefix
Q->>T: POST /collections/t_org123__articles/documents/import?action=upsert
T-->>Q: OK (47 docs)
Q->>D: UPDATE crawl_runs SET status="completed", docs_processed=47
Note over Q: waitUntil: trackDocumentsIndexed(47)
8. Reindex (переиндексация)¶
POST /api/v1/reindex — {collection, type}
| Тип | Описание |
|---|---|
full_rebuild |
Новая коллекция → alias swap (zero downtime) |
full_refresh |
Truncate + перекраулить все data sources |
source_reindex |
Перекраулить конкретный data source |
filter_delete |
Удалить документы по фильтру |
incremental_delta |
Только изменённые (etag/lastmod) |
graph LR
A["POST /reindex<br/>type: full_rebuild"] --> B["Создать<br/>t_{id}__products_rebuild_{ts}"]
B --> C["Enqueue reindex_chunk<br/>для каждого data source"]
C --> D["Chunk: краулинг +<br/>индексация в _rebuild"]
D --> E["Все chunks готовы?"]
E -->|"Да"| F["Alias swap"]
F --> G["Удалить старую коллекцию"]
style F fill:#c8e6c9,color:#000
Warning
Нельзя запустить 2 reindex для одной коллекции одновременно.
9. AI / RAG¶
| Эндпоинт | Feature Gate | Действие |
|---|---|---|
POST /api/v1/ai/conversations |
rag |
Typesense: POST /conversations/models, name = t_{tenantId}__conversation_{uuid}, OPENAI_API_KEY инжектируется |
GET /api/v1/ai/presets |
— | Список AI-моделей: [{name: "gpt-4o", provider: "openai"}, ...] |
POST /api/v1/ai/nl-search |
nl_search |
Typesense: POST /nl_search_models, OPENAI_API_KEY автоматически |
10. Cron (планировщик)¶
sequenceDiagram
participant CR as Cron Trigger
participant W as API Worker
participant D as D1
participant Q as Queue
CR->>W: */15 * * * *
Note over W: resolveSecrets(env)
W->>D: SELECT data_sources<br/>WHERE schedule IS NOT NULL<br/>AND next_run_at <= NOW()
D-->>W: sources[]
loop Для каждого source
W->>D: INSERT crawl_runs (status: "queued")
W->>Q: CRAWL_QUEUE.send({type: "crawl", ...})
W->>D: UPDATE data_sources SET next_run_at = ...
end
sequenceDiagram
participant CR as Cron Trigger
participant W as API Worker
participant L as Lago
participant T as Typesense
CR->>W: 0 2 * * * (ежедневно 2:00 UTC)
Note over W: resolveSecrets(env)
W->>L: GET /customers (все клиенты)
W->>T: GET /collections
T-->>W: все коллекции с кол-вом документов
Note over W: Агрегация по тенантам:<br/>t_org123__products: 5000<br/>t_org123__articles: 3000<br/>→ org123 total = 8000
loop Для каждого тенанта
W->>L: trackUsageEvent("documents_stored",<br/>{count: 8000})
end
Note over W: ⚠ Агрегация MAX —<br/>Lago хранит максимум за период
11. Panel → API маппинг¶
Как Panel (ra-core) транслирует CRUD-операции в HTTP-запросы.
authProvider¶
| ra-core метод | HTTP запрос | Действие |
|---|---|---|
login({email, password}) |
POST /api/auth/v2/login |
Создать V2 Session |
logout() |
POST /api/auth/v2/logout |
Удалить сессию + cookie |
checkAuth() |
GET /api/auth/v2/session |
Проверить cookie (кеш 30с) |
checkError({status}) |
— | 401→logout, 402/403/429→keep session |
getIdentity() |
из кеша checkAuth | {displayName, email} |
canAccess({resource}) |
GET /api/v1/billing/dashboard/usage |
features массив (кеш 60с) → RESOURCE_FEATURE_MAP[resource] |
dataProvider (Engine ресурсы)¶
| ra-core метод | HTTP запрос | Пример |
|---|---|---|
getList("collections") |
GET /api/v1/engine/collections |
Список коллекций тенанта |
getOne("collections", {id}) |
GET /api/v1/engine/collections/{id} |
Одна коллекция |
create("collections", {data}) |
POST /api/v1/engine/collections |
Создать (body.name) |
update("collections", {id, data}) |
PATCH /api/v1/engine/collections/{id} |
Обновить схему |
delete("collections", {id}) |
DELETE /api/v1/engine/collections/{id} |
Удалить |
getList("documents") |
GET /api/v1/engine/collections/{col}/documents/search?q=* |
Поиск с q=* |
create("synonym-sets", {data}) |
PUT /api/v1/engine/synonym_sets/{id} |
Upsert по имени |
dataProvider (Billing ресурсы)¶
| ra-core метод | HTTP запрос | Ответ Lago |
|---|---|---|
getList("invoices") |
GET /api/v1/billing/invoices |
{ invoices: [...] } |
getOne("invoices", {id}) |
GET /api/v1/billing/invoices/{id} |
{ invoice: {...} } |
getList("subscriptions") |
GET /api/v1/billing/subscriptions |
{ subscriptions: [...] } |
getList("wallets") |
GET /api/v1/billing/wallets |
{ wallets: [...] } |
dataProvider (Data Sources)¶
| ra-core метод | HTTP запрос |
|---|---|
getList("data-sources") |
GET /api/v1/data-sources → { data: [...], total: N } |
create("data-sources", {data}) |
POST /api/v1/data-sources |
update("data-sources", {id, data}) |
PUT /api/v1/data-sources/{id} |
delete("data-sources", {id}) |
DELETE /api/v1/data-sources/{id} |
Обработка ошибок в Panel¶
| HTTP код | Panel действие |
|---|---|
| 200 | Успех |
| 401 | authProvider.logout() → редирект /login |
| 402 | НЕ logout; toast "Требуется оплата" |
| 403 | НЕ logout; toast "Фича недоступна в вашем плане" |
| 429 | НЕ logout; toast "Превышен лимит запросов" |
| 502 | НЕ logout; toast "Сервис временно недоступен" |
| 503 | НЕ logout; сохранить stale кеш |