Активность движка
🏡 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.
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#
Why this matters to you:
- No "the server is down" — no PostgreSQL, no Redis, no Docker. One entity — one point of failure.
- Instant start — ~300 ms to the first request.
- Backups — copy a single
kh.sqlitefile. Done.
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.
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.
| Lang | Interface | Content | Notifications |
|---|---|---|---|
| RU | ✅ | ✅ | ✅ |
| EN | ✅ | ✅ | ✅ |
| KA | ✅ | ✅ | ✅ |
Features — 28 modules in one system#
| Module | Status | For whom |
|---|---|---|
| 🛒 Marketplace — sections, lots, cart | ☑ Active | Sellers + buyers |
| 🎛 Designer — the whole site's look behind one door | ☑ Active | Section owners |
| 📃 Pages from blocks — the home page is just one of them | ☑ Active | Administrator |
| 📐 Plans — halls and floors in metres, drawn like in Visio | ☑ Active | Organisers + sellers |
| 🗞 Feed — short posts, threads and clips in one stream | ☑ Active | Everyone |
| 🔗 Bonds — a shared name instead of a friend list: type the same id and you have met | ☑ Active | Users |
👤 Personal pages /~login with secret versions | ☑ Active | Users |
| 🔐 End-to-end encrypted documents — spreadsheets, a safe, contract templates | ☑ Active | Board participants |
| 💬 Chat — personal (end-to-end) and business conversations, one button with quick actions | ☑ Active | Everyone signed in |
| 🔗 Share links with a scope — the guest gets a slice, cut and encrypted in your browser | ☑ Active | Owners + their accountants |
| 📄 PDF export — our own writer, fonts embedded, Georgian included | ☑ Active | Everyone with a document |
| 🏠 Real estate — a section, not a subsystem | ☑ Active | Guests |
| 📅 Bookings + price calendar | ☑ Active | Guests + sellers |
| 🗺 Own map — locally rendered OSM tiles | ☑ Active | Everyone |
| 📊 Seller cabinet: reports, lots, tools | ☑ Active | Sellers |
| 📝 Blog | ☑ Active | Readers |
| 💬 Forum | ☑ Active | All users |
| 🎉 Events + invitations | ☑ Active | Organisers + guests |
| 🤖 Telegram bots (registry, multi-bot) | ☑ Active | Guests + sellers |
| 🧠 AI — key pool, multi-vendor | ☑ Active | Editorial + bots |
| 🔐 Authentication | ☑ Active | Users |
| 💱 Currency rates | ☑ Active | System (auto) |
| 📱 QR codes for lots | ☑ Active | Promotion |
| 🌐 Synchronization (donor → satellites) | ☑ Active | Administrator |
| 📄 Documents | ☑ Active | Guests |
| ⚙️ Control panel | ☑ Active | Managers |
| 📈 Visit analytics | ☑ Active | Managers |
| ⚖️ Legal profile (GE · EU-GDPR · no-data) | ☑ Active | Visitors + admin |
| 🍪 Cookie banner | ☑ Active | Visitors (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#
| Metric | Value |
|---|---|
| Server start | ~300 ms |
| Catalog load | <100 ms |
| Photo conversion | ~50 ms/photo |
| Watermark | cached |
| User session | 30 days |
| POST rate limit | 10/min per IP |
| Max upload | up to 15 MB |
| Database size | ~2 MB |
🔒 Security#
| Feature | Method |
|---|---|
| Authentication | bcrypt + httpOnly cookies |
| Data leaks | API always hides owner data |
| Link access | token reports (no login needed) |
| XSS protection | Unicode escaping in JSON |
| Rate limiting | 10 req/min on public POST |
| GEO blocking | optional (by IP/country) |
| Analytics | aggregates only, no IP stored |
| Document storage | end-to-end: the server only ever sees ciphertext |
| Chat | personal — 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 versions | the URL fragment never reaches the server — so it is never logged, never in Referer, never indexed |
| Résumé privacy | a CV is nameless by default; the author opens the name, nobody else can |
| Public people catalogue | opt-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.
🧠 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).
✍️ 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:
- BBCode formatting —
[b] [i] [u] [s] [color=…] [size=…] [spoiler] [quote=who] [h1…h4] - Layout blocks —
[row]/[col]for columns and[block bg=… border=… align=…]for a styled block; every value passes a whitelist, so arbitrary CSS never reaches the page - Links with site icons —
[link="url" favicon]; icons come through our own caching proxy, so a reader's browser never talks to a third party - Mermaid diagrams — a
`mermaidblock turns into a flowchart, sequence or Gantt chart (securityLevel: strict) - Syntax-highlighted code — a
`langblock gets highlight.js (GitHub theme) plus a one-click "copy" button - LaTeX formulas —
…inline and…as a block, via KaTeX - Image sliders — a short list of photos becomes a swipeable gallery with captions
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:
- All bookings (dates, amounts, statuses)
- Expenses (utilities, cleaning, repairs)
- Payout calculation — in their own currency too, at the rate the agency states
- The whole statement or only the bookings the agency selected
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#
- Availability link — a partner agency sees free dates (no prices, no names) and enters its client's booking without signing up; the team confirms it in the queue.
- Two confirmation lists — "from guests and partners" and "entered by the team", so your own bookings don't get lost among others.
- Guest payments — paid, owed, overpaid; a refund is a reversal, not "minus one hundred".
- Surplus — whatever was charged above the usual price is split separately and hidden from the owner's statement by default.
🔑 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.
- Offer and consent — the agency accepts the listing or declines with a reason; the owner picks its rights: listing fields, contacts, "bump", "sold", archive.
- The owner only watches — the listing, the fact of every enquiry (without the applicant's contact: that goes to the agency) and their own owner statement.
- No lost bookings — pending requests follow the listing, confirmed bookings are seen through to check-out by whoever accepted them, and each side keeps its own accounts.
- Closed sections — the pinned agency decides who may submit "proposal listings"; a binding "all my listings in this section go to this agency" is agreed once.
💬 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.
- Speak as yourself, a private seller or an organisation — the switcher on top; the whole team of an agency sees and answers its conversations, and the server re-checks the right on every message.
- Personal chats are end-to-end — the key opens with the same vault password as your documents; the server and support see only ciphertext, and the contact's fingerprint is shown to compare outside the site.
- Business chats are transparent — stored encrypted, open to the parties and to support for disputes; every read by support is written right under the conversation.
- Edit without losing the thread — an edit is a new version; «🕘 history» unfolds the version tree inside the message, replies stay attached. Personal marks (⭐ 📌 ☐ ✅ ❓, colours) with a filter.
- Listing handed to an agency? — buyers write to the agency, not to the owner.
- batumi.su — the same button, messenger links only.
🖼 A gallery by rooms — and the floor plan as an interactive map#
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.
- The viewer — the photo, rooms as a strip, room characteristics and sleeping places with their circumstances ("the sofa unfolds").
- The floor plan — in metres, with furniture; the "📐 Floor plan" button shows the plan, a mini-plan next to the photo highlights its room, and tapping a room opens its photos.
- Titles and tags — built in the constructor, no code. "1+1 apartment" and "Euro two-room" are assembled by the section's rules from the rooms and the building; a tag like "beach {distance} metres away" is a template with fields and units, and a building is tagged once — its flats inherit it.
- Moving without losses — the previous site's properties were carried over as live listings, and their old
/property/…addresses answer with a permanent redirect to the new card, keeping the language in the path.
✏️ 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.
- ℹ️ Details — the listing gains "my title" (only you see it: "Ekaterina", "flat by the sea"), a team title for managers, its own link preview for messengers and social networks in every language, and manual badges "until a date".
- 👤 Parties and owner — who shares in the property's money, and for the owner a card with phone, email, messenger and bank details. It is stored only if the site's legal profile allows it; "erase data" wipes the personal part while the accounting rows stay.
- 🎨 The agency's own listing look — on top of the section's assembly: hide a block, make it narrower or wider, reconfigure it — and the agency's listings look that way in every section and collection, including those taken into management. Section edits still reach everything the agency didn't touch; every block has "↺ back to base".
🎚 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.
- One way to plug things in — bookings, the price calendar, enquiries, accounting and owner statements, parties, documents, spreadsheets, languages: each is an organ of the designer, attached to the card the same way. What shows is decided by rights and data, not by hard-coded buttons — a button the server would refuse is never drawn.
- Documents from your own vault — contracts, sheets and templates live in the end-to-end encrypted vault and are tied to a listing, a booking or the owner by a blind tag: the server does not learn what is inside. A contract is assembled from a template in the browser, filled with the listing, the booking, the owner and the agency's bank details, in the chosen language.
- 🌐 Languages — an overview of every translation of the listing and a "translate" button, right on the card.
- The manager's panel — filters "mine · awaiting a reply · unpaid · handed over", tick the cards (or "all by filter"), open one organ for all of them at once; each card answers by its own right, and the skipped ones say what was missing. Batch actions go only through the doors that already exist — no mass delete. On a phone it folds into a "🧰 Manage (N)" drawer.
- + Object — a new listing starts as a draft right inside its card, with the same blocks as the posting form.
📮 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).
- In context — opened from a section, the form arrives with the category already chosen and in that section's look; with no context it is the market's standard form, where the category and "buy / sell" must be picked.
- The form is built in the designer — which fields, which deals, which offers: the section's holder assembles it without code.
- Several offers in one listing — sale, monthly rent and daily rent together, each with its own price and its own auction: start, step, deadline, reserve. Bids go only up, the winner is fixed after the deadline, and an auction without a winner is restarted with a new deadline.
🎭 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.
- Different people edit each — the theme belongs to the pinned agency's team, the shared version to the market operator's team; the portal stands above both. These are two rows of team membership, not a branch in the code.
- The designer knows the place — the theme's holder starts the theme's own assembly with one button (an empty surface inherits from the subject area), the shared version follows the market's; a "through a token's eyes" preview shows the page as a guest, a manager or an owner would see it.
- Addresses stay — once the portal opens the shared version, it answers at
/market/<section>, the old ones lead there with a permanent redirect, and listing addresses do not change.
📱 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.
💧 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.
- Just like the previous site — a photo slider grouped by rooms, an RU · EN · GE language rail on every card, "📰 articles and comments" and "📑 description · features" panels, a currency picker right inside the price, "Share" with a QR code and a map by address.
- A four-row property page — the photo next to a thumbnail grid, price and "Contact" beside the title, tags, "description · features · articles", the conversation, the building map, three similar listings.
- Fast — a page of 24 cards costs 9 database queries instead of ~300: a listing view is compiled once per edit, favourites and viewer rights are computed in one pass per page.
- Whole without scripts — the server assembles the card; the script only brings it to life.
🔎 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.
The stack — why this is high-tech#
| Component | Technology | Why |
|---|---|---|
| Language | TypeScript | Type safety without overhead |
| Runtime | Bun | 4× faster than Node.js, built-in SQLite, test runner, bundler |
| Database | SQLite WAL | Zero administration, ~2 MB, 100% ACID |
| Framework | ❌ none | Own SSR — full control, zero bloat |
| Dependencies | 1 (sharp) | Minimal attack surface |
| Image pipeline | sharp + mozjpeg | JPEG 85%, watermark (PNG/text/grayscale), EXIF orientation |
| QR | Own GF(256) | Reed-Solomon level H, no external APIs |
| AI | OpenAI-compatible | Any provider (OpenAI, Anthropic, local Ollama) |
| Telegram | Long-polling (2 bots) | No webhooks, nothing exposed |
| TLS | Let's Encrypt | Certificate auto-picked from the urn registry in README |
| Multi-site | urn-config.json | One codebase+DB → several domains, configured once |
| Backups | cp kh.sqlite backup/ | One command |
Numbers that speak for themselves#
| Metric | Value | |
|---|---|---|
| 📊 | Lines of TypeScript | ~42 000 — 50 source files |
| 🧪 | Tests | 54 files — plus 6 end-to-end acceptance runs |
| 📦 | Dependencies | 1 — sharp (the rest is ours) |
| 🗄️ | Database | ~2 MB — SQLite, serverless |
| 🚀 | Start time | ~300 ms — command to first request |
| 🌍 | Languages | 3 — RU, EN, KA |
| 🗺 | Map tiles | rendered by us — 0 external requests |
| 📸 | Photo limit | 15 MB per upload |
| 🔄 | Currency refresh | 4 times a day |
| 🤖 | Telegram bots | a registry — several per type and per domain |
| 🧠 | AI threshold | 6/10 — configurable |
| ⏱️ | Session | 30-day — no re-logins |
| 🛡️ | QR correction | 30% — level H (Reed-Solomon) |
UX/CX — why guests come back#
| Scenario | Impression |
|---|---|
| Found a flat in 5 sec | Filters: type, price, dates, rooms |
| Swipes photos on a phone | Swipes never fire from scrolling — only a deliberate gesture |
| Saw real photos | Watermark protects, never annoys |
| Booked via Telegram | No website registration |
| Got the contact | After manager confirmation |
| Owner: report by link | No login required |
| Reads articles on Batumi | AI quality, 3 languages |
| Buys/sells | Marketplace with FTS5 search |
| Talks on the forum | Anonymous (with TTL), no fear |
| Switches language | 1 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.
- No monthly fees — your own Contabo server, your own codebase.
- No SaaS limits — any number of properties, users, bookings.
- Full data control — the SQLite database lives on your server.
- A single entry point — website + Telegram + AI + reporting in one system.
- GDPR-ready — two-instance architecture, cookie banner, analytics without stored IPs.
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."
