Published on
Updated on 

Как мы управляем планами и функциями в нашем SaaS-приложении

Authors

Автор статьи — Tim Nolet, ссылка на оригинал: How we manage plans & features in our SaaS app. Перевод опубликован с разрешения автора.

Как контролировать то, что пользователь может делать в своей учетной записи в SaaS-приложении? Сможет ли Джейн на плане «Стартер» создать еще один виджет, когда приблизится к лимиту своего тарифа? А что, если она пользуется пробной версией?

Оказывается, решение включает несколько аспектов:

  • Переключение функций
  • Подсчет ресурсов™
  • Пользовательское промежуточное ПО API, специфичное для вашего случая

Как и в нашей предыдущей статье из этой серии о создании базовой модели данных SaaS, не хватает четких примеров решения этой распространенной проблемы.

Вот как мы делаем это в Checkly с нашим бэкендом на Node.js и Hapi.js. Вероятно, этот подход можно адаптировать и для других платформ.

Суть проблемы

Давайте сделаем это максимально конкретным, ведь, как говорится, страница с ценами SaaS стоит тысячи слов.

У нас есть три тарифных плана с разными ценами: Developer, Starter и Growth. Разные планы предлагают разные лимиты и набор функций.

В этом примере:

  • API- и браузерные проверки имеют ограничения. План Developer предлагает 5 проверок, Starter — 15, Growth — 40.
  • Функция командных участников может быть включена или отключена, а при включении также имеет количественные ограничения.
  • Функция CI/CD триггеров либо включена, либо отключена. Нет количественных ограничений.

На странице с ценами не видно, что происходит во время пробного периода. В течение 14-дневного пробного периода мы не предоставляем новым пользователям защищенную SSL-панель мониторинга. По техническим причинам и во избежание злоупотреблений эта функция становится доступной только после оплаты.

Кроме того, у клиентов могут возникнуть проблемы с кредитной картой, или они могут просто перестать платить по неизвестным причинам. Мы должны это учитывать, но также стараемся не разозлить клиентов, у которых просто возникли временные проблемы с банком.

Выделим четыре категории «вещей, которые мы должны как-то контролировать и отслеживать» в нашем SaaS-приложении:

  1. Пробный период vs платная подписка: Вы все еще присматриваетесь к продукту или уже стали полноценным участником нашего сообщества?
  2. Активная оплата vs истекшая подписка: Раньше вы платили, но больше этого не делаете...
  3. Переключатели функций на основе плана: позволяет ли ваш тариф получить доступ к этой функции?
  4. Ограничения ресурсов на основе плана: позволяет ли ваш тариф создавать больше этих элементов?

Пробный и не пробный период

Каждому зарегистрированному пользователю автоматически назначается пробный план. Модель данных показана ниже. Более подробную информацию можно найти в предыдущей статье.

Проверить это просто, достаточно сделать код примерно такого вида на вашем языке:

if (account.plan.name === 'trial') {
  // do trial things
}

Пробный период — это довольно бинарная концепция, которая хорошо описывается булевым значением. Просто убедитесь, что вы переключаете пользователя на другой план, когда он/она начинает платить. Что приводит нас к...

Оплата vs Истекшая подписка

Должно быть просто, верно? Кто-то подписывается на платный план, и вы переключаете флаг с paying = false на paying = true.

Но что на самом деле означает «платящий»? А что если пользователь перестанет платить?

В Checkly «платящий» означает, что учетная запись в нашей базе данных Postgres имеет ненулевой stripe_subscription_id и дату plan_expiry, которая находится в будущем. В коде на JavaScript:

const paying = account.stripe_subscription_id != null && account.plan_expiry > Date.now()

Оба поля устанавливаются при получении вебхука от Stripe, сигнализирующего об успешной оплате подписки. Это автоматически отслеживает просроченные платежи и отмены подписок. Нет необходимости в дополнительном коде для обновления отдельного поля «оплата».

Вывод: «статус оплаты» — это не булево значение, которое вы обновляете явно. Это вычисляемое свойство, зависящее от группы полей. Учитывайте, что означает статус «платящего пользователя» в вашем конкретном контексте. Если это ежемесячная/ежегодная SaaS-подписка, вам, вероятно, потребуется проверять несколько полей данных.

Переключение функций на основе плана

Чтобы проверить, к каким функциям пользователь может получить доступ на основе своего плана, мы храним набор строковых констант для каждой учетной записи в поле features. Оно дополняет базовый набор функций, доступных каждому подписчику. Пустой список features означает, что у вас базовый план. В коде:

const features = ['CI_CD_TRIGGERS', 'SOME_OTHER_FEATURE']

Этот набор features хранится в виде массива в каждой записи account, с которой связан пользователь. Это поле доступно и для бэкенда, и для фронтенда, но, разумеется, только для записи со стороны бэкенда. Пользователи не могут обновлять свои собственные функции!

