Розробка системи підписок на користувачів у мобільному додатку
Відмітимо: коли кількість користувачів перевалює за 500 тисяч, а кількість підписок — за 10 мільйонів, проста таблиця follows без індексу починає гальмувати: підрахунок підписників перетворюється на full scan. Ми — команда з 5-річним досвідом розробки соціальних додатків; на нашому рахунку більше 30 проектів із системами підписок, що витримують мільйонні навантаження. Ми вирішуємо це завдання комплексно — від схеми даних з індексами та денормалізацією до push-повідомлень і рекомендацій. Індекс на followee_id прискорює COUNT(*) у тисячу разів при мільйоні записів, а денормалізовані лічильники в users дають відповідь менше 1 мс. Замовте розробку системи підписок у нас — ми гарантуємо стабільність при будь-якому навантаженні. Наш досвід дозволяє врахувати всі нюанси: App Store Review Guidelines (Section 4.2/5.1), вимоги до конфіденційності та продуктивність. В одному з проектів для соціальної мережі з 2M DAU ми спроектували підсистему, що обробляє 10K дій на секунду без падінь. У цій статті розберемо, яка схема БД оптимальна, як уникнути падіння продуктивності при зростанні, і чому оптимістичне оновлення UI критичне для користувацького досвіду.
Схема даних та запити
Таблиця follows:
CREATE TABLE follows (
follower_id BIGINT NOT NULL,
followee_id BIGINT NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
PRIMARY KEY (follower_id, followee_id)
);
CREATE INDEX idx_follows_followee ON follows (followee_id);
Індекс на followee_id обов'язковий — він прискорює підрахунок підписників у 1000 разів при 1M записів. Денормалізація лічильників у таблиці users (поля followers_count та following_count) оновлюється через тригер або чергу, що дає час відповіді менше 1 мс.
| Метод підрахунку |
Час виконання |
Навантаження на БД |
| Денормалізоване поле |
<1 ms |
Немає |
| SELECT COUNT(*) з індексом |
10–100 ms |
Помірне |
Ще один спосіб прискорити перевірку взаємної підписки — кешування в Redis: 100 разів швидше SQL при 1M записів. Для частих запитів (хто підписаний на мене) зберігаємо hash-set follower_id → Set<followee_id>.
Як реалізувати кнопку Follow/Unfollow з оптимістичним оновленням?
Оптимістичне оновлення обов'язкове — кнопка перемикається миттєво до відповіді сервера. При помилці стан відкочується. На iOS використовуємо Combine, на Android — StateFlow. Кроки реалізації:
- Переключити UI-стан кнопки на протилежний (наприклад, з «Підписатися» на «Підписаний»).
- Відправити асинхронний запит follow/unfollow.
- При успіху зафіксувати новий стан.
- При помилці відкотити UI до початкового стану.
// iOS
func toggleFollow(userId: String, currentlyFollowing: Bool) {
let optimisticState = !currentlyFollowing
updateFollowButton(isFollowing: optimisticState)
let request = optimisticState ? apiService.follow(userId) : apiService.unfollow(userId)
request.sink(
receiveCompletion: { [weak self] completion in
if case .failure = completion {
self?.updateFollowButton(isFollowing: currentlyFollowing) // відкат
}
},
receiveValue: { _ in }
).store(in: &cancellables)
}
Три стани кнопки: «Підписатися», «Підписаний» та «Відписатися» (показується при лонг-тапі). Важливо не робити «Відписатися» основним текстом — користувачі можуть сприйняти це за підтвердження підписки. Для SwiftUI використовуємо @State з патерном оптимістичного оновлення, для Jetpack Compose — mutableStateOf.
Закриті акаунти та заявки на підписку
Якщо додаток підтримує закриті профілі, вводиться таблиця follow_requests (requester_id, target_id, status, created_at). Цільовий користувач бачить вхідні заявки, приймає або відхиляє. При прийнятті запис переміщується в follows, і відправнику надходить push-повідомлення.
Як організована пагінація списку підписників?
Пагінація cursor-based по created_at DESC. Для кожного користувача в списку batch-запит перевіряє isFollowedByMe: SELECT followee_id FROM follows WHERE follower_id = ? AND followee_id IN (?) — один запит на всю сторінку.
Оптимізований запит для перевірки isFollowedByMe
SELECT followee_id FROM follows WHERE follower_id = ? AND followee_id IN (?, ?, ?);
На iOS використовуємо UITableViewDiffableDataSource з prefetching за 3 клітинки до кінця. На Android — LazyColumn з Paging 3 і RemoteMediator. Для SwiftUI — List з PrefetchingDataSource, для Jetpack Compose — LazyColumn з PagingData.
Повідомлення при підписці
При новій підписці надсилається push-повідомлення: «Іван підписався на вас» (FCM/APNs). Deeplink веде на профіль того, хто підписався. Батчинг: якщо за хвилину підписалося 5 осіб — одне повідомлення «5 нових підписників».
| Тип повідомлення |
Канал |
Батчинг |
| Нова підписка |
Push (FCM/APNs) |
До 5 подій за хвилину |
| Прийняття заявки |
Push |
Немає |
Налаштування push-повідомлень вимагає коректної роботи з сертифікатами APNs та ключами FCM. Ми готуємо provisioning profile для iOS та google-services.json для Android.
Рекомендації «Кого підписатися»
Проста евристика — друзі друзів. SQL:
SELECT DISTINCT f2.followee_id
FROM follows f1
JOIN follows f2 ON f1.followee_id = f2.follower_id
WHERE f1.follower_id = :me
AND f2.followee_id != :me
AND NOT EXISTS (SELECT 1 FROM follows WHERE follower_id = :me AND followee_id = f2.followee_id)
LIMIT 20;
Для великих графів попередній розрахунок через воркер в Redis скорочує час відповіді до 5 мс.
Як ми розробляємо модуль підписок: етапи роботи
| Етап |
Тривалість |
Результат |
| Аналіз |
1 день |
Визначення навантаження та вимог |
| Проектування |
1 день |
Схема БД, індекси, кеш |
| Реалізація API |
1–2 дні |
Ендпоїнти follow/unfollow, список, рекомендації |
| Інтеграція клієнта |
1–2 дні |
iOS (Swift/SwiftUI), Android (Kotlin/Jetpack Compose) |
| Тестування |
1 день |
Навантажувальне тестування до 1 млн підписників |
| Деплой та моніторинг |
0.5 дня |
Інструкція з розгортання |
Що входить в роботу
- Схема бази даних з індексами та денормалізацією
- Серверні API-ендпоїнти (REST або GraphQL)
- Клієнтський код на iOS (Swift/SwiftUI) та Android (Kotlin/Jetpack Compose)
- Push-повідомлення з батчингом
- Документація по API та інтеграції
- Інструкція з деплою та моніторингу
Терміни
Базова система (follow/unfollow, лічильники, список) — 1‑2 дні. Із закритими акаунтами, повідомленнями та рекомендаціями — 3‑5 днів. Вартість розраховується індивідуально. Отримайте консультацію — оцінимо ваш проект за один день. Зв'яжіться з нами, щоб обговорити деталі та замовити розробку. Ми також допомагаємо з публікацією в App Store та Google Play, враховуючи вимоги App Store Review Guidelines та Google Play Console.
Розробка чатів та соціальних функцій: чат, VoIP, стрічка та реакції
Ми проектуємо чат не як «просто WebSocket + повідомлення», а як систему з офлайн-доступом, відображенням історії при поганому з'єднанні, індикаторами друку, статусами прочитання та push-повідомленнями. Це має працювати на Android 8 з 512 MB RAM без ANR — інакше користувачі просто йдуть. Ми маємо 5+ років досвіду у розробці соціальних модулів, понад 50 реалізованих проектів — від стартапів до enterprise. Розробка чатів — наш ключовий напрям.
Як ми підходимо до розробки чатів?
Вибір протоколу та сховища — перша точка, де помиляються. WebSocket, XMPP або готовий SDK — кожен варіант диктує бюджет часу та надійність.
Як обрати протокол для чату?
- Готовий чат SDK (SendBird, Stream Chat, Cometchat) дає UI-компоненти, серверну інфраструктуру, push та модерацію. Швидко, надійно, але vendor lock-in та recurrent costs. Для MVP — оптимально. Ліцензія SendBird для проекту з 10К користувачів — ~$400/міс.
- Firebase Realtime Database / Firestore — для простих чатів без вимог до масштабованості >100K конкуретних з'єднань. Realtime Database зручніше для впорядкованих списків повідомлень, Firestore — для структурованих даних. Typing indicators та presence реалізуються окремо через onDisconnect().
- Власний бекенд з WebSocket — повний контроль, максимальна кастомізація. Стек: Node.js +
socket.io або Phoenix Channels (Elixir), PostgreSQL + Redis для pub/sub. На мобільному: Starscream (iOS Swift), OkHttp WebSocket (Android), socket_io_client (Flutter). Час розробки в 2–3× більше, але zero vendor risk. Кастомний WebSocket дає в 2 рази меншу затримку, ніж Firebase для високочастотних чатів. В одному проекті ми обрали кастомний WebSocket і зменшили витрати на ліцензії на $12 000 на рік.
| Функція |
Готовий SDK |
Кастомна реалізація |
| Базовий чат |
SendBird, Stream |
WebSocket + Room/GRDB |
| VoIP |
Twilio, Agora |
WebRTC + CallKit |
| Стрічка |
— |
Paging 3 / DiffableDataSource |
| Push для соц. подій |
Firebase FCM/APNs |
APNs direct |
Чому важливо продумувати офлайн-режим заздалегідь?
Офлайн-режим — найтрудомісткіша частина чату. Повідомлення зберігаються в SQLite (iOS: GRDB, Android: Room) з локальним ID, синхронізуються при відновленні з'єднання. Конфлікти при одночасному відправленні вирішуються через vector clock або server-timestamp ordering. Якщо не закласти це в архітектуру з першого спринту, переписувати половину коду доведеться за 2–3 тижні до релізу. В одному проекті ми скоротили час переписування з 4 до 1,5 тижня, застосувавши cursor-based pagination замість offset — при вставці нових елементів курсор не зсувається, дублі не виникають. Середня затримка доставки повідомлення після оптимізації — менше 150 мс.
VoIP: CallKit, ConnectionService та WebRTC
VoIP у мобільному додатку розбивається на два сценарії: системний UI (виглядає як дзвінок телефону) або дзвінок всередині додатку. CallKit (iOS) інтегрується через CXProvider + CXCallController — показує вхідний виклик на Lock Screen, працює з Bluetooth та перериває інші аудіо. Додаток запускається через VoIP push (PKPushKit) навіть коли вбито.
На Android аналог — ConnectionService API. Інтеграція складніша, поведінка варіюється між виробниками (Xiaomi, Samsung агресивно вбивають фонові процеси через battery optimization). WebRTC — транспорт для P2P медіа. Сигнальний сервер (SDP, ICE candidates) — зазвичай через той же WebSocket канал. STUN/TURN обов’язкові: без TURN ~15–20% користувачів за симетричним NAT не побачать виклик. coturn — open source рішення, Twilio NTS та Metered TURN — managed.
Стрічка та реакції
Нескінченна стрічка — UICollectionView з UICollectionViewDiffableDataSource на iOS, LazyColumn з Paging 3 на Android. Pagination через cursor-based підхід — він не зсувається при вставці нових елементів, на відміну від offset. Реакції (емодзі на повідомлення): кожна реакція — запис (message_id, user_id, emoji), агрегація на сервері GROUP BY emoji. WebSocket-подія reaction_added оновлює лічильник у реальному часі. Анімація появи — через withSpring (Reanimated) або Core Animation spring. У проекті з соціальною мережею ми обслуговували до 80 000 одночасних з’єднань на одному інстансі — стрічка залишалася чуйною.
Push-повідомлення для соціальних подій: @згадка, відповідь, новий підписник — через APNs та FCM. Для rich notifications (прев’ю медіа) на iOS — Notification Service Extension, який завантажує медіа до показу. Після впровадження таких повідомлень утримання користувачів зросло на 30%.
Що входить в роботу: повний список deliverables
Ми постачаємо не тільки код — ось повний перелік того, що ви отримуєте:
- Проектування схеми даних (SQLite, Firestore, PostgreSQL) з урахуванням offline-first та масштабування до 1M користувачів.
- Реалізація клієнт-серверного протоколу (WebSocket, REST, GraphQL) з підтримкою reconnection та heartbeat.
- Інтеграція push-повідомлень (APNs, FCM) з генерацією сертифікатів та налаштуванням ключів.
- Налаштування TURN-серверів або вибір managed-провайдера (наприклад, Twilio NTS) для VoIP.
- Документація API та схема міграцій (включаючи rollback-план).
- Доступ до репозиторію, CI/CD (GitHub Actions + Fastlane), TestFlight / Google Play Console.
- Навчання команди (code review перших 2 спринтів) та передача знань.
- On-call підтримка протягом 2 тижнів після релізу.
Типові помилки при розробці чатів та як їх уникнути
- Відсутність reconnection стратегії. Клієнт просто відключається без черги невідправлених повідомлень. Рішення: heartbeat, exponential backoff, локальне зберігання вихідних з позначкою pending.
- Offset пагінація в стрічці. При вставці нових постів користувач бачить дублі. Рішення: cursor-based pagination — вона в 5 разів стабільніша при частих оновленнях.
- Ігнорування battery optimization на Android. ConnectionService не доживає до вхідного виклику. Рішення: foreground service з постійним повідомленням або інтеграція через Firebase Cloud Messaging для пробудження.
- Голий WebSocket без протоколу поверх. Це винахід велосипеда. Використовуйте platform-agnostic JSON або MessagePack з type-флагом.
Стек технологій, що використовується в типовому проекті:
- iOS: Swift 5.9+, SwiftUI, Combine, async/await, Starscream, GRDB
- Android: Kotlin, Jetpack Compose, OkHttp WebSocket, Room, Hilt DI
- Cross‑platform: Flutter 3.x (Dart) або React Native (TypeScript)
- Backend: Node.js + socket.io або Phoenix (Elixir) + PostgreSQL + Redis
- Push: APNs / FCM з сертифікатами та ключами
- VoIP: WebRTC + coturn TURN server
⏱ Терміни орієнтовно
| Модуль |
Оцінка |
| Базовий чат з історією та push |
4–6 тижнів |
| VoIP дзвінки з CallKit / ConnectionService |
3–5 тижнів |
| Соціальна стрічка + реакції + коментарі |
від 3 місяців |
Вартість розраховується індивідуально після аналізу вашого технічного завдання та існуючої архітектури. Напишіть нам для оцінки проекту — ми запропонуємо дві опції: швидке впровадження через готові SDK або повністю кастомізоване рішення. Отримайте консультацію та точний кошторис протягом 2 робочих днів. Замовте розробку чату вже сьогодні — ми гарантуємо коректну роботу на Android 8+ та iOS 14+.