Что вы найдёте в этой статье
- модель «комната + участники + свайпы»;
- realtime-синхронизация двух устройств через Supabase;
- логика матчинга (когда выбор «совпал»);
- упаковка веб-кода в Android-приложение через Capacitor;
- что бы изменили, если бы стартовали заново.
Это техническая статья — продолжение рассказа о том, как мы запустили WhatToDo в Google Play.
Стек одной строкой
Vue 3 + TypeScript + Pinia + Vite на фронте. Supabase (Postgres, realtime, auth) на бэкенде. Firebase Analytics + Crashlytics. Capacitor для упаковки в Android.
Модель данных
Базовая схема в Postgres минимальна:
- rooms — комната пары на сессию.
- room_members — кто в комнате (1–2 пользователя).
- ideas — статичная (на старте) колода идей.
- swipes — отдельная запись:
room_id,user_id,idea_id,direction (like/skip). - matches — совпадение:
room_id,idea_id,created_at.
matches мы намеренно делаем отдельной таблицей, а не флагом на swipes. Это позволяет:
- мгновенно подтянуть «уже совпавшие» при возврате в комнату;
- подписаться realtime-каналом только на матчи, без шума всех свайпов.
Realtime поверх Postgres
Supabase даёт postgres_changes-канал. Псевдо-код подписки клиента на матчи комнаты:
const channel = supabase
.channel(`room:${roomId}`)
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'matches',
filter: `room_id=eq.${roomId}`,
},
payload => onNewMatch(payload.new),
)
.subscribe();
Когда второй участник свайпает идею, которую уже лайкнул первый, серверная функция вставляет запись в matches. Обе клиентских стороны получают событие и показывают экран «совпало».
Логика матчинга на сервере
Решение matchera — в Postgres-функции, чтобы не зависеть от живых клиентов. Триггер на INSERT INTO swipes делает следующее:
- ищет «парный» свайп второго участника той же комнаты по той же идее;
- если найдено и оба
direction = 'like'— вставляетmatches; - если уже было — игнорирует (уникальный индекс
(room_id, idea_id)).
Это даёт два важных свойства:
- симметричность: матч появится одинаково, кто бы ни свайпнул вторым;
- идемпотентность: повторный свайп не создаёт дубль.
Что хранится в Pinia
В сторе клиента — три области:
room— текущая комната, статус подключения, второй участник;deck— локальная колода (заранее загружена, чтобы свайпы не ждали сети);matches— массив матчей с timestamp, на основе realtime-канала.
Локальная колода — ключевой UX. Свайп должен быть мгновенным даже при плохой сети. Запросы к серверу идут в фоне.
Авторизация
На старте поддерживаем два сценария:
- Google Sign-In — самый быстрый вход. Для большинства Android-пользователей хватает одного тапа.
- Email + подтверждение — для пар, где у партнёра нет аккаунта Google или он не хочет привязываться.
Реализация — стандартная Supabase Auth. Никаких своих JWT и кастомных сессий — это нам сэкономило недели работы и аудита.
Telemetry, crash и аналитика
- Firebase Analytics — события «комната создана», «комната присоединена», «свайпов за сессию», «матч».
- Firebase Crashlytics — обязательный минимум на этапе closed test. Play Console показывает crash rate, но Crashlytics даёт стек.
- Supabase logs — серверные ошибки и slow queries.
Метрики обмазаны namespace wtd_ чтобы потом не смешивать с возможной общей аналитикой 3vstyle.ru.
Упаковка под Android: Capacitor
Сборка простая:
npm run build # Vite собирает dist/
npx cap sync android # синхронизирует dist в android/
cd android
./gradlew bundleRelease
На выходе — .aab, который мы заливаем в Play Console.
Capacitor запускает WebView с локально упакованной web-сборкой. Это значит:
- нет CORS-проблем — web-bundle грузится с
file://-like схемы; - любые внешние запросы (Supabase, Firebase) идут как обычный fetch;
- platform-specific фичи (push, share) — через Capacitor-плагины.
Что бы сделали иначе
- TypeScript-first с первого дня. Мы стартовали смешанно (часть Vue в JS), потом дотягивали. Стоило сразу.
- Supabase Row Level Security сразу. Мы поздно подключили RLS. Это болезненный рефакторинг; на новом проекте RLS — первое, что включаем.
- Меньше плагинов Capacitor. Каждый плагин = риск, что после обновления Android-таргета что-то отвалится. Чем меньше внешних — тем спокойнее жизнь.
- Sentry рядом с Crashlytics. Crashlytics хорош для нативных крэшей, но web-исключения в WebView Sentry ловит точнее.
Если хотите такое же
Подобная архитектура (web + Supabase + Firebase + упаковка) подходит для много какого «двух-пользовательского» продукта: парные приложения, командные мини-игры, лёгкие SaaS. Это то, что мы делаем под клиентов как студия.
Самому посмотреть приложение в работе — страница WhatToDo. Хроника процесса — Telegram‑канал.