[Из песочницы] Миллион за месяц: как запустить стартап в Европе своими силами
Всем привет!
Два месяца назад я и мой знакомый (для краткости, назовем его Илья) запустили свой стартап.
Пффф… Скажите вы. Каждый день кто-то что-то запускает. Кто-то запускает в одиночку. Некоторые кучкуются в команды. У кого-то есть деньги на разработку\маркетинг, кто-то предлагает долю, пост-оплату, опционы. Все крутятся как могут и ищут партнеров также.
У нас не было денег, был лишь опыт и 2 недели до первых продаж.
Под катом я расскажу о том, с чем мы столкнулись и как заработали миллион в кризис
Мне лишь хочется поделиться тем опытом, который приобрели мы.
Точнее поделиться тем опытом, который позволил запустить это все за 2 недели.
Серьезно! Я не шучу. От первого митинга до запуска проекта — 2 недели!
Неделя на разработку и раскрутку в соц. сетях и неделя на продажи.
В ходе повествования я буду упускать малозначимые (или очевидные, которые можно найти в документациях) детали и делать акцент лишь на определенных тонкостях. Также будут встречаться конфиги и примеры кода.
Кто мы?
Когда начался карантин многие сферы бизнеса начали потихоньку проседать. Это затронуло и сферу Ильи, а именно — «танцы и хореография». Он владеет большим количеством танцевальных школ. Такая же беда настигла и фитнес. И там люди начали выкручиваться путем проведения онлайн тренировок. Так у него появилась идея провести танцевальный лагерь онлайн (по полной аналогии с оффлайн лагерями). С чем он и пришел ко мне.
В конечном итоге команда сформировалась только из 2х людей:
- техническая часть (я)
- маркетинг + управление (Илья)
Если не лукавить, то этот список можно дополнить наемными сотрудниками (дизайнер, смм, переводчик, юрист), но это уже мелочи жизни. Мы это относим к разовым расходам. И эти расходы небольшие.
Что мы делали?
Смысл танцевального лагеря был прост. Есть 12 преподавателей и мероприятие из 3х дней. По 4 класса в день. Необходимо было создать платформу, где пользователь смог бы зарегистрироваться, купить себе доступ в лагерь. Была также информация о преподавателях, лайнап\расписание, некая форма для обратной связи. Ну и самое интересное — необходимо было организовать стриминг артистов, кто также сидит дома на карантине. Почему мы не выбрали решения типа instagram — бан за фоновую музыку. Решения типа Zoom отмели за ненужностью интерактива + слишком дорого + необходимость установки софта.
Отдельный нюанс в том, что преподаватели из Европы и США также находятся на карантине. Это накладывало определенные ограничения на техническую часть.
И еще один нюанс — это должен быть законченный продукт. Люди должны были видеть нечто красивое и понимать, что они покупают, что они получат. Почему им это нужно.
Чего мы хотели?
Мы хотели сделать MVP для запуска первого лагеря и в дальнейшем проводить их еще и еще. Нужно проверить гипотезу, что у людей есть потребность в данном сервисе. И дело не только в карантине. Как взрослые люди мы предъявляли ряд требований к нему:
- Решение должно быть стабильным. Никаких 500-ых ошибок, зависших транзакций. Тикеты в саппорт нам не нужны. Все должно работать без нашего участия
- Решение должно быть гибким и масштабируемым. Сегодня у нас 100 человек. Завтра 10000. Письма должны ходить. Стриминг должен выдерживать любую нагрузку. В случае смены платежного шлюза например, не должно быть временного лага в принятии оплаты
- Интерфейс должен быть понятен как для пользователей, так и преподаватели сидя дома должны суметь организовать стриминг с мобильного телефона
- Мы хотели получить прибыль и в дальнейшем масштабировать наши доходы. Т.о. мы по максимуму минимизировали расходы
Что мы использовали?
- backend на symfony
- frontend на jquery + vue
- упаковка ресурсов через webpack
- очередь nats
- небольшой микросервис на Go для отправки писем
- хранилище Minio для статического контента
- redis для различных подзадач
Панель администратора была сделана с использованием Sonata Admin Bundle. Настраивается этот зверь быстро, удобен в использовании. Не надо тратить время на разработку какой-либо кастомной панели.
Теперь остановимся чуточку подробнее на разных нюансах.
Сразу хочется отметить, что описанные ниже вещи это лишь список рекомендаций (советов), как сделать быстро и недорого. Многое из написанного для кого-то будет очевидным, а для кого-то нет.
Авторизация
Авторизация через SMS это самый большой пылесос для ваших денег. Если вы запускаете стартап — подумайте 10 раз о том, нужно ли оно вам. Представьте, что к вам пришли 1000 пользователей. А теперь умножьте на среднюю цену SMS в Европе\США и добавьте это в себестоимость вашего проекта. Также вы испытаете ряд проблем по согласовании имени отправителя. Да к тому же это еще и долго (в некоторых странах этот процесс длится от 2–3х недель и более). А от стандартного имени отправителя многие операторы сотовой связи блокируют SMS. Мало приятного. Поэтому мы использовали авторизацию через почту\соц. сети.
Очень быстро авторизацию через соц.сети можно интегрировать, добавив в проект библиотеку LexikJWTAuthenticationBundle.
Я более чем уверен, что в других языках и фреймворках также имеются подобные.
Она хорошо документирована и есть различные примеры.
Письма мы сразу стали отсылать через очередь. Я считаю, что даже для маленьких проектов это нужно взять за основу. Делается это очень быстро. Да и кто знает, может через месяц ваш стартап выстрелит и там будет х10 пользователей.
Про NATS есть статьи на хабре в том числе.
Есть готовые библиотеки как для php, так и для Go (и множества других языков)
Т.е. получилась связка PHP → NATS → GO → Amazon SES
При помощи docker-compose устанавливаем NATS:
version: "2"
services:
nats:
container_name: nats-mq
image: nats
networks:
- "back"
ports:
- "127.0.0.1:4222:4222"
- "127.0.0.1:6222:6222"
- "127.0.0.1:8222:8222"
entrypoint: "/gnatsd -c gnatsd.conf --auth SECRET --debug"
restart: always
networks:
back:
driver: "bridge"
Добавляем в composer.json пакет для работы с ним:
...
"repejota/nats": "^0.8.6"
...
Вот пример функции для отправки почты:
public function email($emails, $body, $subject)
{
if (empty($emails)) {
return false;
}
$prefix = getenv('NATS_PREFIX');
$encoder = new JSONEncoder();
$options = new ConnectionOptions([
'token' => getenv('NATS_TOKEN')
]);
$client = new EncodedConnection($options, $encoder);
try {
$client->connect();
$client->publish(
$prefix . 'email',
[
'Emails' => $emails,
'HtmlBody' => $body,
'TextBody' => strip_tags($body),
'Subject' => $subject,
'Sender' => 'info@' . getenv('DOMAIN_NAME')
]
);
} catch (\Exception $exception) {
return false;
}
return true;
}
У себя в проектах я взял за привычку писать множество фабричных методов, и вызывать функцию отправки уже из суперкласса уведомлений.
Это добавляет гибкости. А ведь стартап должен придерживаться этого при разработке.
Еще это удобно для отладки.
Ведь можно добавить некий класс Dummy с функцией:
public function email($emails, $body, $subject)
{
$time = time();
file_put_contents(SRC_PATH . '../var/dummy/email' . $time . '.html', $body);
return true;
}
interface NotificationInterface
{
/**
* Инициализируем нотификатор
*
* init storage
* @void
*/
public function init();
/**
* Синхронные ли уведомления
*
* @return bool
*/
public function isSync();
/**
* Отправляет СМС
*
* @param $phone
* @param $message
* @return bool
*/
public function sms($phone, $message);
/**
* Отправляет письмо
*
* @param $email
* @param $body
* @param $subject
* @return bool
*/
public function email($email, $body, $subject);
}
В Go подписаться на очередь можно примерно так:
opts := nats.Options{
Url: ...,
Token: ...,
}
nc, err := opts.Connect()
if err != nil {
log.Error("error in nats connection", err)
}
c, err := nats.NewEncodedConn(nc, nats.JSON_ENCODER)
if err != nil {
log.Error("error in nats encoding", err)
}
defer c.Close()
defer nc.Close()
c.Subscribe("email", func(p *EmailInput) {
p.sendEmail()
})
объявив перед этим:
type EmailInput struct {
Emails []*string `json:"Emails"`
HtmlBody string `json:"HtmlBody"`
TextBody string `json:"TextBody"`
Subject string `json:"Subject"`
Sender string `json:"Sender"`
}
func (email *EmailInput) sendEmail() {
...
}
Для отправки, как я писал выше, мы использовали Amazon SES.
Почему?
- администрировать свой почтовый сервер очень накладно и это отнимет время
- это готовое решение с хорошим бесплатным лимитом
- легко настраиваются DKIM + SPF (иначе письма будут улетать в спам, конверсия будет падать)
Еще один очень важный нюанс.
Так как у нас не было отдельного отдела поддержки, то мы сразу добавили переадресацию с почты поддержки на наши личные. Это позволило оперативно реагировать на вопросы пользователей.
Теперь ваши пользователи могут БЫСТРО и ЛЕГКО авторизоваться. Самое время получить с них заветные денежки за билеты на ваше мероприятие.
Биллинг
С точки зрения кода система платежей устроена довольно-таки просто. Есть суперкласс для генерации ссылки, и опять же множество фабричных методов. Они умеют сгенерировать ссылку для оплаты, и обработать колбэк. А также запустить пост-продажные хуки (уведомление по почте + добавление доступа к мероприятию).
interface HookInterface
{
/**
* @return string
*/
public function getName();
/**
* @param Transaction $transaction
* @param array $params
*/
public function run(Transaction $transaction, array $params);
}
interface SystemInterface
{
/**
* @return string
*/
public function getName();
/**
* @param Transaction $transaction
* @param string|null $redirect
* @return null
*/
public function createLink(Transaction $transaction, $redirect = null);
/**
* @param Transaction $transaction
* @return string
*/
public function createWidget(Transaction $transaction);
/**
* @return string
*/
public function createWidgetAssets();
/**
* @param $request
* @param $em
* @return PayResponse
*/
public function handleCallback(Request $request, ObjectManager $em);
}
Продукт наш был ориентирован на Европу, поэтому и цены были в Евро.
С какими трудностями же мы столкнулись:
- Можно по пальцам пересчитать платежные шлюзы, которые имеют валютные терминалы. Что это? Это такая штука, которая позволяет принимать оплату именно в ЕВРО, а не рублях. Почему это так важно? Да потому что Европейцы настороженно относятся к Русским, очень много мошенников. Рубли при оплате будут снижать конверсию и добавлять тикеты в саппорт, где вам прийдется объяснять кто вы и что вы.
- Согласование валютного терминала занимает время. Готовьтесь, что это будет от 5 дней и выше. На практике около 10. Потому что вас тщательно проверят, да и в карантин Европейцы работали неохотно. Также вопросы возникнут и со стороны нашего отечественного банка, зачем вам это потребовалось
- 3d secure никто не отменял (SMS, которая приходит с кодом подтверждения оплаты). Поэтому если у клиента в Европе банк не поддерживает эту возможность, то оплатить он УВЫ не сможет. На этот случай прийдется билить клиента ручками на PayPal
- Согласование сайта происходит только тогда, когда весь контент уже присутствует на нем. Т.е. сайт полностью готов, в том числе все юридический документы типа оферты. Т.к. у нас был не интернет-магазин и в целом нетипичное решение — мы обратились к юристам для составления оферты под наш продукт и последующий её перевод
Так получилось, что мы сначала принимали оплаты в рублях. После согласования платежного шлюза — переключились на евро. Это немного снизило конверсию на старте.
Организация стриминга
Если с авторизацией все просто, да и биллинг можно своими силами прикрутить. Такие задачи часто встречаются у средне-статистического разработчика. То вот со стримингом все не так однозначно, как кажется на первый взгляд. Что же не так?
- Мы никогда не имели с ним дело. Нужен был сервер + клиент для преподавателей
- Картинку необходимо зеркалить, т.е. делать вертикальное отображение (особенности танцевальных видео)
- Мы точно не знали сколько будет участников в лагере. И покупать где-либо (в готовых стриминговых решениях) аккаунт на месяц\год\кол-во трафика мы пока не могли. Это сожрало бы всю прибыль
- Нужна была защита iframe от воровства контента хотя бы по рефереру
В качестве решения мы выбрали связку larix broadcastr app → nginx + rtmp → cdn → client
В ходе поиска, приложение Larix Broadcaster для вещания RTMP с мобильных устройств оказалось самым надежным и стабильным. На отдельном маленьком виртуальном сервере мы подняли nginx + rtmp для рестриминга дальше в CDN.
Данная схема является самой дешевой. Намного дешевле, чем пользоваться услугами готовых стриминговых платформ. У вас нет необходимости покупать какой либо премиум\голд\супер аккаунт на месяц\год. Вы платите только за потраченный трафик. Это было идеальным решением в нашей ситуации.
Там же и разворачивали зеркально картинку.
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
include _root.conf;
include _stat.conf;
}
include _rtmp.conf;
server {
listen 80;
server_name localhost;
location / {
root html;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
server {
listen 8080;
auth_basic "Restricted Area";
auth_basic_user_file .htpasswd;
location / {
root www;
}
location /stat {
rtmp_stat all;
rtmp_stat_stylesheet stat.xsl;
}
location /stat.xsl {
root www;
}
}
rtmp {
server {
listen 1935;
chunk_size 4096;
application your-custom-hash-here {
live on;
record off;
allow publish all;
allow play all;
push rtmp://localhost/cdn;
}
application cdn {
allow publish 127.0.0.1;
deny publish all;
live on;
record off;
exec ffmpeg -i rtmp://localhost/cdn/$name -vf "hflip" -crf 30 -preset ultrafast -acodec aac -strict experimental -ar 44100 -ac 2 -b:a 128k -vcodec libx264 -x264-params keyint=60:no-scenecut=1 -r 30 -b:v 2000k -f flv rtmp://rtmp.cdnhost.abc;
}
}
}
На фронт части мы же использовали простой html5 плеер, сконфигурированный с учетом мобильных устройств и их особенностей:
videojs($('#video')[0], {
controls: true,
autoplay: false,
preload: 'auto',
poster: '/static/splash-en.jpg',
language: 'en',
muted: true,
html5: {
hls: {
overrideNative: !videojs.browser.IS_SAFARI
}
}
});
Решение оказалось очень стабильным и удобным.
После мероприятия мы подсчитали процент жалоб на качество — это были 2%. И в основном люди, у которых старые стационарные компьютеры или устаревшее ПО (браузеры).
Заключение
Что же в итоге?
Мы провели 2 онлайн лагеря.
Получили только положительный фидбэк от первых пользователей.
В тяжелое время карантина это было именно то, что нужно людям дома.
Общая выручка составила более 1.000.000 рублей.
Если немножечко поговорить о кастдеве, то ориентируясь на фидбэк первого лагеря, мы доработали систему
- Сделали возможность покупки билета на один день (а не только полный билет) — это подняло выручку, т.к. многие не хотели покупать полный билет, а хотели купить лишь мастер-класс одного или двух преподавателей
- Сделали простой чат около видео-плеера на socket.io — это добавило интерактива в уроки и позволяло получить обратную связь в реальном времени
- Т.к. лагерь международный и многие люди путались в часовых поясах — мы добавили возможность изменять таймзону в расписании и смотреть расписание по своему часовому поясу
Какие выводы можно сделать:
- Не обязательно иметь стартовые инвестиции и огромную команду, чтобы проверить идею
- Ведите разработку гибко, прислушивайтесь к вашим пользователям. Добавляйте только то, что реально нужно людям
- Не пишите сразу огромный монолит с кучей функций, он не нужен для проверки гипотезы. Не копите тех. долг. Умейте находить баланс между «плохим» кодом, и «избыточной» функциональностью. Используйте каждый инструмент по назначению
- Используйте опыт своих знакомых. Большинство задач ведь уже решено. Достаточно лишь найти нужные инструменты, провести исследования, объединить их в единую систему
Надеюсь, что данная статья кому-нибудь покажется полезной, а кого-нибудь мотивирует.