🏡 KristinaHouse · batume.li#

Региональный портал Батуми — маркет, в котором семейное агентство недвижимости лишь один из
продавцов. Технологии — чтобы доверять прозрачно.

flowchart LR E["KristinaHouse Engine<br/>~42 000 строк · 1 зависимость<br/>0 фреймворков · 100% контроль"] E --> W["batume.li (портал)"] E --> N["Сайты-сателлиты (один код)"] E --> T["Telegram-боты (реестр)"]

3 секунды — и сайт работает#

Один файл. Одна команда. Никаких npm install-ритуалов, контейнеров и DevOps.

$ bun run server.ts
✓ SQLite готов
✓ TLS: batume.li (let's encrypt)
✓ Telegram-боты слушают
✓ Курсы валют загружены
✓ Сервер слушает :3000

Стек, который удивляет: Bun (JavaScript/TypeScript runtime) — SQLite (WAL mode) — один исполняемый файл server.ts, в котором живёт всё: роутинг, бизнес-логика, API, авторизация, SSR-рендеринг, обработка изображений, аналитика и интеграции.


Архитектура — без единого фреймворка#

flowchart TB B["Браузер<br/>RU · EN · KA"] subgraph VPS["Contabo VPS (5.189.164.87)"] S["Bun.serve (:3000)"] subgraph Files["Файлы"] direction LR F1["server.ts"] F2["db.ts"] F3["tg.ts"] F4["sync.ts"] F5["urns.ts"] F6["i18n.ts · ai.ts"] F7["qr.ts · rates.ts"] end D["SQLite WAL<br/>kh.sqlite (~2 МБ)"] S --- Files Files --- D end B -- HTTPS --> S S -- SSR HTML --> B

Почему это важно для вас:


Один код — несколько сайтов (мульти-URN)#

Реестр адресов проекта живёт прямо в этом README: шапка задаёт основной urn, список внизу — все зеркала в порядке приоритета. Сервер при старте сам находит свои ключи let's encrypt и понимает, «кто он». Файл urn-config.json настраивает каждый urn один раз — canonical, акцент-город, разделы, синхронизацию и токены — локально, до деплоя.

flowchart LR RM["README.md"] --> URN["batume.li · batumi.su<br/>(реестр urn)"] LE["let's encrypt"] --> ID["я — batumi.su<br/>(автоопределение)"] UC["urn-config.json"] --> CONF["сателлит, город, токен, интервал<br/>(настроено один раз, применяется само)"]

3 языка · 3 страны · 0 компромиссов#

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

ЯзыкИнтерфейсКонтентУведомления
RU
EN
KA

Возможности — 25 модулей в одной системе#

МодульСтатусДля кого
🛒 Маркет — разделы, лоты, корзина☑ АктивенПродавцы + покупатели
🎛 Конструктор — вид всего сайта за одной дверью☑ АктивенХозяева разделов
📃 Страницы из блоков — главная просто одна из них☑ АктивенАдминистратор
📐 Схемы — залы и этажи в метрах, как в Visio☑ АктивенОрганизаторы + продавцы
🗞 Лента — твиты, треды и ролики одним потоком☑ АктивенВсе
👤 Личные страницы /~логин с секретными версиями☑ АктивенПользователи
🔐 Документы со сквозным шифрованием◧ Модель и ручкиУчастники доски
🏠 Недвижимость — раздел, а не подсистема☑ АктивенГости
📅 Брони + календарь цен☑ АктивенГости + продавцы
🗺 Своя карта — тайлы OSM рисуем сами☑ АктивенВсе
📊 Кабинет продавца: отчёты, лоты, инструменты☑ АктивенПродавцы
📝 Блог☑ АктивенЧитатели
💬 Форум☑ АктивенВсе пользователи
🎉 Мероприятия + приглашения☑ АктивенОрганизаторы + гости
🤖 Telegram-боты (реестр, мультибот)☑ АктивенГости + продавцы
🧠 ИИ — пул ключей, мультивендор☑ АктивенРедакция + боты
🔐 Авторизация☑ АктивенПользователи
💱 Курсы валют☑ АктивенСистема (авто)
📱 QR-коды лотов☑ АктивенПродвижение
🌐 Синхронизация (донор → сателлиты)☑ АктивенАдминистратор
📄 Документы☑ АктивенГости
⚙️ Панель управления☑ АктивенМенеджеры
📈 Аналитика посещений☑ АктивенМенеджеры
⚖️ Правовой профиль (GE · EU-GDPR · без ПД)☑ АктивенПосетители + админ
🍪 Cookie-баннер☑ АктивенПосетители (GDPR)

Каждый раздел выключается одним тумблером в панели — per-urn: донор может держать блог открытым, а сателлит — только читаемым (вход лишь для штата) или вовсе без блога.

Маркет-платформа, где недвижимость — просто раздел#

Сайт — это маркет + блог + форум, а KristinaHouse закреплён продавцом одного раздела в нём.
Недвижимость перестала быть подсистемой: квартира — обычный лот раздела «Недвижимость», а
всё, что раньше было зашито под неё, — календарь занятости, сезоны цен, заявки на бронь,
помещения, адрес — стало блоками конструктора интерфейсов, которые может объявить любой
раздел. Включите блок в «Коттеджах» — и у их продавцов появится тот же календарь; выключите —
и он исчезнет без единой строки кода.

Витрины /property и зеркальных объявлений больше нет — оба сняты сознательно. Адрес лота
теперь ссылка на дом в гео-справочнике, поэтому правка дома один раз исправляет все лоты в
нём. Основная навигация — Маркет · Статьи · Форум; главная собирается из конструктора
блоков, где каждый блок прячет себя сам, если под ним нет данных.

Главная — просто страница. Строка в таблице с флагом, собранная тем же конструктором, что и
любая другая: те же блоки, тот же предпросмотр, та же оболочка. У страницы ровно один
канонический адрес (/p/слаг или /слаг, если забрать верхний уровень; второй отвечает 301), а
состав можно форкнуть на язык — там, где грузинскому гостю нужен другой набор полос, а не
переведённая подпись. Каждая публичная подпись блока — заголовок, подзаголовок, надпись на
кнопке — живёт на трёх языках, поэтому перевод страницы редко означает её форк.

Блоки складываются в плоскости. Полоса идёт во всю ширину, режется на колонки, в колонке —
стопка блоков; ширины выбираются словами («шире слева», «узкая справа»), а не числами. До этого
страница умела только «одно под другим», и каждая новая раскладка стоила отдельного блока в
библиотеке — библиотека росла от вёрстки, а не от новых видов содержимого. Собирается всё в
одном месте: пункт «🎛 Конструктор» стои́т рядом с «Обзором» и перечисляет не подсистемы, а
живые страницы и разделы — главную, «о нас», /estate. Раздел маркета правится как раздел, а
не как шаблон: рядом с ним написано, общий у него вид с соседями или свой, и одной кнопкой вид
можно отделить — дальше правки достанутся только ему.

Отделы доски — тоже данные. /market, /bazaar, /gigs и /jobs — фильтрованные виды над
теми же разделами, и завести свой можно из панели. Закрытой осталась единица показа: отдел
выбирает одну из четырёх, которые умеет рендер (лот · лавка · исполнитель · вакансия), и именно
она решает, в каком блоке главной он появится. Открой обе оси — и конструктор начнёт обещать вид,
которого нет.

Любой продавец — не только магазин сайта — получает ту же глубину: **конструктор
витрины профиля (темы/цвета/блоки, без чужого CSS/JS), корзину и заказы**
(товарный режим с остатками), категории с атрибутами и точечным доступом и
кабинет продавца с мини-учётом уровня «отчёт владельцу»: выручка по периодам,
статистика просмотров, CSV-экспорт и ссылки-отчёты без логина.


Технологии, которые работают на вас#

⚡ Производительность#

МетрикаПоказатель
Старт сервера~300 мс
Загрузка каталога<100 мс
Конвертация фото~50 мс/фото
Водяной знаккэшируется
Сессия пользователя30 дней
Лимит POST-запросов10/мин на IP
Максимум загрузкидо 15 МБ
Размер базы~2 МБ

🔒 Безопасность#

ФункцияМетод
Аутентификацияbcrypt + httpOnly cookies
Утечка данныхAPI всегда скрывает данные владельца
Доступ по ссылкетокен-отчёты (не требуют входа)
Защита от XSSэкранирование Unicode в JSON
Rate limiting10 запросов/мин на публичные POST
GEO-блокировкаопционально (по IP/стране)
Аналитикатолько агрегаты, без хранения IP
Документысквозное шифрование: сервер видит только шифротекст
Секретные версии профиляадрес после # до сервера не доходит — его нет ни в логах, ни в Referer, ни в индексе
Приватность резюмепо умолчанию резюме безымянно; имя открывает сам автор, и больше никто
Публичный каталог людейтолько по согласию, и проверяется оно ВНУТРИ выборки, а не вокруг неё

📈 Аналитика — без слежки#

Панель показывает посещения по дням: визиты, уникальные, mobile / desktop, семейства user-agent (iPhone, Android, Windows, macOS, Linux), боты — отдельной строкой. В базе — только суточные счётчики: ни IP-адресов, ни сырых логов.

🤖 Telegram — бронирования без сайта#

Гость пишет в Telegram-бот → видит свободные объекты → бронирует → получает контакты после подтверждения менеджером. Никакого сайта, никакой регистрации — просто чат.

sequenceDiagram participant G as Telegram Guest participant M as Telegram Manager G->>M: Хочу квартиру на 3 дня (booking) M-->>G: ✅ / ❌ M->>G: контакты

🧠 AI — редактор и автор контента#

Редактор пересылает в Telegram-бот статью → AI проверяет качество, категоризирует (туризм/новости/законы), переписывает, генерирует заголовок, slug и предложения по связанным статьям. Порог качества настраивается (по умолч. 6/10).

flowchart LR A["Статья<br/>Текст"] --> P["AI Pipeline<br/>Оценка (0–10)<br/>Категоризация<br/>Переписывание<br/>Slug + ссылки"] P --> O["На выходе<br/>Черновик в блоге"]

✍️ BBCode и markdown — диаграммы, код и формулы#

Статьи, посты форума, объявления и документы используют один безопасный движок разметки (md.js — один и тот же файл на сервере и в браузере, поэтому превью в редакторе и опубликованная страница не могут разойтись). Всё экранируется до разметки: никакого сырого HTML, ссылок javascript: и XSS. Разметка — BBCode ∪ markdown: классика (заголовки, списки, GFM-таблицы, цитаты, картинки) плюс:

Тяжёлые рендереры (mermaid, highlight.js, KaTeX) грузятся лениво с CDN и только если на странице реально есть такой блок — обычная текстовая статья не тянет ни одного лишнего байта, а у сервера по-прежнему одна зависимость.

💰 Прозрачная отчётность для собственников#

Каждый владелец квартиры получает ссылку-токен — без логина и пароля видит:

Никаких «потеряли бумажку». Всё онлайн, всё прозрачно.

📱 QR-код на каждом объекте#

У каждого объявления — уникальный QR-код (SVG, уровень коррекции H — 30%, переживёт даже наклейку на центр). Генерируется на сервере за микросекунды. Цвета — из тегов объекта.

flowchart LR QR["📸 QR-код"] --> SITE["batume.li/property/apartment-..."] SITE --> VIEW["Фото, цена, свободные даты"]

💧 Водяной знак — картинкой или текстом#

Фото объектов защищаются на лету: PNG-логотип или текст (размер, жирность, курсив, цвет, шрифт — всё настраивается в панели), плюс опция grayscale — посетители видят чёрно-белые фото, оригиналы доступны только менеджерам объекта. Результат кэшируется.

🔎 SEO — которое видно поисковику, а не только вам#

Карта сайта собирается на каждый запрос и кладёт в индекс сами объекты: лоты, витрины продавцов и их страницы, жилые комплексы, гео-лендинги. Каждая ссылка идёт с hreflang-альтернативами по трём языкам, а зовётся ровно тем адресом, который страница объявляет каноническим, — обход не тратится на редиректы. Спрятали раздел настройкой — его товар уходит из карты вместе с ним; продавец переехал на свой домен — его страницы уходят в его собственную карту, а не дублируются в портальной.

Гео-лендинги/market/in/грузия/батуми/cat/недвижимость: страница на пересечение места и типа, с перелинковкой между соседними городами. Пары строятся по фактическому наличию объектов: страницы «ни одного объявления» не заводятся вовсе.

🖼 Карточка репоста — рисуется сама#

Ссылка без картинки в ленте выглядит как битая. Если своего фото у страницы нет — у гео-лендинга его не бывает по построению, у нового бренда может не быть, — сервер рисует карточку 1200×630 сам: имя, место, цифра, подпись сайта, свой цвет на каждый вид страницы. Ноль внешних запросов и ноль дополнительных зависимостей: SVG собирается строкой, растрирует его тот же sharp, что делает водяные знаки. Переименовали организацию — картинка перерисовывается, ссылка остаётся прежней.

💱 Курсы валют — всегда актуальные#

4 раза в сутки (00/06/12/18) сервер обновляет курсы GEL/USD/RUB/EUR. Если API недоступен — продолжают работать старые курсы. Цены в объявлениях всегда корректны, даже при скачках валют.


Синхронизация — двухинстансная архитектура#

Данные разделены на два уровня: Donor (полные данные, включая персональные) и Satellite (только публичный контент, без данных владельцев и учётных записей гостей). Позволяет соблюдать требования к хранению данных в разных юрисдикциях. Сателлит делится ссылками на себя; канонический адрес живёт только в meta-тегах для поисковиков.

flowchart LR subgraph Donor["Donor (основной)"] D1["Все данные"] D2["Владельцы + учёт"] D3["Пользователи"] D4["Чёрный список"] end subgraph Satellite["Satellite (копия)"] S1["Публичный контент"] S2["Нет персональных данных"] S3["Только менеджеры"] S4["(вход для штата)"] end Donor -- sync --> Satellite

Стек — почему это high-tech#

КомпонентТехнологияПочему
ЯзыкTypeScriptТипобезопасность без оверхеда
RuntimeBunВ 4× быстрее Node.js, встроенный SQLite, тестер, bundler
База данныхSQLite WALНоль администрирования, ~2 МБ, 100% ACID
ФреймворкнетСобственный SSR — полный контроль, нулевой bloat
Зависимости1 (sharp)Минимальная поверхность атаки
Обработка фотоsharp + mozjpegJPEG 85%, водяной знак (PNG/текст/ч-б), EXIF-ориентация
QRСобственный GF(256)Reed-Solomon level H, без внешних API
AIOpenAI-совместимыйЛюбой провайдер (OpenAI, Anthropic, локальный Ollama)
TelegramLong-polling (2 бота)Без webhook, ничего не торчит наружу
TLSLet's EncryptАвтовыбор сертификата по реестру urn из README
Мульти-сайтurn-config.jsonОдин код+БД → несколько доменов, настройка один раз
Резервное копированиеcp kh.sqlite backup/Одна команда

Цифры, которые говорят сами за себя#

МетрикаПоказатель
📊Строк TypeScript~42 000 — 50 исходных файлов
🧪Тестов54 файла — плюс 6 сквозных приёмок на живом сервере
📦Зависимостей1 — sharp (остальное — своё)
🗄️База данных~2 МБ — SQLite, без сервера
🚀Старт~300 мс — от команды до запроса
🌍Языков3 — RU, EN, KA
🗺Тайлы картырисуем сами — 0 внешних запросов
📸Лимит фото15 МБ на загрузку
🔄Обновление курсов4 раза в сутки
🤖Telegram-ботовреестр — несколько на тип и на домен
🧠Порог AI6/10 — настраиваемый
⏱️Сессия30 дней — без перелогинов
🛡️Коррекция QR30% — уровень H (Reed-Solomon)

UX/CX — почему гости возвращаются#

СценарийВпечатление
Нашёл квартиру за 5 секФильтры: тип, цена, даты, комнаты
Листает фото на телефонеСвайпы не срабатывают от скролла — только явный жест
Увидел реальные фотоВодяной знак — защита, не мешает
Забронировал в TelegramБез регистрации на сайте
Получил контактПосле подтверждения менеджером
Собственник: отчётПо ссылке, без входа
Читает статьи о БатумиAI-качество, 3 языка
Покупает/продаётМаркетплейс с FTS5-поиском
Общается на форумеАнонимно (с TTL), без страха
Переключает язык1 клик — RU/EN/KA

Customer Experience (CX) — это когда система не мешает, а помогает. KristinaHouse спроектирована так, чтобы гость тратил время на выбор квартиры, а не на разбирательство с сайтом.


Для владельца бизнеса#

KristinaHouse — это не CRM, не WordPress, не конструктор. Это собственная высокопроизводительная платформа, написанная специально под модель семейного агентства в Батуми.


Источники данных#

Гео-справочник портала — страна, край, город, улица, дом — наполняется из OpenStreetMap
через Overpass API. Импортированные строки помечены происхождением: правка человеком выводит
строку из-под будущих прогонов, и её исправление живёт вечно.

Оттуда же приезжает геометрия для карты — и подложку сайт рисует сам, своим рендером из
собственной базы контуров. Ни одного обращения к чужому тайл-серверу при показе карты не
происходит: карта работает и без интернета.

Данные © OpenStreetMap contributors, лицензия ODbL.
Оформление карты — собственное.


KristinaHouse — Batumi, Georgia.
batume.li · batumi.su
«Мы сами здесь живём, лично проверяем каждую квартиру и ведём прозрачную отчётность для собственников.»