Menu

🏡 KristinaHouse · batume.li#

A regional portal for Batumi — a marketplace where a family-run real estate agency is one of
the sellers. Technology built for transparent trust.

flowchart LR E["KristinaHouse Engine<br/>~42 000 lines · 1 dep<br/>0 frameworks · 100% control"] E --> W["batume.li (portal)"] E --> N["Satellite sites (one codebase)"] E --> T["Telegram bots (registry)"]

3 seconds — and the site is live#

One file. One command. No npm install rituals, no containers, no DevOps.

$ bun run server.ts
✓ SQLite ready
✓ TLS: batume.li (let's encrypt)
✓ Telegram bots listening
✓ Currency rates loaded
✓ Server listening on :3000

A stack that surprises: Bun (JavaScript/TypeScript runtime) — SQLite (WAL mode) — a single executable server.ts that contains everything: routing, business logic, API, auth, SSR rendering, image processing, analytics and integrations.


Architecture — not a single framework#

flowchart TB B["Browser<br/>RU · EN · KA"] subgraph VPS["Contabo VPS (5.189.164.87)"] S["Bun.serve (:3000)"] subgraph Files["Source 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"] F8["builder.ts — one designer core"] F9["pages.ts · profile.ts"] end D["SQLite WAL<br/>kh.sqlite (~2 MB)"] S --- Files Files --- D end B -- HTTPS --> S S -- SSR HTML --> B

Why this matters to you:


One codebase — several sites (multi-URN)#

The project's address registry lives right in this README: the header defines the primary urn, the list at the bottom — every mirror in priority order. On startup the server finds its own let's encrypt keys and understands "who it is". The urn-config.json file configures each urn once — canonical, accent city, sections, sync and tokens — locally, before deploy.

flowchart LR RM["README.md"] --> URN["batume.li · batumi.su<br/>(urn registry)"] LE["let's encrypt"] --> ID["I am batumi.su<br/>(self-detection)"] UC["urn-config.json"] --> CONF["satellite, city, token, interval<br/>(configured once, applies itself)"]

3 languages · 3 countries · 0 compromises#

The system speaks to guests in their language — Russian, English, Georgian — switchable in one click. Translations live in a single dictionary that managers edit right in the control panel.

LangInterfaceContentNotifications
RU✅✅✅
EN✅✅✅
KA✅✅✅

Features — 28 modules in one system#

ModuleStatusFor whom
🛒 Marketplace — sections, lots, cart☑ ActiveSellers + buyers
🎛 Designer — the whole site's look behind one door☑ ActiveSection owners
📃 Pages from blocks — the home page is just one of them☑ ActiveAdministrator
📐 Plans — halls and floors in metres, drawn like in Visio☑ ActiveOrganisers + sellers
🗞 Feed — short posts, threads and clips in one stream☑ ActiveEveryone
🔗 Bonds — a shared name instead of a friend list: type the same id and you have met☑ ActiveUsers
👤 Personal pages /~login with secret versions☑ ActiveUsers
🔐 End-to-end encrypted documents — spreadsheets, a safe, contract templates☑ ActiveBoard participants
💬 Chat — personal (end-to-end) and business conversations, one button with quick actions☑ ActiveEveryone signed in
🔗 Share links with a scope — the guest gets a slice, cut and encrypted in your browser☑ ActiveOwners + their accountants
📄 PDF export — our own writer, fonts embedded, Georgian included☑ ActiveEveryone with a document
🏠 Real estate — a section, not a subsystem☑ ActiveGuests
📅 Bookings + price calendar☑ ActiveGuests + sellers
🗺 Own map — locally rendered OSM tiles☑ ActiveEveryone
📊 Seller cabinet: reports, lots, tools☑ ActiveSellers
📝 Blog☑ ActiveReaders
💬 Forum☑ ActiveAll users
🎉 Events + invitations☑ ActiveOrganisers + guests
🤖 Telegram bots (registry, multi-bot)☑ ActiveGuests + sellers
🧠 AI — key pool, multi-vendor☑ ActiveEditorial + bots
🔐 Authentication☑ ActiveUsers
💱 Currency rates☑ ActiveSystem (auto)
📱 QR codes for lots☑ ActivePromotion
🌐 Synchronization (donor → satellites)☑ ActiveAdministrator
📄 Documents☑ ActiveGuests
⚙️ Control panel☑ ActiveManagers
📈 Visit analytics☑ ActiveManagers
⚖️ Legal profile (GE · EU-GDPR · no-data)☑ ActiveVisitors + admin
🍪 Cookie banner☑ ActiveVisitors (GDPR)

Every section toggles with one switch in the panel — per urn: the donor can keep the blog open while a satellite keeps it read-only (staff-only sign-in) or disables it entirely.

A market platform where real estate is just a section#

The site is a market + blog + forum, and KristinaHouse is a pinned seller of one section
in it. Real estate is not a subsystem any more: an apartment is an ordinary lot of the
"Real estate" section, and everything that used to be hard-wired for it — the availability
calendar, price seasons, booking requests, rooms, address — became **blocks of the interface
designer** that any section may declare. Turn a block on for "Cottages" and its sellers get the
same calendar; turn it off and it disappears without a line of code.

There is no /property storefront and no mirror listings: both were removed on purpose. A lot's
address is a reference to a building in the geo directory, so correcting a house once
corrects every lot in it. Primary navigation is Market · Articles · Forum; the home page is
assembled from a block designer where every block hides itself when it has no data.

The home page is just a page. It is a row in pages with a flag, built by the same designer
as any other one you make — same blocks, same preview, same shell. A page has exactly one
canonical address (/p/slug, or /slug if you claim the top level; the other one answers 301),
and its composition can be forked per language when the Georgian audience needs a different
set of stripes rather than a translated caption. Every public caption a block carries — heading,
subheading, button text — lives in three languages, so translating a page rarely means forking it.

Blocks stack into planes. A stripe runs the full width, splits into columns, and a column
holds a stack of blocks; widths are picked by name («wider on the left», «narrow on the right»),
not by numbers. Before that a page could only do «one below another», and every new layout cost a
separate block in the library — the library grew from markup rather than from new kinds of
content. It is all assembled in one place: the «🎛 Designer» item sits next to «Overview» and
lists live pages and sections — the home page, «about us», /estate — rather than subsystems. A
marketplace section is edited as a section, not as a template: next to it you read whether its
look is shared with neighbours or its own, and one button splits it off so further edits reach
only that section.

Board departments are data too. /market, /bazaar, /gigs and /jobs are filtered views
over the same sections, and you can add your own. What stays closed is the unit of display:
a department picks one of the four the renderer knows (lot · stall · contractor · vacancy), and
that choice alone decides which home-page block shows it. Open both axes and the builder starts
promising a layout that does not exist.

The board is not one feed. /board groups by department, because «by price» across a lot, a
stall, a contractor and a vacancy means nothing; a department page is the same builder blocks as
the home page. Vacancies, CVs and customers' tasks come as rows — salary range, format,
experience, number of responses — while a contractor is a card: trade, price with its scale
(«from 40 ₾ · per hour»), reputation, languages and «Invite to a task» — a chat bound to your own
task, with no new «invitation» entity. The price filter appears after a department is chosen:
there is no single price across four scales. Numbers get a block of their own — «🔢 Listing count»:
a group of live counters under their own filters, each linked only to a page that shows the same
number; otherwise the number stays unlinked. The palette rule: a block is separate if and only if
its «kind × form» pair differs — a list or a count — and a test guards that, not memory.

Every seller — not just the house — gets the same depth: a storefront **profile
builder (themes/colours/blocks, no third-party CSS/JS), a cart and orders**
(goods mode with stock), categories with attributes and per-user access, and
a seller cabinet with accounting-grade insight: revenue by period, view
statistics, CSV export, and login-free report links (mirroring owner reporting).


Technology that works for you#

⚡ Performance#

MetricValue
Server start~300 ms
Catalog load<100 ms
Photo conversion~50 ms/photo
Watermarkcached
User session30 days
POST rate limit10/min per IP
Max uploadup to 15 MB
Database size~2 MB

🔒 Security#

FeatureMethod
Authenticationbcrypt + httpOnly cookies
Data leaksAPI always hides owner data
Link accesstoken reports (no login needed)
XSS protectionUnicode escaping in JSON
Rate limiting10 req/min on public POST
GEO blockingoptional (by IP/country)
Analyticsaggregates only, no IP stored
Document storageend-to-end: the server only ever sees ciphertext
Chatpersonal — end-to-end (vault password); business — encrypted with a key kept outside the database, and every read by support is shown in the conversation
Secret profile versionsthe URL fragment never reaches the server — so it is never logged, never in Referer, never indexed
Résumé privacya CV is nameless by default; the author opens the name, nobody else can
Public people catalogueopt-in only, and the consent is checked inside the query, not around it

📈 Analytics — without surveillance#

The panel shows visits by day: totals, uniques, mobile / desktop, user-agent families (iPhone, Android, Windows, macOS, Linux), bots on a separate line. The database keeps daily counters only: no IP addresses, no raw logs.

🤖 Telegram — bookings without the website#

A guest writes to the Telegram bot → sees available apartments → books → receives contacts once the manager confirms. No website, no registration — just chat.

sequenceDiagram participant G as Telegram Guest participant M as Telegram Manager G->>M: I need a flat, 3 nights (booking) M-->>G: ✅ / ❌ M->>G: contacts

🧠 AI — content editor and author#

An editor forwards an article to the Telegram bot → AI checks quality, categorizes it (travel/news/laws), rewrites, generates a title, slug and related-article suggestions. The quality threshold is configurable (default 6/10).

flowchart LR A["Article<br/>Text"] --> P["AI Pipeline<br/>Score (0–10)<br/>Categorization<br/>Rewriting<br/>Slug + links"] P --> O["Output<br/>Draft in the blog"]

✍️ BBCode and markdown — diagrams, code and formulas#

Articles, forum posts, listings and documents share one safe text engine (md.js — the same file on the server and in the browser, so the editor preview and the published page cannot drift apart). Everything is escaped before parsing: no raw HTML, no javascript: links, no XSS. The markup is BBCode ∪ markdown — the classic set (headings, lists, GFM tables, quotes, images) plus:

The heavy renderers (mermaid, highlight.js, KaTeX) load lazily from a CDN and only if the page actually contains such a block — a plain text article ships zero extra bytes, and the server still has its single dependency.

💰 Transparent reporting for owners#

Every apartment owner gets a token link — no login or password needed to see:

A calculation web instead of a cascade. Who gets what from a booking, the agency draws as a graph — "just like in Visio": "Booking" and "Surplus" inputs, pools, parties; arrows as a percentage of inflow, a sum per booking, night or month, or the remainder. Agency standard → edit for the building → edit for the apartment; the manager confirms — and the apartment's money is frozen until they confirm a new version themselves. Every booking channel has its own web: website guest, team, partner agency.

No more "we lost the paper". Everything online, everything transparent.

🤝 Partner bookings and guest payments#

🔑 Handing a listing over to an agency#

The owner creates the property, and entrusts showing it, taking enquiries and bookings to an agency — by handing the listing over, not by sharing an account.

💬 Chat — one button for every conversation#

A round button in the bottom-right corner of every page. Hover it and a carousel of quick actions slides out: my chats, support, message the seller (on a listing), call · WhatsApp · Telegram, post a listing, favourites. Staff and market members arrange it for themselves; staff also set the domain default.

Photos are laid out by rooms, not as one strip: "living room with kitchen" is one room with zones, as in real life, and the listing counts its rooms, bedrooms and kitchens from that tree by itself. A photo has a caption and alt text in three languages, a type (drawing · construction · finished room) and a date; video, gif and YouTube live alongside.

✏️ Editing — right on the card#

The buttons on the card's edge open a window over the page with the same blocks as the editing page: the page stays as a fallback, and no second editor appeared.

🎚 The card is a control panel — the catalog is the manager's desk#

A guest sees a storefront; whoever runs the listing sees the same card with its controls, right in the catalog: a rail of buttons on the left, ✏️ on the right, any number of cards worked from one tab.

📮 Posting — by right, in the section, with several offers#

"Post a listing" is seen only by someone the server will actually let in: the button, the page and the door ask one question, so a guest does not stumble on a form that would turn them away (a guest on the form is sent to sign in and back).

🎭 One section — two versions and two teams#

A section can live twice: as the agency's theme (/estate — its own look, storefront, card and posting form, a mini-site of the agency) and as the shared version inside the market (/market/estate — uniform with its neighbours). The listings are the same — facts, offers, photos, bookings, accounting; only the look and the posting differ, by the place where they are shown.

📱 A QR code on every property#

Every listing has a unique QR code (SVG, correction level H — 30%, survives even a sticker in the middle). Generated server-side in microseconds. Colors come from the property's tags.

flowchart LR QR["📸 QR Code"] --> SITE["batume.li/estate/…"] SITE --> PHOTOS["Photos, price, free dates"]

💧 Watermark — image or text#

Property photos are protected on the fly: a PNG logo or text (size, bold, italic, color, font — all configurable in the panel), plus a grayscale option — visitors see black-and-white photos while originals stay visible only to the property's managers. Results are cached.

🃏 The property card — for the crawler first, for the eyes second#

Every card carries its own schema.org markup: an apartment or a house with rent and sale offers, floor area, rooms, address and a map point; goods, vacancies and services get their own types. The tile and the markup are built from one compiled view of the listing, so "what the robot sees" and "what the visitor sees" cannot drift apart, and a results page points to the very same objects with its list.

🔎 SEO — visible to the crawler, not just to you#

The sitemap is assembled on every request and puts the objects themselves into the index: listings, seller storefronts and their pages, residential complexes, geo landings. Every link carries hreflang alternates for all three languages and points at exactly the address the page declares canonical — no crawl budget wasted on redirects. Hide a section with one switch and its goods leave the map with it; move a seller to their own domain and their pages move to their sitemap instead of being duplicated in the portal's.

Geo landings — /market/in/georgia/batumi/cat/property: a page per place × type intersection, cross-linked between neighbouring cities. Pairs are built from what actually exists: "no listings here" pages are never created.

🖼 Share card — drawn by the server#

A link with no image looks broken in a feed. When a page has no photo of its own — a geo landing never does, a new brand may not — the server draws a 1200×630 card itself: name, place, count, site signature, its own colour per page kind. Zero external requests and zero extra dependencies: the SVG is built as a string and rasterized by the same sharp that does watermarks. Rename the organization and the picture is redrawn while the link stays the same.

💱 Currency rates — always current#

4 times a day (00/06/12/18) the server refreshes GEL/USD/RUB/EUR rates. If the API is down, the last rates keep working. Listing prices stay correct even when currencies jump.


Synchronization — a two-instance architecture#

Data is split into two tiers: Donor (full data, including personal) and Satellite (public content only, no owner data and no guest accounts). This satisfies data-residency requirements across jurisdictions. A satellite shares links to itself; the canonical address lives only in meta tags for search engines.

flowchart LR subgraph Donor["Donor (primary)"] D1["All data"] D2["Owners + accounting"] D3["Users"] D4["Blacklist"] end subgraph Satellite["Satellite (copy)"] S1["Public content"] S2["No personal data"] S3["Managers only"] S4["(staff-only sign-in)"] end Donor -- sync --> Satellite

The stack — why this is high-tech#

ComponentTechnologyWhy
LanguageTypeScriptType safety without overhead
RuntimeBun4× faster than Node.js, built-in SQLite, test runner, bundler
DatabaseSQLite WALZero administration, ~2 MB, 100% ACID
Framework❌ noneOwn SSR — full control, zero bloat
Dependencies1 (sharp)Minimal attack surface
Image pipelinesharp + mozjpegJPEG 85%, watermark (PNG/text/grayscale), EXIF orientation
QROwn GF(256)Reed-Solomon level H, no external APIs
AIOpenAI-compatibleAny provider (OpenAI, Anthropic, local Ollama)
TelegramLong-polling (2 bots)No webhooks, nothing exposed
TLSLet's EncryptCertificate auto-picked from the urn registry in README
Multi-siteurn-config.jsonOne codebase+DB → several domains, configured once
Backupscp kh.sqlite backup/One command

Numbers that speak for themselves#

MetricValue
📊Lines of TypeScript~42 000 — 50 source files
🧪Tests54 files — plus 6 end-to-end acceptance runs
📦Dependencies1 — sharp (the rest is ours)
🗄️Database~2 MB — SQLite, serverless
🚀Start time~300 ms — command to first request
🌍Languages3 — RU, EN, KA
🗺Map tilesrendered by us — 0 external requests
📸Photo limit15 MB per upload
🔄Currency refresh4 times a day
🤖Telegram botsa registry — several per type and per domain
🧠AI threshold6/10 — configurable
⏱️Session30-day — no re-logins
🛡️QR correction30% — level H (Reed-Solomon)

UX/CX — why guests come back#

ScenarioImpression
Found a flat in 5 secFilters: type, price, dates, rooms
Swipes photos on a phoneSwipes never fire from scrolling — only a deliberate gesture
Saw real photosWatermark protects, never annoys
Booked via TelegramNo website registration
Got the contactAfter manager confirmation
Owner: report by linkNo login required
Reads articles on BatumiAI quality, 3 languages
Buys/sellsMarketplace with FTS5 search
Talks on the forumAnonymous (with TTL), no fear
Switches language1 click — RU/EN/KA

Customer Experience (CX) is when the system helps instead of standing in the way. KristinaHouse is designed so a guest spends time choosing an apartment — not fighting the website.


For the business owner#

KristinaHouse is not a CRM, not WordPress, not a site builder. It is a proprietary high-performance platform written specifically for a family agency model in Batumi.


Data sources#

The portal's geo directory — country, region, city, street, building — is populated from
OpenStreetMap via the Overpass API. Imported rows carry their origin: a human edit takes the
row out of future import runs, so the correction lives forever.

Map geometry comes from the same place — and the site renders its own basemap from that local
store of outlines. Serving a map makes no request to any third-party tile server: the map works
offline.

Data © OpenStreetMap contributors, licensed under ODbL.
Cartographic styling is our own.


KristinaHouse — Batumi, Georgia.
batume.li · batumi.su
"We live here ourselves, personally inspect every apartment and keep transparent reporting for owners."

💬