Redirect Service
Независимый микросервис сокращения ссылок с Open Graph превью, QR-кодами и аналитикой кликов.
Домен:
start.goloot.onlineСтек: Fastify + Prisma + QRCode
1. Summary
Goal: Обеспечить красивые короткие ссылки для маркетинговых кампаний, реферальной программы и push-уведомлений с поддержкой OG-превью для соцсетей.
User Value:
- Реферальные ссылки с красивым превью при вставке в мессенджеры и соцсети
- UTM-ссылки для инфлюенсеров формата
start.goloot.online/influencer/hash - Push-уведомления с трекингом кликов
- QR-коды для оффлайн-промо
2. Business Logic
Link Types
- Referral Links
- UTM Links
- Push Links
Формат: start.goloot.online/ref_CODE
Поведение:
- Проверяет
ReferralCodeв БД поcode(безref_префикса) - Если не найден или неактивен -- 404
- Инкрементирует
clicksCount(fire-and-forget, только для реальных пользователей) - Для social media ботов -- отдаёт HTML с OpenGraph meta-тегами (настройки из
GlobalSettings) - Для обычных мобильных браузеров (Safari, Chrome, Telegram browser) -- 302 redirect на
https://t.me/...(Universal Link / перехват Telegram browser) - Для in-app браузеров (TikTok, Instagram, Facebook, VK, Snapchat, LINE, WeChat, Twitter/X) -- bridge page с кнопкой
https://t.me/...(user gesture запускает Universal Link) - Для desktop -- показывает кастомную hypnotic-redirect страницу с QR-кодом
Destination: t.me/{BOT_USERNAME}/{APP_NAME}?startapp=ref_CODE
OG Settings: Берутся из global_settings (key: referral_opengraph), кешируются на 60 секунд
Формат: start.goloot.online/influencer/hash
Поведение:
- Конвертирует path
influencer/hashв shortCodeinfluencer_hash - Ищет в
short_links(по shortCode), затем fallback вutm_campaigns - Проверяет
isActiveиexpiresAt - Инкрементирует
UTMCampaign.linkClicksCount(fire-and-forget, только для реальных пользователей).linkClicksCount— это клики по redirect-ссылке, в отличие отclicksCount— открытия приложения - Для social media ботов -- отдаёт HTML с OG meta-тегами (из ShortLink или UTMCampaign)
- Для обычных мобильных браузеров (Safari, Chrome, Telegram browser) -- 302 redirect на
https://t.me/... - Для in-app браузеров (TikTok, Instagram, Facebook, VK, Snapchat, LINE, WeChat, Twitter/X) -- bridge page с кнопкой
https://t.me/... - Для desktop -- показывает кастомную hypnotic-redirect страницу
Destination:
- Реальные пользователи: 302 redirect на динамически построенный
t.me/{BOT_USERNAME}/{APP_NAME}?startapp={shortCode}(аналогично referral links) - Social media боты: OG HTML с
meta http-equiv="refresh"ведёт наdestinationUrlиз ShortLink (илиbotLinkиз UTMCampaign)
Формат: start.goloot.online/push_HASH или start.goloot.online/brand/push_HASH
Поведение:
- Ищет
PushRedirectпо shortCodepush_HASH - Если не найден или неактивен -- 404
- Трекает клик (инкремент
clicksCountв PushRedirect иclickedCountв PushNotification, создание PushAnalytics записи) - Выполняет прямой 302 redirect на
originalUrl
Брендированный вариант: /:brand/push_:hash -- поведение идентичное, brand параметр игнорируется при поиске (используется только для красивого URL)
Core Mechanics
1. Bot Detection
Сервис определяет social media ботов по User-Agent для показа OG-превью вместо редиректа. Поддерживаемые боты:
- Facebook (
facebookexternalhit,facebot) - Twitter/X (
twitterbot) - Telegram (
telegrambot) - Discord (
discordbot) - WhatsApp, LinkedIn, Slack, VK, Reddit, Skype, Google, Apple
2. Open Graph Preview
Для ботов генерируется HTML с meta-тегами:
og:title,og:description,og:image,og:urltwitter:card,twitter:title,twitter:description,twitter:imagetelegram:channelmeta http-equiv="refresh"для автоматического редиректа (1 сек для referral OG, 0.5 сек для UTM OG ботов, 2 сек для preview страниц)
Для реферальных ссылок OG-настройки (title, description, image) берутся из GlobalSettings с ключом referral_opengraph и кешируются in-memory на 60 секунд. Для UTM-ссылок настройки берутся из ShortLink.title/description/imageUrl или из UTMCampaign.
3. Image URL Resolution
Относительные URL картинок преобразуются в полные:
- Production:
static.goloot.online+ путь - Development:
localhost:4000+ путь - Referral preview:
/images/opengraph/referrals/{filename} - UTM preview:
/images/opengraph/utm/{filename} - Fallback: Дефолтные картинки (
default-referral.jpg,default-utm.jpg,og-image.png)
4. QR Code Generation
Эндпоинт /api/qr/:param генерирует PNG QR-код:
- Содержимое:
t.me/{BOT}/{APP}?startapp={param} - Размер: 280px, margin: 2, error correction: M
- Кеширование:
Cache-Control: public, max-age=3600
5. Mobile Device Detection
Сервис определяет тип устройства и браузера для двухуровневой стратегии редиректа:
| Тип клиента | Определение | Действие |
|---|---|---|
| Social Media Bot | isSocialMediaBot() | OG HTML с meta-тегами |
| Regular Mobile | deviceType === 'mobile' | 'tablet' + !isInAppBrowser() | 302 redirect на https://t.me/... |
| In-app Browser | deviceType === 'mobile' | 'tablet' + isInAppBrowser() | Bridge page с https://t.me/... кнопкой |
| Desktop | Иначе | Landing page + QR |
In-app browsers определяются через isInAppBrowser() (redirect-service/src/utils/user-agent.ts) по паттернам User-Agent:
- TikTok (
tiktok,bytedancewebview,musical_ly) - Instagram (
instagram) - Facebook (
fban,fbav,fb_iab) - VK (
vkandroid,vkclient) - Snapchat (
snapchat) - LINE (
line/) - WeChat (
micromessenger) - Twitter/X (
twitter)
Regular mobile (Safari, Chrome, Telegram browser): 302 redirect на https://t.me/... работает корректно — Telegram browser перехватывает навигацию на t.me на уровне приложения, iOS Safari / Android Chrome запускают Universal Link / App Link.
In-app browsers (TikTok, Instagram, Facebook и др.): перехватывают HTTP навигацию и блокируют Universal Links, поэтому 302 redirect на https://t.me/... откроет t.me как веб-страницу. Решение — bridge page, где user gesture на кнопке с https://t.me/... запускает Universal Link.
tg:// scheme (как в button href, так и в auto-JS window.location.href) показывает нативный iOS диалог «Открыть в приложении Telegram?» — используется только в крайнем fallback.
6. Bridge Page
Bridge page используется только для in-app браузеров (TikTok, Instagram, Facebook, VK, Snapchat, LINE, WeChat, Twitter/X):
- Показывает кнопку "Открыть в Telegram" с
https://t.me/...(Universal Link) - User gesture на Universal Link открывает Telegram напрямую без диалогов подтверждения
- Минималистичный дизайн — логотип, подзаголовок, одна кнопка
Обычные мобильные браузеры (Safari, Chrome, Telegram browser) получают прямой 302 redirect на https://t.me/... без bridge page.
7. Click Tracking
Реальные переходы (не от ботов) трекируются:
| Маршрут | Модель | Поле | Стратегия |
|---|---|---|---|
ref_CODE | ReferralCode | clicksCount (increment) | fire-and-forget |
influencer/hash | UTMCampaign | linkClicksCount (increment) | fire-and-forget |
push_HASH | PushRedirect + PushNotification | clicksCount + clickedCount + PushAnalytics запись | await (блокирует до завершения записи) |
linkClicksCount — клики по redirect-ссылке (инкрементируется Redirect Service). clicksCount — открытия приложения (app opens, инкрементируется Backend).
Referral и UTM: fire-and-forget — ошибка трекинга не блокирует редирект. Потеря одного клика при сбое БД — некритично.
Push: используется await — редирект выполняется только после успешной записи в БД (но ошибка трекинга всё равно перехватывается try/catch и не ломает ответ).
{ increment: 1 } — атомарная операция в PostgreSQL, корректна при конкурентных запросах.
Edge Cases
| Ситуация | Поведение | HTTP Status |
|---|---|---|
| Пустой shortCode | Bad Request | 400 |
| Пустой influencer/hash | Bad Request | 400 |
| Ссылка не найдена | Not Found | 404 |
| Реферальный код не найден/неактивен | Not Found | 404 |
| Push redirect не найден/неактивен | Not Found | 404 |
Ссылка истекла (expiresAt < now) | Gone | 410 |
Неизвестный формат /:shortCode (не ref_) | Not Found + подсказка формата UTM | 404 |
| Rate limit exceeded (>100 req/min) | Rate limit exceeded | 429 |
| Ошибка БД на health check | Database connection failed | 503 |
| Ошибка OG-настроек для referral бота | Fallback: обычный 302 redirect | 302 |
3. ADR (Architectural Decisions)
Почему отдельный микросервис?
Проблема: Redirect-сервис должен работать на отдельном домене (start.goloot.online) с минимальной задержкой. Объединение с основным backend добавило бы оверхед от middleware (auth, sessions, etc.) для каждого редиректа.
Решение: Независимый Fastify-сервер с собственным Prisma-клиентом. Общая БД с backend, но изолированный runtime.
Последствия:
- Минимальная задержка редиректа (нет auth middleware)
- Независимый деплой и масштабирование
- Дублирование Prisma Client (отдельный
pnpm prisma generate) - Общая
schema.prisma(изменения в моделях требуют синхронизации)
Почему двухуровневая мобильная стратегия?
Проблема: In-app browsers (TikTok, Instagram, Facebook и др.) перехватывают HTTP навигацию и блокируют Universal Links, поэтому 302 redirect на https://t.me/... открывает t.me как веб-страницу, а не приложение. Обычные мобильные браузеры (Safari, Chrome, Telegram browser) обрабатывают 302 redirect корректно.
Решение: Двухуровневая стратегия на основе isInAppBrowser():
- Regular mobile (Safari, Chrome, Telegram browser) — 302 redirect на
https://t.me/.... Telegram browser перехватывает навигацию на t.me и открывает Mini App, Safari/Chrome запускают Universal Link. - In-app browser (TikTok, Instagram, Facebook, VK, Snapchat, LINE, WeChat, Twitter/X) — bridge page с кнопкой
https://t.me/.... User gesture на кнопке запускает Universal Link.
Альтернативы (отклонены):
- Bridge page для всех мобильных — лишний тап для пользователей обычных браузеров, где 302 redirect работает корректно.
tg://scheme — всегда показывает confirmation dialog на iOS.- Автоматический
window.location.href = tg://...— iOS WKWebView блокирует автопереходы по custom schemes; диалог всё равно появляется в нативном Safari.
Последствия: Оптимальный UX: 0 тапов для regular mobile (прямой редирект), 1 тап для in-app browsers (bridge page). Desktop → hypnotic landing page с QR.
Почему монолитный server.ts?
Проблема: Сервис имеет ~14 роутов, но все следуют одному паттерну: найти в БД, проверить, отдать HTML/redirect.
Решение: Все роуты в одном файле server.ts (~1710 строк). Утилиты вынесены в constants/ и utils/templates.ts.
Альтернативы (отклонены):
- Domain-driven разделение -- избыточно для сервиса с одной ответственностью
- Controller/Service layers -- YAGNI, нет сложной бизнес-логики
Последствия: Простая навигация, но файл великоват. При росте функционала стоит рассмотреть разделение.
Dual-source lookup для UTM
Проблема: UTM-ссылки могут существовать как в short_links (legacy), так и в utm_campaigns (новая архитектура).
Решение: Каскадный поиск: сначала short_links, затем fallback в utm_campaigns. Из utm_campaigns создаётся mock ShortLink объект для унификации обработки.
URL формата influencer/hash конвертируется в influencer_hash через underscore для поиска в БД. Это правило жёстко зашито в логику сервиса.
4. Architecture
Services Overview
Key Components
| Компонент | Путь | Описание |
|---|---|---|
| Server | redirect-service/src/server.ts | Все роуты, утилиты, запуск сервера |
| Environment Config | redirect-service/src/config/environment.config.ts | Конфигурация ENV (fail-fast валидация) |
| Constants | redirect-service/src/constants/ | 6 файлов: network, security, template, urls, paths, index |
| Templates | redirect-service/src/utils/templates.ts | Рендеринг HTML шаблонов с переменными (generateHypnoticHTML, generateBridgeHTML) |
| Hypnotic Template | redirect-service/src/templates/hypnotic-redirect.html | Desktop страница с QR-кодом и кнопкой "Открыть напрямую" |
| Bridge Template | redirect-service/src/templates/bridge-redirect.html | Mobile bridge page с одной кнопкой https://t.me/... |
Configuration
Environment Variables
| Переменная | Обязательна | Описание |
|---|---|---|
NODE_ENV | Да | Окружение (development / production) |
PORT | Да | Порт сервера |
HOST | Да | Хост сервера |
LOG_LEVEL | Да | Уровень логирования |
DATABASE_URL | Да | PostgreSQL connection string |
STATIC_URL | Да | URL статического контента (например https://static.goloot.online) |
START_URL | Да | URL сервиса (например https://start.goloot.online) |
TELEGRAM_BOT_USERNAME | Да | Username Telegram бота |
TELEGRAM_APP_NAME | Да | Имя Telegram Mini App |
Все переменные валидируются при старте (fail-fast). Отсутствие любой -- crash.
Security & Rate Limiting
| Параметр | Значение | Описание |
|---|---|---|
| Rate Limit | 100 req / 1 min | Per-IP ограничение |
| CORS | origin: true | Все домены (необходимо для редиректов) |
| Helmet CSP | false | Отключён (для HTML-страниц с inline styles) |
| Cache (QR) | max-age=3600 | 1 час кеширования |
| Cache (Redirects) | no-cache, no-store, must-revalidate | Без кеширования |
5. Database Schema
Models
| Модель | Описание | Ключевые поля |
|---|---|---|
| ShortLink | Короткая ссылка для UTM кампании | shortCode, destinationUrl, title, description, imageUrl, isActive, expiresAt |
| UTMCampaign | Маркетинговая кампания (fallback source) | shortCode, botLink, source, medium, campaign, influencer, clicksCount (app opens), linkClicksCount (redirect clicks), conversions |
| ReferralCode | Реферальный код пользователя | code, isActive, userId, clicksCount |
| PushRedirect | Redirect для push-уведомления | shortCode, pushId, originalUrl, isActive, clicksCount |
| PushNotification | Push-уведомление (для трекинга кликов) | clickedCount, sentCount |
| PushAnalytics | Аналитика кликов по push | pushId, action, userAgent, ipAddress |
| GlobalSettings | Глобальные настройки (OG для referrals) | key, ogReferralTitle, ogReferralDescription, ogReferralImage |
Relationships
Redirect Service использует БД в read-only режиме для всех таблиц, кроме:
ReferralCode.clicksCount(инкремент при переходе по ref_ ссылке)UTMCampaign.linkClicksCount(инкремент при переходе по UTM redirect-ссылке)PushRedirect.clicksCount(инкремент при клике по push)PushNotification.clickedCount(инкремент при клике по push)PushAnalytics(создание записи при клике по push)
6. API Endpoints
- Redirect Routes
- Preview & QR
- Statistics
| Метод | Эндпоинт | Описание |
|---|---|---|
| GET | / | Service info (name, version, endpoints) |
| GET | /health | Health check (проверка подключения к БД) |
| GET | /metrics | Prometheus metrics |
| GET | /:shortCode | Основной redirect (только ref_CODE формат) |
| GET | /:influencer/:hash | Path-based UTM redirect (influencer/hash -> influencer_hash) |
| GET | /push_:hash | Push notification redirect с трекингом клика |
| GET | /:brand/push_:hash | Branded push redirect с трекингом клика |
| Метод | Эндпоинт | Описание |
|---|---|---|
| GET | /api/qr/:param | Генерация PNG QR-кода для Telegram Mini App |
| GET | /preview/:shortCode | OG-превью для ShortLink |
| GET | /preview/:influencer/:hash | OG-превью для path-based ссылки (UTM или referral r/hash) |
| Метод | Эндпоинт | Описание |
|---|---|---|
| GET | /stats/:shortCode | Статистика по shortCode (short_links или utm_campaigns) |
| GET | /stats/:influencer/:hash | Path-based статистика (UTM или referral r/hash) |
| GET | /stats/push_:hash | Статистика push redirect (клики, CTR) |
| GET | /stats/:brand/push_:hash | Статистика branded push redirect |
Подробности роутов
GET /:shortCode -- обрабатывает только ссылки с префиксом ref_. Другие форматы возвращают 404 с подсказкой использовать influencer/hash.
GET /:influencer/:hash -- основной роут для UTM-ссылок. Конвертирует path в underscore shortCode. Двойной lookup: short_links -> utm_campaigns.
GET /preview/:influencer/:hash -- специальная обработка для r/hash (referral preview). Если influencer === 'r', то hash трактуется как referral code.
GET /stats/:influencer/:hash -- аналогично preview, если influencer === 'r', возвращает статистику реферального кода.
Push stats -- включают CTR (clickRate) = clickedCount / sentCount * 100.
7. Related
- UTM Tracking -- маркетинговые кампании и UTM-параметры
- Referrals -- реферальная система и коды
- Push Notifications -- push-уведомления и PushRedirect
- Static Nginx -- раздача статических файлов (OG-картинки)