Всем привет! Пишу ознакомительную статью о моем творении и как работают супераппы, если возникнут вопросы, обращайтесь в коменты.
Первый шаг в бездну
Начну с вопроса: «что такое этот ваш суперапп?» и уже дальше опишу принцип работы и что в итоге получилось воссоздать за 8 месяцев непрерывного кодинга. Суперапп — это объединение двух и более функционалов нескольких сервисов в одно единое приложение или веб‑приложение. Проще говоря, его задача избавить обычного юзера интернета от лишних вкладок на компе и сделать все более централизованным. Хотя тут скорее зависит от того, на какой суперапп мы смотрим.
Архитектура подобных проектов всегда упирается в четыре важные вещи: бэкенд, фронтенд, сервер и защита всего этого. От сюда уже идут другие, не менее важные нюансы, которые следует упомянуть. А что вообще совмещать? Мой ответ, на такой вопрос прост и банален: Маркет и Мессенджер — что я и сделал. Звучит нереально, однако это реально, но крайне трудно, если у вас нет опыта и идеи как все это можно организовать. Хотя пожалуй самый важный аспект это безопасность и защита от IDOR атак. Но и они в этом сценарии отлетают, так как их крайне легко отражать проверками на уровне сервера.
Вот банальный пример, который гарантирует защиту, но при этом является одним из простейших защитных методов. Предположим, у нас есть WebSocketManager с обработкой подключений в consumers.py. Там реализован универсальный базовый метод connect. Для подключения пользователя к беспрерывному соединению с чатом как раз на этом уровне следует внедрять проверку безопасности. Делается это следующим образом:
@database_sync_to_async
def is_user_in_chat(self, user, chat_id):
# Проверяем, существует ли чат и входит ли в него данный юзер
chat = Chat.objects.filter(pk=chat_id, participants=user).exists()
if chat:
return chat
else:
chat = Chat.objects.filter(pk=chat_id, subscribers=user).exists()
return chat
# 1. Подключение
async def connect(self):
try:
# Получаем chat_id с проверкой
self.chat_id = str(self.scope['url_route']['kwargs']['chat_id'])
self.room_group_name = f"chat_{self.chat_id}"
self.user = self.scope["user"]
# (Безопасность соединения!)
# Проверка если юзер не аутентифицирован никак в чате:
user_has_access = await self.is_user_in_chat(self.user, self.chat_id)
if not user_has_access:
logger.warning(f"User {self.user.id} tried to access chat {self.chat_id} without permission!")
await self.close(code=4403) # Закрываем соединение (Forbidden)
return
# остальные проверки
# ...Таким образом происходит четкая валидация пользователей в каждом чате, следовательно утечка данных полностью пресекается, а использование проекта становится абсолютно безопасным.

Проект в вебе, поэтому стек у меня банальный для такого масштаба: Django на бэкенде (Python) и JavaScript на фронтенде. Как я уже упоминал, на разработку ушло 8 месяцев, и всё это время задачи были четко распределены по этапам.
Первые два месяца ушли на верстку самого сайта — дизайн, архитектуру фронтенда и базовую функциональность кнопок. В этот срок удалось уложиться идеально. По сути, так же гладко прошли и остальные этапы, кроме интеграции коммерческого API. Тут пришлось провести тотальную работу, чтобы соответствовать стандартам платежных систем и логистических сервисов.
Второй шаг в бездну — погружение
Следующим этапом (с третьего по пятый месяц) стало создание своего API и синхронизация базы данных. Пожалуй, это самое захватывающее и безусловно самое важное. Задача заключалась в построении единой системы защиты. Данные, передаваемые между пользователями, ясное дело, должны как-то шифроваться и быть недоступны посторонним.