Это поле заполняется или обновляется только в двух случаях:

  1. Пользователь регистрируется на пробный период. Мы заполняем поле features функциями пробного периода.
  2. Пользователь переходит на платный аккаунт. Мы обновляем поле features функциями соответствующего тарифного плана.

У нас нет особого интерфейса для управления этими функциями. Это не какие-то эксперименты или загадочные фреймворки для запуска.

Checkly — это одностраничное приложение на Vue.js с бэкендом на Hapi.js API. Но этот подход, вероятно, работает в любой системе, независимо от того, использует она SPA или нет.

Вот как выглядит наш маршрут для контроллера триггеров:

const a = require('../../models/defaults/access-rights')
const f = require('../../models/defaults/features')

{
  method: 'POST',
  path: '/accounts/triggers/{checkId}',
  config: {
    plugins: {
      policies: [hasAccess([a.OWNER, a.ADMIN]), hasFeature(f.TRIGGERS)]
    },
    handler: TriggerController.createTrigger
  }
},

Здесь есть два интересных момента:

  • Функция hasAccess, которая проверяет права доступа пользователей.
  • Функция hasFeature, которая проверяет наличие функций.

Обе функции используются с помощью плагина mr-horse, позволяющего привязывать политики к любому маршруту API. Вы также видите, что мы импортируем канонический список прав доступа и функций из централизованного списка значений по умолчанию.

То, как именно работают функции hasAccess и hasFeature, сильно зависит от того, какой язык/фреймворк вы используете.

Вот упрощенные версии кода, показывающие, как мы реализуем проверку прав доступа и функций. Обратите внимание, что обе функции возвращают функции, которые HTTP-маршрутизатор внедряет в цикл обработки HTTP-запросов:

const hasAccess = function (accessRights) {
  // Определяем функцию для проверки доступа на основе данных запроса.
  // На предыдущем этапе аутентификации данные учетной записи были 
  // получены из базы данных.

  const hasSpecificAccess = function (request, reply, next) {
    if (accessRights.includes(access)) {
      next(null, true)
    } else {
      next(null, false)
    }
  }
  return hasSpecificAccess
}

Проверка функциональности:

const hasFeature = function (feature) {
  const hasSpecificFeature = function (request, reply, next) {
    // проверяем, включена ли функция

    return features && features.includes(feature) ? next(null, true) : next(null, false)
  }
  return hasSpecificFeature
}

Ограничения ресурсов по плану

Проверка функций плана довольно аккуратно реализуется общим способом подтверждения того, включена функция или нет.

Проверка количественных ограничений несколько отличается. Почему? Потому что нам нужно проверять текущее состояние конкретных ресурсов, которые мы предлагаем клиентам, а не просто флаги в учетной записи.

Это означает, что вам нужно активно опрашивать базу данных и подсчитывать элементы при каждом запросе. Да, можно использовать кэширование, и небольшая погрешность может не быть концом света.

В приведенном выше примере страницы с ценами видно, что Checkly предлагает 5 API-проверок для одного плана и 15 для другого. Вот как мы проверяем этот количественный лимит в нашем бэкенд-API:

function getVolumeLimits(accountId, delta) {
  const checksCountQuery = Checks.query().where({ accountId }).count()
  const accountLimitsQuery = Account.query().findOne({ accountId })

  return Promise.all([checksCountQuery, accountLimitsQuery]).then((res) => {
    const count = res[0].count
    const { maxChecks } = res[1]
    const newTotal = parseInt(count) + delta
    return newTotal <= maxChecks
  })
}
  1. Эта функция выполняется после базовой авторизации, но до выполнения какой-либо реальной работы.
  2. Мы одновременно получаем текущее количество проверок и лимит проверок для текущего аккаунта. Это JavaScript выражение с Promise.all.
  3. Мы сравниваем текущее количество с новым общим количеством. В нашем случае пользователь может создать несколько проверок одновременно, отсюда и аргумент delta. В этом примере он равен 1, но в реальности может быть любое число больше 0. Нам нужно проверить, вписывается ли общее количество новых «создаваемых вещей» в лимиты плана.
  4. В конце мы возвращаем результат сравнения: меньше или равно newTotal значению maxChecks нашего плана.

Проверка того, что пользователи не превышают лимиты своих планов на бэкенде, действительно важна по разным причинам. Но как мы будем «красиво» обрабатывать это на фронтенде, особенно в SPA-приложении? Мы не хотим ситуации, когда пользователь радостно создает новый элемент, нажимает отправить и получает сообщение «вы превысили лимиты вашего плана».

А что насчет остального?

А как насчет управления доступом на основе ролей?
Как, черт возьми, вы управляете этим на фронтенде?

Да, все это важные темы. Все они связаны с материалом, рассмотренным выше, поэтому мы рассмотрим их в следующей статье.

Was this helpful?

Result: 0, total votes: 0

I'm Mike, your guide in the expansive world of technology journalism, with a special focus on GPS technologies and mapping. My journey in this field extends over twenty fruitful years, fueled by a profound passion for technology and an insatiable curiosity to explore its frontiers.