Схема базы данных планов и прав доступа для апгрейдов и дополнений
Схема базы данных для планов и прав доступа, поддерживающая апгрейды, дополнения, триалы и отказы без жёстко прописанных правил с помощью ясных таблиц и проверок.

Почему планы и фичи быстро превращаются в хаос
На странице с ценами планы выглядят просто: Basic, Pro, Enterprise. Хаос начинается в тот момент, когда вы пытаетесь превратить эти имена в реальные правила доступа внутри приложения.
Жёстко закодированные проверки фич (например, if plan = Pro then allow X) подходят для первой версии. Потом меняется прайсинг. Фича переезжает из Pro в Basic, появляется новое дополнение, или в продажу входит кастомный пакет. Вдруг одно и то же правило копируется в API, UI, мобильные приложения и фоновые джобы. Вы меняете в одном месте и забываете другое. Пользователи замечают.
Второй проблемой является время. Подписки — это не статичная метка; они меняются в середине периода. Кто-то апгрейдится сегодня, даунгрейдится в следующем месяце, ставит паузу или отменяет, имея ещё оплаченный период. Если в базе хранится только «текущий план», вы теряете хронологию и не сможете ответить на простые вопросы позже: к чему у них был доступ в прошлый вторник? Почему поддержка одобрила возврат?
Дополнения усугубляют ситуацию, потому что они пересекают планы. Дополнение может открыть дополнительные места, убрать ограничение или включить конкретную фичу. Люди могут купить его на любом плане, снять позже или сохранить после даунгрейда. Если правило вшито в код, вы получите растущую кучу исключений.
Типичные ситуации, которые ломают наивные дизайны:
- Апгрейд в середине периода: доступ должен измениться сразу, а прайорирование биллинга может выполняться по другим правилам.
- Запланированный даунгрейд: доступ может оставаться «высоким» до конца оплаченного периода.
- Грандфазеринг: старые клиенты сохраняют фичу, которой нет у новых.
- Кастомные сделки: одному аккаунту дают Фичу A, но не Фичу B, хоть у них одинаковое имя плана.
- Нужды аудита: поддержка, финансы или комплаенс спрашивают «что именно было включено и когда?»
Цель проста: гибкая модель контроля доступа, которая выдерживает изменения в прайсе, без переписывания бизнес-логики каждый раз. Вам нужно одно место, где можно спросить «могут ли они это?» и база данных, которая объяснит ответ.
К концу этой статьи у вас будет шаблон схемы, который можно скопировать: планы и дополнения становятся входными данными, а entitlements — единственным источником прав на фичи. Такой же подход подходит и для no-code-конструкторов вроде AppMaster: правила хранятся в данных и к ним можно консистентно обращаться из бэкенда, веба и мобильных приложений.
Важные термины: план, дополнение, entitlement и доступ
Многие проблемы с подписками начинаются с путаницы в словах. Если все используют одно и то же слово в разных значениях, ваша схема превращается в набор исключений.
Термины, которые стоит различать в схеме баз данных для планов и прав доступа:
- Plan: базовый пакет, который получает подписчик (например, Basic или Pro). План задаёт исходные лимиты и включённые фичи.
- Add-on: опциональная покупка, которая меняет базу (например, «доп. места» или «расширённая отчётность»). Дополнения должны прикрепляться и сниматься без изменения плана.
- Entitlement: итоговое, вычисленное «что у них есть сейчас» после объединения plan + add-ons + overrides. Именно этот слой должно запрашивать приложение.
- Permission (или capability): конкретное действие (например, «export data» или «manage billing»). Права часто зависят от роли плюс entitlements.
- Access: реальный результат, когда приложение применяет правила (экран показывает или скрывает функцию, API-запрос разрешён или заблокирован, применяется лимит).
Фич-флаги близки по смыслу, но другие по назначению. Feature flag — это переключатель продукта для рул-оутов, экспериментов или отключения фичи во время инцидента. Entitlement — это доступ, привязанный к конкретному клиенту на основе оплаты или вручную предоставленного права. Используйте флаги для изменения поведения групп без вмешательства в биллинг. Используйте entitlements, когда доступ должен соответствовать подписке, счёту или контракту.
Ещё один источник путаницы — область применения (scope). Разделяйте понятия:
- User: один человек. Подходит для ролей (admin vs member) и личных лимитов.
- Account (customer): платящее юрлицо. Подходит для биллинга и владения подпиской.
- Workspace (project/team): место работы. Во многих продуктах entitlements применяются на уровне workspace (места), например места/хранилище/модули.
Время важно, потому что доступ меняется. Моделируйте это явно:
- Start и end: entitlement может быть активен только в окне (триал, промо, годовой контракт).
- Запланированное изменение: апгрейды могут начаться сейчас; даунгрейды часто на следующем ренюале.
- Грейс и отмены: вы можете разрешить ограниченный доступ после провала оплаты, но только до явной конечной даты.
Пример: компания на Pro добавляет «Advanced Reporting» в середине месяца и затем планирует даунгрейд до Basic с нового цикла. План меняется позже, дополнение стартует сейчас, а слой entitlements остаётся единственным местом для вопроса: «Может ли этот workspace просматривать расширенные отчёты сегодня?»
Простая коревая схема для планов и фич
Хорошая схема разделяет то, что вы продаёте (планы и дополнения), и то, что люди могут делать (фичи). Если поддерживать эти два понятия чистыми, апгрейды и новые дополнения — это правка данных, а не переписка кода.
Практический набор таблиц, который подойдёт большинству продуктов подписки:
products: продаваемая сущность (базовый план, командный план, дополнение «доп. места», «приоритетная поддержка»).plans: опционально, если вы хотите хранить планы как особый типproductsс полями для биллинга и публичного порядка отображения. Многие хранят планы прямо вproductsи используют колонкуproduct_type.features: каталог возможностей (доступ к API, max projects, экспорт, SSO, SMS кредиты).product_features(илиplan_features, если разделять): таблица связей, описывающая, какие фичи включены в какой продукт, обычно с указанием значения.
Именно таблица связей даёт большую часть гибкости. Фичи редко просто вкл/выкл. План может включать max_projects = 10, а дополнение добавить +5. Поэтому в product_features стоит поддерживать по крайней мере:
feature_value(число, текст, JSON или отдельные колонки)value_type(boolean, integer, enum, json)grant_mode(replace vs add), чтобы дополнение могло «добавить 5 мест», не перезаписывая базовый лимит
Моделируйте дополнения как продукты тоже. Единственная разница — способ покупки. Базовый продукт обычно «один в наборе», а дополнение может иметь количество. Но оба мапятся на фичи одинаково. Это избегает специальных кейсов вроде «если addon X, то включить Y» рассеянных по коду.
Фичи должны быть данными, а не константами кода. Если проверки фич захардкожены в сервисах, вы рано или поздно получите рассинхрон (веб говорит да, мобильный — нет, бэкенд — другое). Когда фичи в базе, приложение задаёт один вопрос и вы правите строки, чтобы менять поведение.
Имена важнее, чем кажется. Используйте стабильные идентификаторы, которые не меняются, даже если маркетинговое название поменяется:
feature_key, напримерmax_projects,sso,priority_supportproduct_code, напримерplan_starter_monthly,addon_extra_seats
Отделяйте display-лейблы (feature_name, product_name). В инструментах вроде AppMaster Data Designer с PostgreSQL хранение этих ключей как уникальных полей окупается быстро: вы можете безопасно менять отображение, не ломая интеграции и отчётность.
Слой entitlements: одно место для вопроса «могут ли они?»
Большинство систем с подписками летят в сторону, когда «что они купили» хранится в одном месте, а «что они могут» вычисляется в пяти разных кодовых путях. Решение — слой entitlements: таблица (или view), представляющая эффективный доступ субъекта в конкретный момент времени.
Если вы хотите схему, которая переживёт апгрейды, даунгрейды, триалы и единичные гранты — этот слой делает поведение предсказуемым.
Практичная таблица entitlements
Думайте про каждую строку как про одно утверждение: «этот субъект имеет доступ к этой фиче с этим значением, с этого времени по это время, из этого источника». Обычная форма:
- subject_type (например
account,user,org) и subject_id - feature_id
- value (эффективное значение для этой фичи)
- source (откуда пришло:
direct,plan,addon,default) - starts_at и ends_at (nullable ends_at для продолжающегося доступа)
Значение можно хранить разными способами: одна колонка текст/JSON плюс value_type, или отдельные колонки value_bool, value_int, value_text. Держите это просто и удобно для запросов.
Типы значений, покрывающие большинство случаев
Фичи не всегда вкл/выкл. Эти типы обычно покрывают реальные потребности биллинга и контроля доступа:
- Boolean: включено/выключено (
can_export= true) - Quota number: лимит (
seats= 10,api_calls= 100000) - Tier level: ранг (
support_tier= 2) - String: режим или вариант (
data_retention=90_days)
Приоритеты: как решать конфликты
Конфликты нормальны. Клиент может иметь план с 5 местами, купить дополнение ещё на 10, и получить ручной грант от поддержки.
Задайте чёткое правило и используйте его везде:
- Direct grant переопределяет план
- Затем идут дополнения
- Затем — значения по умолчанию
Простой подход — хранить все кандидатные строки (derived from plan, addon-derived, direct) и вычислять итог «победителя» по subject_id + feature_id, сортируя по приоритету источника, затем по newest starts_at.
Конкретный сценарий: клиент даунгрейдит план сегодня, но у него оплачено дополнение до конца месяца. С starts_at/ends_at на entitlements даунгрейд применяется сразу для плановых фич, а дополнение остаётся активно до ends_at. Приложение отвечает «могут ли они?» одним запросом, вместо набора специальных правил.
Подписки, items и доступ, ограниченный по времени
Каталог планов (plans, add-ons, features) — это «что». Подписки — это «кто имеет что и когда». Если разделять это, апгрейды и отмены перестают быть страшными.
Практический паттерн: одна подписка на аккаунт и много subscription_items под ней (один для базового плана и ноль или больше для дополнений). Это даёт чистое место для записи изменений по времени без переписывания правил доступа.
Основные таблицы для моделирования временной шкалы покупок
Можно упростить до двух таблиц, легко доступных для запросов:
subscriptions: id, account_id, status (active, trialing, canceled, past_due), started_at, current_period_start, current_period_end, canceled_at (nullable)subscription_items: id, subscription_id, item_type (plan, addon), plan_id/addon_id, quantity, started_at, ends_at (nullable), source (stripe, manual, promo)
Обычная деталь: храните каждую позицию с её датами. Так вы можете дать дополнение только на 30 дней или позволить плану работать до конца оплаченного периода даже при отмене.
Держите прайорирование и биллинг вне логики доступа
Прайорирование, инвойсы и ретраи платежей — это задачи биллинга. Доступ — это задача entitlements. Не пытайтесь «вычислять доступ» из строк счёта.
Вместо этого биллинг обновляет записи подписки (например, продлевает current_period_end, создаёт новую subscription_item строку или ставит ends_at). Приложение отвечает на вопросы о доступе, опираясь на временные записи подписки (а позже — на слой entitlements), а не на расчёты в биллинге.
Запланированные изменения без сюрпризов
Апгрейды и даунгрейды часто должны вступать в силу в конкретный момент:
- Добавьте
pending_plan_idиchange_atвsubscriptionsдля одного запланированного изменения. - Или используйте
subscription_changesтаблицу (subscription_id, effective_at, from_plan_id, to_plan_id, reason), если нужно сохранять историю и несколько будущих изменений.
Это предотвращает захардкоженные правила вроде «даунгрейды происходят в конце периода» в случайных частях кода. Расписание — это данные.
Где помещаются триалы
Триалы — это просто доступ, ограниченный по времени, с другим источником. Два чистых варианта:
- Обрабатывать trial как статус подписки (
trialing) с датами trial_start/trial_end. - Или создать trial-granted items/entitlements с started_at/ends_at и source =
trial.
В AppMaster эти таблицы аккуратно мапятся в Data Designer (PostgreSQL), и даты позволяют просто запросить «что активно сейчас» без специальных случаев.
Шаг за шагом: внедрение паттерна
Хорошая схема начинается с одного обещания: логика фич живёт в данных, а не разбросана по коду. Приложение должно задавать один вопрос — «каковы эффективные entitlements сейчас?» — и получать ясный ответ.
1) Определите фичи со стабильными ключами
Создайте таблицу feature со стабильным, читаемым ключом, который вы никогда не будете менять (даже если UI-лейбл поменяется). Хорошие ключи: export_csv, api_calls_per_month, seats.
Добавьте тип, чтобы система знала, как трактовать значение: boolean (вкл/выкл) vs numeric (лимиты). Держите всё просто и предсказуемо.
2) Сопоставьте планы и дополнения с правами
Теперь нужны два источника истины: что включает план и что даёт каждое дополнение.
Простая последовательность:
- Поместите все фичи в таблицу
featureсо стабильными ключами и типом значения. - Создайте
planиplan_entitlement, где каждая строка даёт значение фичи (напримерseats = 5,export_csv = true). - Создайте
addonиaddon_entitlement, которые дают дополнительные значения (напримерseats + 10,api_calls_per_month + 50000, илиpriority_support = true). - Решите, как комбинировать значения: булевы обычно OR, числовые лимиты часто MAX (выбирается большее), а количественные места — SUM.
- Фиксируйте начало и конец entitlements, чтобы апгрейды, отмены и прайорирование не ломали проверки доступа.
В AppMaster эти таблицы моделируются в Data Designer, а правила комбинирования можно вынести в простую таблицу «policy» или enum, используемый Business Process логикой.
3) Генерируйте «эффективные entitlements»
Есть два подхода: вычислять на чтение (query и merge каждый раз) или создавать кэш-снимок при изменениях (когда меняется план, добавляется дополнение, происходит ренюал). Для большинства приложений снимки проще понимать и они быстрее под нагрузкой.
Обычный подход — таблица account_entitlement, хранящая итоговое значение по фиче, плюс valid_from и valid_to.
4) Применяйте доступ одной проверкой
Не разбрасывайте правила по экранам, эндпоинтам и фоновых джобам. Сделайте одну функцию, которая читает эффективные entitlements и решает.
can(account_id, feature_key, needed_value=1):
ent = get_effective_entitlement(account_id, feature_key, now)
if ent.type == "bool": return ent.value == true
if ent.type == "number": return ent.value >= needed_value
Когда всё вызывает can(...), апгрейды и дополнения становятся обновлениями данных, а не переписываемым кодом.
Пример: апгрейд и дополнение без сюрпризов
Шестичленная команда поддержки на Starter. Starter включает 3 места агентов и 1000 SMS в месяц. В середине месяца у них 6 агентов и они покупают пакет на 5000 SMS. Всё должно работать без специальных проверок вроде «если plan = Pro, то…».
День 1: они на Starter
Создаёте subscription для аккаунта с платёжным периодом (например, 1–31 января). Затем добавляете subscription_item для плана.
На чекауте (или через nightly job) вы записываете гранты entitlements для этого периода:
entitlement_grant:agent_seats, value3, startJan 1, endJan 31entitlement_grant:sms_messages, value1000, startJan 1, endJan 31
Приложение никогда не спрашивает «на каком плане они?» — оно спрашивает «каков их эффективный entitlement сейчас?» и получает seats = 3, SMS = 1000.
День 15: апгрейд до Pro и добавление SMS-пака
15 января они апгрейдятся до Pro (10 мест, 2000 SMS). Не изменяйте старые гранты. Добавляете новые записи:
- Закрываете старый subscription_item: ставите ends_at =
Jan 15для Starter - Создаёте новый subscription_item: Pro start
Jan 15, endJan 31 - Добавляете subscription_item: SMS Pack 5000 start
Jan 15, endJan 31
Потом добавляются гранты для этого периода:
entitlement_grant:agent_seats, value10, startJan 15, endJan 31entitlement_grant:sms_messages, value2000, startJan 15, endJan 31entitlement_grant:sms_messages, value5000, startJan 15, endJan 31
Что происходит сразу 15 января?
- Seats: эффективные места становятся 10 (правило: взять max для seats). Они могут добавить ещё 3 агентов в тот же день.
- SMS: эффективный лимит становится 7000 на остаток периода (мы выбираем правило «суммирование» для пакетов сообщений).
Никакие старые использования не нужно «переносить». Таблица с использованием продолжает считать отправленные сообщения; проверка entitlement просто сравнивает использование этого периода с текущим лимитом.
День 25: запланированный даунгрейд, доступ до конца периода
25 января они планируют даунгрейд до Starter с 1 февраля. Вы не трогаете январские гранты. Создаёте будущие элементы для следующего периода:
subscription_item(Starter) startFeb 1, endFeb 28- Нет SMS-пака с
Feb 1
Результат: они сохраняют Pro-места и SMS-пак до 31 января. С 1 февраля эффективные места падают до 3 и SMS сбрасывается до лимитов Starter в новом периоде. Всё просто и предсказуемо — в AppMaster это делается изменением дат и созданием новых строк, а запрос entitlements остаётся неизменным.
Частые ошибки и ловушки
Большинство багов с подписками — это не биллинг-баги. Это баги доступа, вызванные рассредоточением логики по продукту. Быстрее всего схема ломается, когда на вопрос «могут ли они?» отвечают в пяти разных местах.
Классическая ошибка — захардкодить правила в UI, API и фоновых джобах по отдельности. UI скрывает кнопку, API забывает заблокировать эндпоинт, а nightly job всё ещё выполняется, потому что он проверяет что-то иное. Получается «работает иногда» — такие баги трудно воспроизвести.
Ещё одна ловушка — проверять plan_id вместо фич. Сначала кажется проще («план A может экспортировать, план B нет»), но это разваливается, когда вы добавляете дополнение, грандфазеринг, триал или enterprise-исключение. Если вы пишете if plan is Pro then allow…, вы строите лабиринт поддержки навсегда.
Крайние случаи с временем и отменой
Доступ «застревает», когда вы храните только булеву переменную has_export = true и никогда не привязываете её к времени. Отмены, возвраты, chargeback и даунгрейды в середине периода требуют временных границ. Без starts_at и ends_at вы случайно дадите постоянный доступ или отберёте доступ слишком рано.
Простые правила, чтобы этого избежать:
- Каждый entitlement grant должен иметь источник (plan, add-on, manual override) и временной диапазон.
- Каждое решение по доступу должно проверять «now между start и end» (с явными правилами для null end дат).
- Фоновые джобы должны повторно проверять entitlements во время выполнения, а не полагаться на вчерашнее состояние.
Не смешивайте биллинг и доступ
Команды садятся в неприятности, смешивая записи биллинга и правила доступа в одной таблице. Биллинг нуждается в инвойсах, налогах, прайорировании, идентификаторах провайдера и состояниях ретраев. Доступ нуждается в чётких feature_key и временных окнах. Когда это перепутано, миграция биллинга может стать причиной падения продукта.
Наконец, многие системы пропускают аудиторский след. Когда пользователь спрашивает «почему я могу экспортировать?», вам нужен ответ: «Включено дополнением X с 2026-01-01 по 2026-02-01» или «Выдано вручную поддержкой, тикет 1842». Без этого поддержке и инжинирингу приходится гадать.
В AppMaster храните поля аудита в Data Designer и делайте проверку «могут ли они?» единственным Business Process, который используют веб, мобильные и расписания.
Бычеклист перед релизом
Прежде чем выпустить схему, прогоните простой чеклист с реальными вопросами. Цель — сделать доступ объяснимым, тестируемым и гибким для изменений.
Вопросы реальности
Возьмите одного пользователя и одну фичу и попытайтесь объяснить результат так, как вы бы объяснили поддержке или финансам. Если вы можете только сказать «они на Pro» (или хуже, «так код сказал»), вы почувствуете боль при первом апгрейде в середине периода или единичной сделке.
Короткий чеклист:
- Можете ли вы объяснить «почему у этого пользователя есть доступ?» только по данным (subscription items, add-ons, overrides, временные окна), без чтения кода?
- Все ли проверки доступа основаны на стабильных ключах фич (например
feature.export_csv), а не на названиях планов (Starter,Business)? Названия планов меняются; ключи фич — нет. - Есть ли у entitlements явные start и end, включая триалы, грейс-периоды и запланированные отмены? Если время отсутствует, даунгрейды становятся предметом споров.
- Можно ли дать или снять доступ для одного клиента с помощью override записи, без ветвления логики? Это позволяет «дать им 10 доп. мест в этом месяце» без кастомного кода.
- Можете ли вы протестировать апгрейд и даунгрейд на паре тестовых строк и получить предсказуемый результат? Если нужен сложный скрипт для симуляции — модель слишком неявная.
Практичный тест: создайте три аккаунта (новый, апгрейдившийся в середине месяца, отменивший) и одно дополнение (например «доп. места» или «advanced reports»). Запустите запрос entitlements для каждого. Если результаты очевидны и объяснимы — вы готовы.
В AppMaster одно правило: сделайте один запрос или Business Process, отвечающий на вопрос «могут ли они?», чтобы все клиенты и экраны использовали один и тот же ответ.
Следующие шаги: упростите поддержку апгрейдов
Лучший способ удержать апгрейды в порядке — начать меньше, чем кажется нужным. Выберите 5–10 фич, которые действительно приносят ценность, и постройте одну entitlement-функцию, отвечающую на вопрос: «Может ли этот аккаунт сделать X прямо сейчас?» Если вы не можете ответить на это в одном месте, апгрейды всегда будут рискованными.
Когда эта проверка заработает, относитесь к путям апгрейда как к поведению продукта, а не только к биллингу. Самый быстрый способ поймать странные кейсы — написать набор тестов доступа, основанных на реальных движениях клиентов.
Практичные шаги, приносящие быстрый эффект:
- Определите минимальный каталог фич и сопоставьте каждый план с набором entitlements.
- Добавляйте дополнения как отдельные «items», которые дают или расширяют entitlements, вместо встраивания их в правила плана.
- Напишите 5–10 тестов доступа для типичных сценариев (апгрейд в середине периода, даунгрейд при ренюале, добавление/удаление дополнения, переход триал→платный, грейс-период).
- Делайте изменения цен как правки данных: обновляйте строки плана, сопоставления фич и гранты, а не код.
- Введите правило: каждый новый план или дополнение должно сопровождаться по крайней мере одним тестом, доказывающим ожидаемое поведение доступа.
В no-code-бэкенде всё равно можно чисто смоделировать этот паттерн. В AppMaster Data Designer создайте таблицы (plans, features, subscriptions, subscription items, entitlements). Business Process Editor пусть хранит поток принятия решения по доступу (загрузить активные entitlements, применить временные окна, вернуть allow/deny), чтобы не писать разбросанные проверки по эндпоинтам.
Преимущество появляется при следующем изменении прайса. Вместо переписывания правил вы редактируете данные: фича переехала из «Pro» в add-on, изменилось время entitlements, или у legacy-плана остаются старые гранты. Логика доступа остаётся стабильной, и апгрейды превращаются в контролируемые обновления, а не в кодовый спринт.
Если вы хотите быстро проверить схему — смоделируйте один апгрейд и одно дополнение end-to-end и прогоните тесты доступа перед добавлением нового функционала.


