Вход на сайт

Просмотр новости

Найдите то, что Вас интересует

Одна карточка на всех без сервера: детерминированная генерация, RLS и флаг retired в шахматном бинго

Дата публикации: 22-09-2026 05:35:51

Разбираю несколько инженерных решений из небольшого браузерного проекта: бинго на шахматных задачах Lichess без собственного сервера. Как получить одну и ту же карточку у всех игроков в один день с помощью детерминированного сида, почему запись из пула данных нельзя удалять (флаг retired), как записать правило «одна партия в день» частичными уникальными индексами и почему политик RLS в Supabase недостаточно: по умолчанию клиентским ролям выданы права, в том числе TRUNCATE, который RLS не проверяет. Читать далее

Основное содержимое страницы с новостью.

Время на прочтение4 мин

Охват и читатели5.3K

Кейс

Разбираю несколько инженерных решений из небольшого браузерного проекта: бинго на шахматных задачах Lichess. Механика такая: карточка 4×4 с тактическими мотивами (вилка, связка, мат в два), игроку по очереди показывают позиции, он решает задачу и закрывает подходящую клетку. Ограничения, с которыми я сел за проект: своего сервера нет, только Supabase; играть можно без регистрации; у всех игроков в один день одна и та же карточка.

Карточка дня без сервера

Чтобы у всех была одна карточка, не нужен эндпоинт «выдай карточку дня»: достаточно детерминированного генератора. Сид строится из даты и уровня сложности (dailySeed(date, level)), из сида создаётся генератор псевдослучайных чисел (rngFrom(seed)), и generateCard(pool, categories, options, rng) собирает клетки и колоду. Один и тот же сид и один и тот же пул дают одну и ту же карточку на любом устройстве.

Слабое место такой схемы — пул данных. Стоит убрать из него одну запись или пересобрать данные, и тот же сид даст другую карточку: у уже сыгранных партий и у дуэлей, которые друг другу отправили по ссылке, «поплывёт» содержимое. Поэтому в решении три правила.

Первое: сыгранная партия хранит собственный состав. В таблицу games пишутся seed, расставленные клетки (placed, jsonb) и ошибки (misses, jsonb), и при просмотре старой партии карточка восстанавливается из сохранённого состава, а не пересчитывается заново. Второе: записи из пула не удаляются. Неудачный шахматист или партия получают флаг retired: в новые карточки они не попадают, но по выданному id их по‑прежнему находят старые карточки и дуэли. Третье: выданные id при пересборке данных не меняются. Всё это проверяет тест: он собирает 30 карточек с разными сидами и убеждается, что в колоду и в ответы не попал ни один retired‑элемент, а сохранённая карточка остаётся прежней.

Одна партия в день — на уровне базы

Правило «карточку дня и дуэль играют один раз» нельзя оставлять на клиенте: его обойдёт любой, кто откроет консоль. Оно записано в схему частичными уникальными индексами: create unique index games_one_daily on public.games (user_id, level, card_date) where kind = 'daily'; и create unique index games_one_per_duel on public.games (user_id, duel_id) where kind = 'duel'. Ограничение check ((kind = 'daily') = (card_date is not null)) не даёт записать дневную партию без даты или обычную — с датой. Вторая партия за день просто не вставится.

Отдельная тонкость — какая «сегодня» дата. У каждого игрока своя полночь, поэтому серверное время для серий и статистики не подходит: в RPC‑функции статистики локальная дата игрока приходит параметром p_today.

RLS: политики недостаточно

На таблице games включён RLS и есть только две политики: читать свои партии и добавлять свои (user_id = auth.uid()). Политик на update и delete нет намеренно: результат нельзя переписать задним числом. Казалось, этого достаточно, но при перепроверке нашлась дыра, не связанная с политиками. Supabase по умолчанию выдаёт ролям anon и authenticated все права на таблицы схемы public, в том числе TRUNCATE, а TRUNCATE не подчиняется RLS. То есть любой вошедший игрок, пусть и анонимный гость, мог одной командой очистить games, profiles, duels и лиги.

Лечится не политиками, а правами: revoke truncate, references, trigger on all tables in schema public from anon, authenticated; плюс revoke update, delete на games и аналогичные отзывы там, где клиент не должен писать. Заодно нашлось лишнее право EXECUTE на триггерной функции handle_new_user (она создаёт профиль при появлении пользователя). Напрямую её вызвать нельзя, но security definer‑функции клиентам не нужны, и право тоже отозвано. Вывод: для каждой новой таблицы стоит проверять права ролей, а не только наличие политик RLS. Проверки прогоняются скриптом на живом проекте: гость входит, читает только свои партии, запись от чужого имени и вторая карточка за день отклоняются.

Гость без регистрации и защита от ботов

Первая партия идёт от анонимного Supabase‑аккаунта, позже его можно превратить в обычный (email, пароль, ник) без потери истории. Открытый анонимный вход — приглашение для ботов, поэтому в Supabase включена CAPTCHA (Cloudflare Turnstile). Сначала виджет работал в режиме Managed и показывал галочку перед первой партией, потом его перевели в Invisible. У этого режима есть условие от Cloudflare: в политике конфиденциальности должна быть ссылка на Turnstile Privacy Addendum. Побочный эффект, о котором стоит знать заранее: с включённой CAPTCHA гостевой вход не проходит на localhost, пока хост не добавлен в список виджета.

Итоги

Детерминированный сид вместо серверной выдачи, уникальные индексы вместо проверок на клиенте, права ролей вместе с RLS и флаг retired вместо удаления данных — четыре решения, которые позволили обойтись без собственного сервера. Стек: React, Vite, TypeScript, Tailwind, chessground (поэтому код открыт под GPL-3.0), Supabase, Cloudflare Pages. Данные — открытая база задач Lichess (CC0) и Wikidata. Живой пример: chess‑bingo.pages.dev

Схожие новости

#Наименование новостиТональностьИнформативностьДата публикации
1Мы написали автономного агента для VACUUM/ANALYZE и запустили на 800+ тестовых БД: что из этого вышло08.1623-09-2026
2Каким по счёту ты родился07.5218-09-2026
3Как уронить базу данных06.7217-08-2026
4Ежедневный Хабр: 9 интересных публикаций каждый день012.2106-08-2026
5Четверо в одной транзакции: кейс-батлы, PgBouncer под Prisma и реплика, которая показывает пустой инвентарь010.6520-09-2026
6Как я пытался сделать стратегию из котов в стиральных машинах — и почему правила оказалось сложнее написать, чем код07.1811-08-2026
7Собрать прошлое: как архивировать весь трафик сборки SONiC08.9420-07-2026
8Свой VPN на Rust: как я спорил с сетью, TLS и самим собой7827-06-2026
9Как я пишу RTS про Марс на Rust вместе с ИИ-агентами: архитектура, ошибки и 400+ тестов011.2608-08-2026
10Алгоритм был правильным. Ошибка была в контракте графа-17.5713-08-2026

Классификация: Мнения. Схожих патентов: 0. Схожих новостей: 10. Тональность: 0. Информативность: 5.67. Источник: habr.com.