В начале этого года я выпустил приложение AI-powered fitness app, ориентированное на создание индивидуальных тренировок с использованием имеющегося в вашем распоряжении оборудования. Получилось довольно неплохо, но оно случайно было ориентировано на то, как я занимаюсь спортом. Для общего использования оно не подходит.
Поэтому я полностью переписал его для хакатона. Я взял лучшие идеи и [переделал его с нуля] (https://dev.to/aws-heroes/solo-saas-how-i-built-a-serverless-workout-app-by-myself-1c62). Я обобщил приложение, сделал его многопользовательским и убрал функции, которые были важны только для меня.
Это сработало.
В течение первых трех недель у меня было 70 пользователей, и я превысил свой лимит расходов на OpenAI. Мне нужно было срочно что-то придумать, чтобы избежать разрушительных сбоев в работе сервиса.
Я остановился на модели подписки, чтобы покрыть расходы на OpenAI. Участники бесплатного уровня получают ранее сгенерированные тренировки для нужной им группы мышц и уровня квалификации, а участники уровня pro получают оригинальный уровень сервиса - свежие, сгенерированные ИИ тренировки, соответствующие их уровню квалификации, имеющемуся оборудованию и требованиям по времени - всего за 7 долларов в месяц (не так уж и плохо, если хотите знать мое мнение).
Когда я начал реализовывать логику подписки в приложении, я понял, что это гораздо больше, чем я думал. Поэтому мы здесь, чтобы вместе проработать детали, чтобы вы могли начать игру с минимальными трудностями.
Обработка платежей
Прежде всего, вам нужно выбрать поставщика для обработки платежей. Я не рекомендую пытаться создать все управление платежами самостоятельно.
Ничто не приводит к большей потере конверсии, чем сомнительный платежный шлюз.
У вас может быть самый лучший сервис в мире, но если у вас есть платежный шлюз, из-за которого люди чувствуют себя некомфортно, вы их потеряете. Мало того, вам придется иметь дело с PCI compliance и множеством других вопросов, которые решаются за вас с помощью таких поставщиков платежей, как Stripe, Square или PayPal.
Эти поставщики выполняют всю тяжелую работу по сбору платежей и управлению ежемесячными подписками, чтобы вы могли сосредоточиться на бизнес-ценностях. Это стоит каждого цента (поверьте мне).
Поскольку у Stripe лучшая в своем классе документация для разработчиков, я предпочитаю обращаться именно к ним. Так что давайте пройдемся по частям управления подписками с их помощью.
Создание клиента
Мое приложение построено на AWS с использованием Cognito в качестве хранилища пользователей и механизма авторизации. Когда пользователь регистрируется, он добавляется в мой пул идентификаторов и ему присваивается идентификатор пользователя, известный как `sub (расшифровывается как субъект). Чтобы настроить биллинг в Stripe, мне нужно создать клиента в моем аккаунте, который соотносится с моим пользователем Cognito. Для этого я использую триггер PostConfirmation для выполнения функции Lambda, когда пользователь первоначально регистрируется.
Эта функция Lambda отвечает за создание клиента в Stripe и связывает вместе пользователя Cognito и идентификатор Stripe.
const saveProfileRecord = async (userId) => {
const customerId = await getPaymentId(userId);
await ddb.send(
new PutItemCommand({
TableName: process.env.TABLE_NAME,
Item: marshall({
pk: userId,
sk: 'user',
signUpDate: new Date().toISOString(),
customerId,
subscription: {
уровень: 'бесплатно',
},
}),
}),
);
};
const getPaymentId = async (userId) => {
const stripeApiKey = await getSecret('stripe');
const stripe = new Stripe(stripeApiKey);
const customer = await stripe.customers.create({
описание: 'Клиент фитнес-приложения',
метаданные: {
userId,
},
});
return customer.id;
};
Вход в полноэкранный режим
Здесь предоставленный userId является подзаписью Cognito. Поэтому мы сохраняем запись в DynamoDB, которая использует sub в качестве ключа раздела и включает customerId в качестве хэш-ключа GSI. Мы также сохраняем sub в качестве метаданных в записи пользователя в Stripe для целей перекрестных ссылок. Теперь, когда новый пользователь регистрируется в нашем приложении, у нас есть метаданные о нем и созданный в Stripe идентификатор клиента!
Создание подписок
Детали реализации дико варьируются в зависимости от вашего технологического стека, модели подписки, языка программирования и т. д. Я не буду пытаться предложить предписывающее руководство по сохранению ваших подписок в Stripe, но я предложу несколько рекомендаций.
По возможности используйте их размещенные компоненты. На момент написания статьи Stripe имеет бета-программу, которая предлагает hosted checkout components. Вы можете настроить цвета, брендинг и подписки, доступные на приборной панели Stripe, а затем просто добавить сгенерированный компонент в свое приложение. Это будет мой первый выбор, если вы примите участие в бета-программе. Он будет обрабатывать отказоустойчивость, логику повторных попыток, сеансы проверки и вызовы API для вас без особых усилий с вашей стороны. Очень круто!
Если вы хотите создать его самостоятельно, у Stripe есть front-end и back-end код шаблона, чтобы вы могли быстро запустить его. Моя главная рекомендация для вас, если вы пойдете по этому пути, - убедитесь, что вы делаете идемпотентные вызовы, чтобы избежать создания нескольких подписок или взимания платы с кого-то более одного раза. Это никому не нужно.
Убедитесь, что вы используете идентификатор клиента, который вы установили для своих пользователей при их создании, чтобы вы могли связать пользователей приложения с соответствующей подпиской.
Ответ на подписки
Stripe является источником истины для подписок. Когда одна из них добавляется/обновляется/удаляется, нам нужно реагировать на это внутри нашего приложения. Для этого мы воспользуемся функциональностью Stripe webhook. После успешного изменения подписки Stripe отправит детали транзакции в событии на указанный нами URL.
К счастью для нас, есть EventBridge Quick Start, предоставленный AWS, чтобы мы могли быстро начать работу. Мы создаем вебхук в панели управления Stripe, развертываем EventBridge Quick Start, а затем обновляем вебхук в Stripe с помощью вывода из Quick Start.
При использовании Quick Start в AWS вы развертываете стек CloudFormation от поставщика (в данном случае Stripe). При этом развертывается минимальный набор ресурсов, создающих функцию Lambda и URL-адрес функции, которая получает событие webhook. Функция проверит подпись события и убедится в легитимности полезной нагрузки перед преобразованием полезной нагрузки в событие в EventBridge.
После того как мы создали webhook в Stripe и развернули Quick Start в нашем аккаунте AWS, мы можем создать функцию Lambda для ответа на событие через EventBridge. В SAM это может выглядеть следующим образом:
HandleSubscriptionEventFunction:
Тип: AWS::Serverless::Function
Свойства:
CodeUri: functions/handle-subscription-event
Политики:
- AWSLambdaBasicExecutionRole
- Версия: 2012-10-17
Заявление:
- Эффект: Разрешить
Действие: dynamodb:UpdateItem
Ресурс: !GetAtt FitnessTable.Arn
События:
PaymentEvent:
Тип: EventBridgeRule
Свойства:
EventBusName: !Ref FitnessEventBus
Шаблон:
источник:
- stripe.com
Вход в полноэкранный режим
Вы можете видеть, что наш триггер Lambda - это событие EventBridge от stripe.com. Дальше реализация зависит от вас. Вы можете поместить пользователя в группу пользователей Cognito, соответствующую подписке, которую он только что приобрел. Вы также можете обновить запись метаданных пользователя в DynamoDB информацией о подписке. Или вы можете сделать и то, и другое! Помните, что чем больше вариантов вы выберете, тем больше данных вам придется синхронизировать.
Если вы структурируете запись метаданных пользователя таким образом, чтобы включить идентификатор клиента Stripe в качестве ключа раздела или хэш-ключа GSI, вы сможете получить доступ к пользователю через полезную нагрузку события, поступающего из EventBridge. Вот как мы можем обновить запись о клиенте в DynamoDB при создании новой подписки.
exports.handler = async (event) => {
const customerId = event.detail.data.object.customer;
if (event['detail-type'] !== 'customer.subscription.created') {
return;
}
try {
const response = await ddb.send(
new QueryCommand({
TableName: process.env.TABLE_NAME,
IndexName: 'customers',
KeyConditionExpression: 'customerId = :customerId',
ExpressionAttributeValues: marshall({
':customerId': customerId,
}),
Limit: 1,
}),
);
if (!response.Items.length) {
console.error({ error: 'Could not find customer', id: customerId });
return;
}
const user = unmarshall(response.Items[0]);
await ddb.send(
new UpdateItemCommand({
TableName: process.env.TABLE_NAME,
Key: marshall({
pk: user.pk,
sk: user.sk,
}),
UpdateExpression: 'SET subscription = :subscription',
ExpressionAttributeValues: marshall({
':subscription': {
level: event.detail.data.object.plan.product,
startDate: event.detail.data.object.start_date,
},
}),
}),
);
} catch (err) {
console.error(err);
throw err;
}
};
Вход в полноэкранный режим
Приведенный выше код сохраняет “уровень”, или план подписки, в записи пользователя. Он также сохраняет дату начала для исторических целей. Если во время сохранения данных произошла ошибка, мы полагаемся на механизм повторных попыток из EventBridge, чтобы отступить и повторить попытку.
Конечно, это не единственное событие, которое вы захотите обрабатывать с помощью подписок. Stripe предлагает события created, deleted, paused, resumed и updated для подписок, которые вам нужно будет учитывать в своем приложении. Вы же не хотите случайно предложить премиум-функции, если кто-то отменит свой тарифный план!
Поскольку в сети случаются ошибки и сбои, также полезно иметь механизм синхронизации между Stripe и вашим приложением. Раз в день вы можете запускать задание, которое запрашивает Stripe и делает перекрестные ссылки на пользователей в вашем приложении, чтобы убедиться, что все получают те функции, за которые они платят. Частота может быть такой, как вам удобно, но не ждите слишком долго!
Что дальше?
Дальнейшая часть внедрения подписок в ваше приложение полностью зависит от вас. Вы создали своих клиентов, управляете подписками и синхронизировали метаданные пользователей со Stripe, теперь вам нужно использовать эти данные. Поскольку все приложения разные, я не могу подсказать, что делать дальше. Но я могу дать вам ориентир.
В моем фитнес-приложении премиум-пользователи получают доступ к пользовательским тренировкам, сгенерированным OpenAI. В моем рабочем процессе Step Function, который [создает расписание тренировок на неделю] (https://github.com/allenheltondev/serverless-ai-fitness/blob/main/back-end/workflows/build-user-workouts.asl.json), я проверяю уровень подписки каждого пользователя и добавляю логическую ветвь в зависимости от уровня подписки. Если это бесплатный пользователь, рабочий процесс будет брать данные из кэша существующих тренировок. Если они премиум, то их данные передаются в OpenAI для создания пользовательских тренировок.
Как бы мне ни хотелось думать, что все используют Step Functions over Lambda, я знаю, что это не так. Если бы вы писали свою бизнес-логику в коде, будь то в функции Lambda или контейнере, у вас все равно были бы деревья логики, разделенные на подписки пользователей.
Summary
Вот и все! Кому-то это может показаться много, а кому-то - ничтожно мало. Но если подвести итог, то вот шаги, которые вам нужно сделать, чтобы внедрить подписки Stripe в ваше SaaS-приложение:
- Создайте в Stripe клиентов, которые будут сопоставлены 1 к 1 с пользователями вашего приложения.
- Отправьте подписки из вашего приложения либо через хостинговый компонент, либо через Stripe API, не забывая при этом делать идемпотентные запросы.
- Запустите EventBridge Quick Start для Stripe в своем аккаунте AWS и настройте веб-хук Stripe для просмотра созданного url функции.
- Добавьте обработчик событий для синхронизации пользователей приложения с подписками Stripe
- (Необязательно) Создайте повторяющееся задание, которое синхронизирует Stripe с вашим приложением
- Предоставление функций, специфичных для подписки, на основе записей пользователей
Надеюсь, это прояснило для вас план действий. Создание приложения может быть сложной задачей, особенно в одиночку, и вдвойне сложнее, когда вы работаете с деньгами. Поскольку детали реализации сильно отличаются от приложения к приложению, дать предписания может быть сложно. Тем не менее, основные компоненты одинаковы, и теперь вы должны иметь представление о том, как к этому подойти.
Удачи вам и пусть ваше приложение будет успешным!
Счастливого кодинга!

