# πŸ”€ Hybrid Architecture: Vercel Edge + Quoter ## πŸ“‹ АрхитСктурноС Ρ€Π΅ΡˆΠ΅Π½ΠΈΠ΅ Π’Π°ΡˆΠ° спСцификация описываСт **ΠΈΠ΄Π΅Π°Π»ΡŒΠ½ΡƒΡŽ Π³ΠΈΠ±Ρ€ΠΈΠ΄Π½ΡƒΡŽ Π°Ρ€Ρ…ΠΈΡ‚Π΅ΠΊΡ‚ΡƒΡ€Ρƒ**: ``` πŸ“€ Upload: Quoter (ΠΊΠΎΠ½Ρ‚Ρ€ΠΎΠ»ΡŒ + ΠΊΠ²ΠΎΡ‚Ρ‹) πŸ“₯ Download: Vercel Edge API (ΠΏΡ€ΠΎΠΈΠ·Π²ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΡŒ) 🎨 OG: @vercel/og (динамичСская гСнСрация) ``` ## βœ… ΠŸΡ€Π΅ΠΈΠΌΡƒΡ‰Π΅ΡΡ‚Π²Π° Π³ΠΈΠ±Ρ€ΠΈΠ΄Π½ΠΎΠ³ΠΎ ΠΏΠΎΠ΄Ρ…ΠΎΠ΄Π° ### 🎯 **Π›ΡƒΡ‡ΡˆΠ΅Π΅ ΠΈΠ· Π΄Π²ΡƒΡ… ΠΌΠΈΡ€ΠΎΠ²** | ΠšΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ | БСрвис | ΠŸΠΎΡ‡Π΅ΠΌΡƒ ΠΈΠΌΠ΅Π½Π½ΠΎ ΠΎΠ½ | |-----------|---------|------------------| | **Upload** | Quoter | ΠšΠΎΠ½Ρ‚Ρ€ΠΎΠ»ΡŒ ΠΊΠ²ΠΎΡ‚, кастомная Π»ΠΎΠ³ΠΈΠΊΠ°, Π±Π΅Π·ΠΎΠΏΠ°ΡΠ½ΠΎΡΡ‚ΡŒ | | **Download** | Vercel | АвтоматичСский WebP/AVIF, Π³Π»ΠΎΠ±Π°Π»ΡŒΠ½Ρ‹ΠΉ edge | | **Resize** | Vercel | ЛСнивая гСнСрация, auto-optimization | | **OG** | Vercel | ДинамичСская гСнСрация, ΠΊΡΡˆΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ | ### πŸ’° **ЭкономичСская ΡΡ„Ρ„Π΅ΠΊΡ‚ΠΈΠ²Π½ΠΎΡΡ‚ΡŒ** - **Upload costs**: Волько VPS + Storj (ΠΊΠΎΠ½Ρ‚Ρ€ΠΎΠ»ΠΈΡ€ΡƒΠ΅ΠΌΡ‹Π΅) - **Download costs**: Vercel edge (pay-per-use, Π½ΠΎ дСшСвлС CDN) - **Storage costs**: Storj (~$4/TB ΠΏΡ€ΠΎΡ‚ΠΈΠ² $20+/TB Ρƒ Vercel) ### πŸš€ **ΠŸΡ€ΠΎΠΈΠ·Π²ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΡŒ** - **Upload**: Direct to S3, Π±Π΅Π· proxy overhead - **Download**: Vercel Edge (~50ms globally) - **Caching**: Π”Π²ΡƒΡ…ΡƒΡ€ΠΎΠ²Π½Π΅Π²ΠΎΠ΅ (Vercel + S3) ## πŸ”§ Π˜Π½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΡ с Ρ‚Π΅ΠΊΡƒΡ‰ΠΈΠΌ Quoter ### 1. **ОбновлСниС CORS для Vercel** ```rust // src/main.rs - Π΄ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ Vercel Π² allowed origins let cors = Cors::default() .allowed_origin("https://discours.io") .allowed_origin("https://new.discours.io") .allowed_origin("https://vercel.app") // для Vercel edge functions .allowed_origin("http://localhost:3000") // для Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ .allowed_methods(vec!["GET", "POST", "OPTIONS"]) // ... ``` ### 2. **Π”ΠΎΠ±Π°Π²Π»Π΅Π½ΠΈΠ΅ Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΊΠΎΠ² для Vercel Image API** ```rust // src/handlers/common.rs - Π΄ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΊΠΈ для Vercel pub fn create_vercel_compatible_response(content_type: &str, data: Vec, etag: &str) -> HttpResponse { HttpResponse::Ok() .content_type(content_type) .insert_header(("etag", etag)) .insert_header(("cache-control", CACHE_CONTROL_IMMUTABLE)) .insert_header(("access-control-allow-origin", "*")) .insert_header(("x-vercel-cache", "HIT")) // для ΠΎΠΏΡ‚ΠΈΠΌΠΈΠ·Π°Ρ†ΠΈΠΈ Vercel .body(data) } ``` ### 3. **Endpoint для ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΈ доступности** ```rust // src/handlers/universal.rs - Π΄ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ health check для Vercel async fn handle_get( req: HttpRequest, state: web::Data, path: &str, ) -> Result { match path { "/" => crate::handlers::user::get_current_user_handler(req, state).await, "/health" => Ok(HttpResponse::Ok().json(serde_json::json!({ "status": "ok", "service": "quoter", "version": env!("CARGO_PKG_VERSION") }))), _ => { // GET /{path} - ΠΏΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ Ρ„Π°ΠΉΠ»Π° Ρ‡Π΅Ρ€Π΅Π· proxy let path_without_slash = path.trim_start_matches('/'); let requested_res = web::Path::from(path_without_slash.to_string()); crate::handlers::proxy::proxy_handler(req, requested_res, state).await } } } ``` ## πŸ”„ ΠœΠΈΠ³Ρ€Π°Ρ†ΠΈΠΎΠ½Π½Π°Ρ стратСгия ### Π­Ρ‚Π°ΠΏ 1: ΠŸΠΎΠ΄Π³ΠΎΡ‚ΠΎΠ²ΠΊΠ° Quoter (Ρ‚Π΅ΠΊΡƒΡ‰ΠΈΠΉ) - βœ… **Π“ΠΎΡ‚ΠΎΠ²ΠΎ**: Upload API с ΠΊΠ²ΠΎΡ‚Π°ΠΌΠΈ ΠΈ Π±Π΅Π·ΠΎΠΏΠ°ΡΠ½ΠΎΡΡ‚ΡŒΡŽ - βœ… **Π“ΠΎΡ‚ΠΎΠ²ΠΎ**: БистСма ΠΌΠΈΠ½ΠΈΠ°Ρ‚ΡŽΡ€ ΠΈ рСсайзинга - βœ… **Π“ΠΎΡ‚ΠΎΠ²ΠΎ**: Multi-cloud storage (Storj + AWS) - πŸ”„ **Π”ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ**: CORS для Vercel edge functions - πŸ”„ **Π”ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ**: Health check endpoint ### Π­Ρ‚Π°ΠΏ 2: Настройка Vercel Edge ```javascript // vercel.json - конфигурация для ΠΎΠΏΡ‚ΠΈΠΌΠΈΠ·Π°Ρ†ΠΈΠΈ { "images": { "deviceSizes": [64, 128, 256, 320, 400, 640, 800, 1200, 1600], "imageSizes": [10, 40, 110], "remotePatterns": [ { "protocol": "https", "hostname": "files.dscrs.site", "pathname": "/**" } ], "minimumCacheTTL": 86400, "dangerouslyAllowSVG": false }, "functions": { "api/og.js": { "maxDuration": 30 } } } ``` ### Π­Ρ‚Π°ΠΏ 3: ΠšΠ»ΠΈΠ΅Π½Ρ‚ΡΠΊΠ°Ρ интСграция ```typescript // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° доступности ΠΈ fallback export const getImageService = async (): Promise<'vercel' | 'quoter'> => { // Vercel ΠΏΠΎ ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ для Π±ΠΎΠ»ΡŒΡˆΠΈΠ½ΡΡ‚Π²Π° случаСв if (typeof window === 'undefined') return 'vercel'; // SSR try { // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ Π΄ΠΎΡΡ‚ΡƒΠΏΠ½ΠΎΡΡ‚ΡŒ Vercel Image API const response = await fetch('/_next/image?url=' + encodeURIComponent('https://files.dscrs.site/test.jpg') + '&w=1&q=1'); return response.ok ? 'vercel' : 'quoter'; } catch { return 'quoter'; // fallback } }; ``` ## πŸ“Š ΠœΠΎΠ½ΠΈΡ‚ΠΎΡ€ΠΈΠ½Π³ Π³ΠΈΠ±Ρ€ΠΈΠ΄Π½ΠΎΠΉ систСмы ### ΠœΠ΅Ρ‚Ρ€ΠΈΠΊΠΈ Quoter (Upload) ```log # Upload ΡƒΡΠΏΠ΅ΡˆΠ½ΠΎΡΡ‚ΡŒ INFO Upload successful: user_123 uploaded photo.jpg (2.5MB) INFO Quota updated: user_123 now using 45% (5.4GB/12GB) # Rate limiting WARN Rate limit applied: IP 192.168.1.100 blocked for upload (10/5min exceeded) ``` ### ΠœΠ΅Ρ‚Ρ€ΠΈΠΊΠΈ Vercel (Download) ```javascript // api/metrics.js - собираСм ΠΌΠ΅Ρ‚Ρ€ΠΈΠΊΠΈ download export default async function handler(req) { const { searchParams } = new URL(req.url); const source = searchParams.get('source'); // 'vercel' | 'quoter' const filename = searchParams.get('filename'); // Π›ΠΎΠ³ΠΈΡ€ΡƒΠ΅ΠΌ использованиС console.log(`Image served: ${filename} via ${source}`); return new Response('OK'); } ``` ## 🎯 Production Π³ΠΎΡ‚ΠΎΠ²Π½ΠΎΡΡ‚ΡŒ ### Load Testing ```bash # Test upload Ρ‡Π΅Ρ€Π΅Π· Quoter ab -n 100 -c 10 -T 'multipart/form-data; boundary=----WebKitFormBoundary' \ -H "Authorization: Bearer $TOKEN" \ https://files.dscrs.site/ # Test download Ρ‡Π΅Ρ€Π΅Π· Vercel ab -n 1000 -c 50 \ 'https://discours.io/_next/image?url=https%3A//files.dscrs.site/test.jpg&w=600&q=75' ``` ### Failover Strategy ```typescript export const getImageWithFailover = async (filename: string, width: number) => { const strategies = [ () => getVercelImageUrl(`https://files.dscrs.site/${filename}`, width), () => getQuoterWebpUrl(filename, width), () => `https://files.dscrs.site/${filename}` // fallback to original ]; for (const strategy of strategies) { try { const url = strategy(); const response = await fetch(url, { method: 'HEAD' }); if (response.ok) return url; } catch (error) { console.warn('Image strategy failed:', error); } } throw new Error('All image strategies failed'); }; ``` ## πŸ’‘ Π Π΅ΠΊΠΎΠΌΠ΅Π½Π΄Π°Ρ†ΠΈΠΈ ΠΏΠΎ ΠΎΠΏΡ‚ΠΈΠΌΠΈΠ·Π°Ρ†ΠΈΠΈ ### 1. **ΠšΡΡˆΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅** - Vercel Edge: автоматичСскоС ΠΊΡΡˆΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ - Quoter: ETag + immutable headers - CDN: Π΄ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹ΠΉ слой ΠΊΡΡˆΠΈΡ€ΠΎΠ²Π°Π½ΠΈΡ ### 2. **ΠœΠΎΠ½ΠΈΡ‚ΠΎΡ€ΠΈΠ½Π³** - Sentry для error tracking - Vercel Analytics для performance - Custom metrics для quota usage ### 3. **Costs optimization** ```typescript // Π£ΠΌΠ½ΠΎΠ΅ ΠΏΠ΅Ρ€Π΅ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅ ΠΌΠ΅ΠΆΠ΄Ρƒ сСрвисами export const getCostOptimalImageUrl = (filename: string, width: number, useCase: ImageUseCase) => { // Для часто ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌΡ‹Ρ… Ρ€Π°Π·ΠΌΠ΅Ρ€ΠΎΠ² - Vercel (Π»ΡƒΡ‡ΡˆΠ΅ кэш) if ([300, 600, 800].includes(width)) { return getVercelImageUrl(`https://files.dscrs.site/${filename}`, width); } // Для Ρ€Π΅Π΄ΠΊΠΈΡ… Ρ€Π°Π·ΠΌΠ΅Ρ€ΠΎΠ² - Quoter (ΠΈΠ·Π±Π΅Π³Π°Π΅ΠΌ Vercel billing) return getQuoterWebpUrl(filename, width); }; ``` ## βœ… Π’Ρ‹Π²ΠΎΠ΄Ρ‹ Π’Π°ΡˆΠ° Π°Ρ€Ρ…ΠΈΡ‚Π΅ΠΊΡ‚ΡƒΡ€Π° идСальна ΠΏΠΎΡ‚ΠΎΠΌΡƒ Ρ‡Ρ‚ΠΎ: 1. **Upload остаСтся Π² Quoter** - ΠΏΠΎΠ»Π½Ρ‹ΠΉ ΠΊΠΎΠ½Ρ‚Ρ€ΠΎΠ»ΡŒ бСзопасности ΠΈ ΠΊΠ²ΠΎΡ‚ 2. **Download Ρ‡Π΅Ρ€Π΅Π· Vercel** - глобальная ΠΏΡ€ΠΎΠΈΠ·Π²ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΡŒ ΠΈ auto-optimization 3. **OG Ρ‡Π΅Ρ€Π΅Π· @vercel/og** - динамичСская гСнСрация Π±Π΅Π· слоТности 4. **ΠŸΠΎΡΡ‚Π΅ΠΏΠ΅Π½Π½Π°Ρ миграция** - ΠΌΠΎΠΆΠ½ΠΎ Π²Π½Π΅Π΄Ρ€ΡΡ‚ΡŒ поэтапно 5. **Fallback стратСгия** - Π½Π°Π΄Π΅ΠΆΠ½ΠΎΡΡ‚ΡŒ Ρ‡Π΅Ρ€Π΅Π· redundancy πŸ’‹ **Π£ΠΏΡ€ΠΎΡ‰Π΅Π½ΠΈΠ΅ достигнуто**: ΡƒΠ±ΠΈΡ€Π°Π΅ΠΌ ΡΠ»ΠΎΠΆΠ½ΠΎΡΡ‚ΡŒ рСсайзинга ΠΈΠ· Quoter, оставляСм Ρ‚ΠΎΠ»ΡŒΠΊΠΎ upload + storage, всю ΠΎΠΏΡ‚ΠΈΠΌΠΈΠ·Π°Ρ†ΠΈΡŽ ΠΎΡ‚Π΄Π°Π΅ΠΌ Vercel Edge. Π‘Ρ‚ΠΎΠΈΡ‚ Π»ΠΈ Π΄ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ эти измСнСния Π² ΠΊΠΎΠ΄ Quoter для ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΊΠΈ Vercel ΠΈΠ½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΠΈ?