“LILY BEYOND” — bilingual tour leader website with a CMS admin panel and private analytics
A bilingual (RU/EN) tour leader website: a showcase of author-led tours, a gallery, moderated reviews, and request intake. All content — texts, tours, photos, videos, and resume — is edited from a custom admin panel and stored in the database, while the site runs its own private analytics via tracking links, without a cookie banner.

Slide 1 of 11
DESCRIPTION
A full-stack web application on Node.js (Express, ESM) over PostgreSQL. A deliberately minimal stack was chosen: no frontend frameworks, no bundlers, and no ORM — the server side is a thin layer of parameterized SQL queries through a pg connection pool, and the frontend is plain HTML/CSS and vanilla JavaScript. The idea is the same one that sets the project apart from a static landing page: the site is a database-driven system where every text, tour, photo, review, and section is edited through the admin panel, while public pages pull data from a single source of truth — PostgreSQL.
The “database is the source of truth, RAM is the hot cache” architecture
The key engineering decision of the project. The database is remote (cloud-hosted), and its latency must not affect the site’s speed, so all public content — texts, tours, gallery, approved reviews, requests, contacts, resume, and even binary media — is kept in process memory and served from there instantly. The database remains the source of truth: caches are warmed at startup and updated pointwise after every save from the admin panel. Warm-up is made fault-tolerant — the first connection to the remote database often times out, so each cache is loaded with several retries and pauses; a background timer re-reads all caches every 5 minutes (self-healing during brief database outages). If the database is entirely unavailable, the site keeps running on the latest data or on built-in defaults rather than crashing.
Media in the database
Videos and images are stored directly in the database as bytea together with metadata (mime, name, size) and served through a custom route /api/media/:key from the RAM cache — reading a 14-megabyte bytea from the remote database takes seconds, so already-loaded files are kept in memory. The route fully supports HTTP Range requests (206 Partial Content responses with correct range trimming), thanks to which the browser starts playing the hero video immediately without waiting for a full download; delivery uses an immutable one-year cache.
CMS admin panel
The restricted /admin section is a full-fledged control panel with separate screens: texts of each page, tours (with image uploads, ordering, and publishing), gallery, reviews (moderation), requests (folders, statuses, campaigns), contacts and social links, resume, legal texts, analytics, a notepad, and design settings. All content is edited without a single line of code — this is what sets the project apart from a regular static tour leader site.
Authentication and backend protection
Access to /admin and all /api/admin/* is closed with HTTP Basic Auth at the server level; the login and password are verified in constant time via crypto.timingSafeEqual (protection against timing attacks). A “fail-closed” policy is implemented: if the admin password is not set in the environment, the admin panel is entirely unavailable — it physically cannot accidentally end up open due to misconfiguration. A separate middleware layer reliably blocks serving environment files (.env and other dotfiles), even if they were accidentally requested directly.
Bilingual support (RU/EN) at the data level, without i18n libraries
Editable texts are stored under a base (Russian) key and its English variant with the suffix “__en”; the API serves the requested language via ?lang=en with an automatic fallback to Russian if the translation is empty. Tour translations are stored inside the tour itself (data.i18n.en) and overlaid onto the base version with a custom recursive “smart” merge: translation values override the base only where they are non-empty, arrays are merged element by element, and shared fields (slug, image, accent color, flag) are not duplicated into the translation. Hardcoded interface labels are translated by a small script using data-i18n / data-i18n-ph attributes. The language choice is stored in localStorage and shared between the site and the admin panel; content switches on the fly without a reload.
Tour model
The entire tour object is stored in JSONB (a flexible schema with no migrations for each field) plus service columns: slug, sort order, and a publication flag. Tours are accessible at human-readable addresses /tours/:slug; old links with the .html extension are neatly redirected (301) to clean URLs, including moving tour.html?slug=X to /tours/X — this preserves SEO when the URL scheme changes.
Request and review intake
Requests come from the contact form and from tour pages (bound to a specific tour and tariff), pass server-side validation and length trimming, are stored in the database with statuses (new / read / accepted / archived), and are organized into folders that the admin creates on their own (for example, for specific tours). Reviews are published only after moderation (pending → approved / rejected). The “new” badges in the admin menu are counted from the cache without querying the database.
Private analytics without cookie banners
A custom tracking system in the same database: the admin creates short tracking links (campaigns with a ?ref=… code) to separate traffic sources. Event types are limited to a whitelist (page view, time on page, request, review, contact click). Time on page is counted honestly — only active time (tab visibility is taken into account), sent in batches and topped up on leave via navigator.sendBeacon so as not to lose data when the tab is closed. For the owner themselves there is a notrack mode (remembered in the browser) — their own visits do not distort the statistics. The server aggregates events per campaign and separately for “direct visits,” builds a “today” summary (visitors, total time, top tours), and a full breakdown with contact clicks and tariff popularity.
Instant hero screen
The first screen appears immediately: before the video loads, a static poster is shown as the background, and the video itself is drawn on top in a canvas with a transparent background (until there is a single frame, the poster is visible through the canvas). Frames are scaled with the cover principle taking the screen’s pixel density (DPR) into account, and drawing the video into the canvas — the heaviest repeated operation — runs only while the hero is visible and the tab is active: IntersectionObserver and visibilitychange stop the requestAnimationFrame loop and pause the video off-screen.
Animations and “live” feeds
The first-screen heading appears letter by letter — and the animation starts only after the text is hydrated from the database, so that the final string is split into letters (with correct word wrapping on narrow screens). The tour feed is a “marquee” of glass cards that can be scrolled by dragging (Pointer Events) and freezes off-screen. Reviews run as a seamless feed (three copies of the set for a continuous loop), loaded from the real database of approved reviews.
Tour page: a route with a flying plane
The day-by-day route is drawn with an SVG curve: a smooth cubic curve is built through the centers of the day circles, wave-like bending right and left. As you scroll, the line “draws itself” (stroke-dashoffset animation from full length to zero), and a plane icon flies along its end — the position is taken via getPointAtLength, the rotation angle is computed from the tangent to the curve (using two nearby points), the scroll direction is accounted for (the nose turns around when scrolling up), and near the end of the route the plane smoothly fades out. Everything respects prefers-reduced-motion.
Resume and PDF
The resume exists as a page on the site and is downloaded as a “pixel-perfect” PDF — the page itself is captured via html2canvas and assembled into a single A4 PDF sheet in html2pdf, without scaling to the sheet, so the text size in the file matches what is seen on screen. The PDF is generated in the language of the current version of the site; the contact icons (Telegram / Instagram / VK) are drawn to render crisply in the resulting document. The resume PDF file can also be uploaded in the admin panel and served at /resume.pdf.
End-to-end solutions
All secrets and external accesses (database, admin password, SSL) are extracted into environment variables; traffic is gzip-compressed (compression) for html/css/js/json; the PostgreSQL pool is limited in the number of connections and correctly survives dropped idle background connections to the remote database (a pool error handler so an unhandled event does not crash the process). Dependencies are deliberately minimal (express, pg, compression, dotenv). Production is deployed on a VPS under systemd.
TOOLS USED
- JavaScript (ES Modules)
- Node.js
- Express
- PostgreSQL
- pg (node-postgres
- пул соединений)
- SQL
- JSONB
- bytea
- HTTP Range (206 Partial Content)
- HTTP Basic Auth
- Web Crypto / crypto.timingSafeEqual
- compression (gzip)
- dotenv
- HTML5
- CSS3
- Canvas 2D
- SVG (getPointAtLength / stroke-dashoffset)
- IntersectionObserver
- Pointer Events
- requestAnimationFrame
- navigator.sendBeacon
- localStorage
- Fetch API
- html2pdf.js / html2canvas
- systemd (VPS-деплой)
- Git
RESULT
- Designed and implemented a bilingual (RU/EN) tour leader website on a lightweight Node.js + Express + PostgreSQL stack — without frontend frameworks, bundlers, or an ORM.
- Built the “database is the source of truth, RAM is the hot cache” architecture: all public content is served from memory instantly and does not depend on remote database latency.
- Implemented fault tolerance: cache warm-up with retries at startup, background refresh every 5 minutes (self-healing), and running on defaults when the database is unavailable.
- Media (video/photos) is stored in the database as bytea and served from the RAM cache with HTTP Range (206) support and a one-year immutable cache — the hero video starts immediately.
- Built a custom CMS admin panel: texts, tours, gallery, reviews, requests, contacts, resume, analytics, and design are edited from the database without code changes.
- Admin panel protection at the server level: Basic Auth with constant-time password verification (timingSafeEqual), a fail-closed policy, and blocking the serving of .env/dotfiles.
- Achieved bilingual support at the data level without i18n libraries: the __en suffix for KV texts and a recursive “smart” merge of tour translations (data.i18n.en).
- Tours are modeled flexibly in JSONB with slug addresses /tours/:slug and 301 redirects from old .html links — SEO is preserved when the URL scheme changes.
- Implemented request intake bound to a tour and tariff, with statuses and folders, as well as moderated reviews (pending → approved/rejected) and “new” counters in the menu.
- Built private analytics without a cookie banner: tracking links (campaigns), a whitelist of events, honest counting of active time with top-up via sendBeacon, a notrack mode for the owner, and aggregation by campaigns and “direct visits.”
- Created an instant hero: poster background + video rendering in a canvas with render pausing off-screen (IntersectionObserver + visibilitychange) and cover scaling.
- Implemented “live” interfaces: letter-by-letter appearance of the heading after hydration, a drag-scrollable tour feed, and a seamless feed of real reviews.
- On the tour page, built a day-by-day SVG route with a plane flying along the tangent to the curve on scroll (getPointAtLength + stroke-dashoffset), accounting for scroll direction and prefers-reduced-motion.
- Implemented “pixel-perfect” resume download to PDF (html2pdf/html2canvas): a single A4 page with text size as on screen and correct contact icons.
- All secrets are extracted into the environment, traffic is gzip-compressed, the PostgreSQL pool is resilient to dropped background connections; production is deployed on a VPS under systemd.
AI-ASSISTANT
Ask a question about this project