Контекст
Совместный вклад — это таблица в WhatsApp
Семья или компания друзей собирает деньги на общий депозит. Кто-то владелец счёта, остальные — соучастники. Все хотят видеть сумму, ставку, срок и сколько уже накапало. Сейчас это решается скриншотами из интернет-банка и строчкой в общем чате. Ни одного продукта, который делает именно это — просто, без лишнего.
Проблема
Три боли одного вклада
01
Видимость
Участники не видят реальных данных по вкладу — только то, что скинул владелец. Актуальность под вопросом.
02
Доходность
Никто не считает вручную проценты. «Сколько уже накапало?» — вопрос без ответа до закрытия.
03
Контроль
Владелец принимает решение о закрытии в одностороннем порядке. Участники узнают постфактум.
Решение
Один экран — вся информация по вкладу
Владелец создаёт вклад (сумма, ставка, срок), приглашает участников по email. Каждый видит те же данные: накопленная доходность считается в реальном времени, история участников — прозрачна. Владелец может закрыть вклад — все получают уведомление. Данные защищены на уровне БД через Row Level Security: чужой вклад недоступен даже по прямой ссылке.
Ключевые решения
Четыре нетривиальных выбора
Решение #1
Разрыв рекурсии RLS через SECURITY DEFINER
Было
При создании вклада Supabase возвращал «infinite recursion detected in policy for relation deposits». Политика SELECT на deposits обращалась к deposit_members, а политика на deposit_members — обратно к deposits. Классический взаимный цикл.
Стало
Функция is_deposit_owner(did) с атрибутом SECURITY DEFINER читает deposits в обход RLS — цикл разрывается в одной точке. Политики перестроены поверх этой функции.
Решение #2
Приём приглашения только адресатом — сверка email
Было
Bearer-подход: любой авторизованный, открывший ссылку /auth/invite/<token>, может вступить в вклад. Просто реализовать.
Стало
В функции accept_invitation email авторизованного пользователя сверяется с invitation.email (case-insensitive). При несовпадении — ошибка EMAIL_MISMATCH. Регистрация/вход прокидывают ?next=, чтобы участник возвращался на страницу инвайта под нужным email.
Решение #3
Модель доходности: простой процент, год = 365 дней
Было
Несколько вариантов: простой процент, сложный (с капитализацией), банковский год 360 дней. Каждый даёт разные цифры.
Стало
Простой процент без капитализации, год строго 365 дней. Формула: accrued = amount · (rate / 100 / 365) · daysElapsed, где daysElapsed ограничен сроком вклада. Реализована в чистой функции computeYield, покрытой юнит-тестами.
Решение #4
middleware.ts → proxy.ts: обход конфликта Next.js + Supabase
Было
Стандартный middleware.ts для обновления Supabase-сессии. В Next.js 16 (App Router) middleware конфликтует с некоторыми Server Actions — сессия не пробрасывается.
Стало
Supabase-клиент вынесен в proxy.ts, который оборачивает cookie-чтение. Middleware остался минимальным (только редиректы). Сессия обновляется через server-side хелпер, а не через middleware.
Архитектура
Почему Next.js + Supabase, а не проще
Финансовое приложение с инвайтами требует авторизации, хранения данных и разграничения доступа. Supabase даёт PostgreSQL с RLS, Auth и realtime из коробки — без написания API-слоя. Next.js 16 с Server Actions позволяет держать логику на сервере, не выставляя эндпоинты наружу. Альтернативы (Firebase, PocketBase) рассматривались, но отвергнуты: Firebase — vendor lock-in без SQL, PocketBase — нет проверенного RLS для финансовых сценариев.
PostgreSQL + RLS
Доступ к строкам проверяется на уровне БД — не в коде приложения. Даже прямой SQL-запрос вернёт только свои данные.
Server Actions
Мутации (создать вклад, принять инвайт, закрыть) — серверные функции без REST-эндпоинтов. Меньше поверхности атаки.
computeYield — чистая функция
Расчёт доходности изолирован и покрыт тестами. Изменить модель (капитализация, 360 дней) — 1 файл + 1 тест-сюит.
Email-сверка на инвайт
Принять приглашение может только тот, чей email совпадает с invitation.email. Утёкшая ссылка не даёт доступ.
Доверие к документации Supabase без проверки версии Next.js
RLS-рекурсия обнаружена только в проде
Рефлексия
Что вынесла
Первый проект с полноценным бэкендом после MedLog и razdelit-schet (оба без сервера). Главный урок: RLS — это не «настроить один раз», а архитектурное решение, которое нужно проектировать с самого начала и тестировать с несколькими ролями, а не одним пользователем. Второй урок: документация экосистемы (Supabase, Next.js) часто пишется для предыдущей версии — перед тем как следовать примеру, проверяй версию в примере и версию в проекте.