Тут обычно есть два пути: либо использовать сквозное шифрование (E2EE), либо защищать данные в движении с помощью TLS‑протоколов, а на самом сервере шифровать их при хранении в базе данных с использованием секретных ключей. Выбор почти у всех всегда один — второй.
Это объясняется тем, что так проще управлять данными и реализовывать привычный бэкенд-функционал. Ведь в случае с E2EE данные становятся абсолютно «слепыми» для сервера. Мне кажется, что сквозной шифр эффективно подошел бы только для классического, изолированного чата без каких-либо надстроек и сложной бизнес-логики.
В качестве примера второго варианта шифрования (на стороне сервера) можно взять специализированный класс кастомного поля для моделей Django. Он перехватывает текстовые данные при записи в базу и автоматически расшифровывает их при чтении, гарантируя защиту данных «из коробки»:
class EncryptedTextField(models.TextField):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._cipher = None
@property
def cipher(self):
if self._cipher is None:
try:
from cryptography.fernet import Fernet
if settings.ENCRYPTION_KEY:
key = settings.ENCRYPTION_KEY.encode()
self._cipher = Fernet(key)
else:
logger.warning("ENCRYPTION_KEY not set, encryption disabled")
except Exception as e:
logger.error(f"Failed to initialize cipher: {e}")
return self._cipher
def from_db_value(self, value, expression, connection):
try:
encrypted_bytes = base64.b64decode(value)
decrypted_bytes = self.cipher.decrypt(encrypted_bytes)
return decrypted_bytes.decode('utf-8')
except (base64.binascii.Error, ValueError):
return value
except Exception as e:
# Ошибка расшифровки
logger.error(f"Decryption error: {e}")
return f"[DECRYPT_ERROR: {value[:30]}...]"
def get_prep_value(self, value):
if value is None:
return None
# Если значение уже зашифровано или это ошибка
if isinstance(value, str) and (value.startswith('gAAAAA') or value.startswith('[DECRYPT_ERROR:')):
return value
try:
# Шифруем
encrypted_bytes = self.cipher.encrypt(value.encode('utf-8'))
return base64.b64encode(encrypted_bytes).decode('utf-8')
except Exception as e:
logger.error(f"Encryption error: {e}")
return value
После чего этот класс можно спокойно использовать для текстового поля у сообщений, заменив им базовый CharField. Что гарантирует безопасность и надежность для пользователей.
Взять тот же поиск или фильтрацию: если данные зашифрованы на клиенте, реализовать их силами сервера или силами админа в БД становится технически невозможно. Именно поэтому техпроекты со сложной архитектурой выбирают второй способ — шифрование на стороне сервера (At Rest), чтобы сохранить контроль над собственной архитектурой. Безусловно, E2EE обеспечивает максимальную приватность, но платить за неё приходится огромным усложнением разработки и потерей гибкости.

Ну и, пожалуй, самое приятное — это логика работы Маркета. Здесь разработка шла по довольно понятному сценарию, который можно описать классическими методами записи и выборки данных из БД. Всё устроено прозрачно: на бэкенде отрабатывают триггеры и проверки, определяющие, будет ли опубликован конкретный товар и соответствует ли он правилам платформы.
Намного интереснее устроены алгоритмы ранжирования, или, проще говоря, лента листингов. Для её формирования нужен полноценный перебор и скоринг данных. Система начисляет определенные баллы за каждый кейс: например, учитывается дата публикации, (триггер для моментального взлета листинга вверх ), количество просмотров, количество лайков, количество заказов на объявление. На основе финального веса товара Django формирует персональную выдачу для каждого пользователя в реальном времени.
Пример такой работы можно представить в виде выделенной функции, которая обрабатывает каждый запрос по листингам и возвращает отсортированный кверисет с использованием встроенных инструментов агрегации Django ORM:
def get_listings(search_query=None):
base_qs = Listing.objects.filter(
delete_status=False,
is_available=True,
successfull_flag=True,
).select_related('category').only(
'name', 'description', 'type_listing', 'state_listing',
'address', 'price_listing', 'sum_listing', 'delivery_state',
'rating', 'model_type', 'created_at', 'category__name'
)
# Параметры алгоритма
new_listing_boost_hours = 1
base_rating_weight = 1.0
newness_weight = 2.0
def annotate_listings(queryset):
return queryset.annotate(
hours_since_creation=ExpressionWrapper(
ExtractHour(timezone.now() - F('created_at')),
output_field=FloatField()
),
is_new=Case(
When(hours_since_creation__lt=new_listing_boost_hours,
then=Value(newness_weight)),
default=Value(0),
output_field=FloatField()
),
weighted_score=ExpressionWrapper(
F('rating') * Value(base_rating_weight) +
F('is_new') * (Value(newness_weight) - F('hours_since_creation')/Value(new_listing_boost_hours)),
output_field=FloatField()
)
)
result = list(annotate_listings(base_qs).order_by('-weighted_score', '-created_at'))
return resultТретий шаг в бездну — всплытие
После разбора основных технических нюансов нельзя не упомянуть, что я разрабатывал весь проект в соло. Поэтому мои рассуждения и технические придирки сформулированы только на личном опыте. Я всегда буду рад услышать любой комментарий в свой адрес и ознакомиться с мнением, так сказать, народа и читателей данной статьи.
Супераппом уже можно воспользоваться и ознакомиться лично: vendergram.com. Это бета‑версия, поэтому я готов к любой конструктивной критике в свой адрес.






















