Skip to content

Case study

Classified listings for the used-car market

AvtoTochka connects private sellers, dealers and buyers through listings, VIN reports, chat and credit requests — as an installable PWA with real web push notifications.

Challenge

The used-car market runs on trust — and trust rarely appears on its own. Buyers can never be sure the mileage is correct, whether the vehicle has been in an accident, or whether the seller is really the registered keeper. Sellers, in turn, compete against countless near-identical listings where price, condition and equipment are not presented on comparable terms.

The second barrier is vehicle history. VIN reports exist, but they are scattered across providers, inconsistently structured and often only available after the purchase. Anyone checking a car today has to jump between portals, pay several times over and still ends up without a coherent view of mileage, ownership changes or accident history.

The third barrier is financing. Credit, insurance and trade-in are central to the buying decision, yet they traditionally happen outside the listing. Handing the buyer off to an external bank flow breaks context, loses people mid-way and makes conversion hard to measure for dealers.

The fourth barrier is technical: a used-car portal has to load fast, work on every device, remain visible in search engines and deliver push notifications for new matches reliably — without a large team behind it. Classic framework stacks solve this with a lot of JavaScript and even more operating cost. We wanted to take the other route.

Approach

Five principles shaped the architecture and feature set:

  • Trust as a product surface — ratings, response time, verified dealers and VIN badges are visible signals, not hidden data fields. Buyers should see at a glance who they are dealing with.
  • No framework, no magic — the server runs on the Node.js standard library with a hand-written router and server-side string templates. No React, no Next, no Vue. That keeps the delivery path short and the running cost low.
  • Edge cache for anonymous traffic — nginx serves the most common pages from cache, while CSRF cookies for signed-in users remain intact. That required deliberate nginx tuning rather than another framework layer.
  • Web push without a third-party dependency — push notifications are implemented on the standard library (RFC 8291 aes128gcm + ES256 JWT), not through an npm package. The critical delivery path stays legible and free of supply-chain risk.
  • Partner integration behind a feature flag — the Tinkoff Credit client is fully wired but disabled; activation is three lines of .env and a container restart. Partner integrations can land safely without going live before the contracts are in place.

What we built

A cascade of brand → model → generation → trim, complemented by geo and city selection. Filters resolve on the server, so first paint works without JavaScript and search engines can index the full listings.

Trust signals

Seller ratings, response-time badge, Verified Partner badge for dealers and Verified VIN badge for checked vehicles. Each signal has a clear definition in the data model and a consistent position in the UI.

VIN report with paywall

An aggregated vehicle-history report behind a paywall (299 ₽) with explicit red-flag surfacing. Anomalies — mileage regression, number of owner changes, open obligations — appear at the top rather than in the fine print.

Dealer back office

Registration, an admin approval workflow and Organization JSON-LD for search engines. Dealers maintain inventory, opening hours and company profile themselves; approval ensures that a verified badge really stands for a checked company.

Monetisation widgets

Credit, OSAGO and trade-in widgets live directly on the listing page. Requests flow through /api/leads with per-IP rate-limiting; the admin sees leads in one view and can export as CSV.

Content hub

Six editorial SEO articles rendered from Markdown with automatic brand auto-linkify. Each article ships Article, BreadcrumbList and ItemList JSON-LD so search engines pick up structure and relations cleanly.

PWA with real web push

An installable Progressive Web App with service worker, install banner and offline shell. Push notifications run in production — GET /api/push/public-key returns 200 live — and are used for new matches and messages.

Admin & operations

An admin surface for leads, approvals and content. On the operations side: hourly SQLite snapshots with 7-day retention, an rsync-safe layout under /opt/auto-tochka-ops/ and Docker Compose deploy behind nginx.

Architecture

AvtoTochka is deliberately simply cut: a single Node.js process serves HTTP through a hand-written router, renders views as SSR strings and talks directly to a SQLite database. No framework, no ORM, no server build pipeline — what ships is what sits in the repository and runs unchanged inside the container.

In front of the application container, nginx sits as reverse proxy and edge cache. Anonymous requests are served from cache, while signed-in sessions bypass it. Making that work with CSRF cookies required narrowing the $at_bypass map to at_session only, setting proxy_ignore_headers Set-Cookie, and fixing add_header always shadowing so security headers (CSP, HSTS, X-Frame-Options, Referrer-Policy, Permissions-Policy) survive the cache.

Web push is implemented on the standard library: RFC 8291 aes128gcm for the payload, ES256 JWT for the VAPID header. There is no web-push dependency; the full delivery path lives in our own code and is testable. VAPID keys are threaded into the container via Docker Compose env_file.

The Tinkoff Credit partner integration is a sandbox client with HMAC signing, built on the Node standard library. It sits behind a feature flag: if the env keys are absent, the system falls back to the existing lead flow. Activation is a deliberate, small step in the production .env.

Deployment runs on Docker Compose: the app container, nginx and the backup routines live in a versioned layout under /opt/auto-tochka-ops/. The repository is the source of truth for docker-compose.yml (including env_file and external networks), so rsync deploys no longer overwrite server-side changes.

Numbers & facts

MetricValue
Tests122 / 122 green
Lighthouse mobile (avg)Perf 97 · A11y 100 · BP 100 · SEO 100
Cumulative Layout Shift0
Render-blocking (wastedMs)0 (critical CSS inline, style.css deferred)
Security headers5 / 5 (CSP · HSTS · X-Frame-Options · Referrer-Policy · Permissions-Policy)
Edge cacheHIT for anonymous second requests (nginx tuning)
Sitemap~287 URLs (catalog · 6 articles · dealer · VIN pages)
Backuphourly SQLite snapshot, 7-day retention

What we learned

Edge cache and CSRF only coexist with deliberate nginx tuning. As long as any Set-Cookie slips through, the cache bypasses everything. Only with a narrow $at_bypass map (just at_session), proxy_ignore_headers Set-Cookie, and a corrected add_header always did we keep cache HITs and security headers at the same time.

Web push on the standard library is doable — and worth it. RFC 8291 aes128gcm and ES256 JWT are not trivial, but they are tractable. Skipping web-push removes a supply-chain dependency from the most critical delivery path and makes failures traceable. The harder part was not the cryptography but threading the VAPID keys through Docker Compose; only after env_file was declared cleanly did the container see more than ADMIN_KEY and PORT.

Rsync deploys need a single source of truth. As long as the server carried its own compose file, every deploy overwrote infra configuration. Once the repository became authoritative (env_file and external networks declared), that whole class of bug disappeared.

Honest limits beat inflated metrics. During LCP tuning we reached 0 ms of render-blocking, hero preload and font preload — but the LCP candidate is a paragraph of text whose paint time cannot be shortened any further on the front end. We stopped tuning rather than inventing a number.

Next steps

  • Capacitor wrap for App Store presence — the PWA is installable and a native wrap is the natural next step.
  • Activate the Tinkoff Credit integration in production once the partner contracts are in place — technically it is three lines of .env and docker compose restart app.
  • Expand the content hub in line with actual search demand: more guides, deeper brand and model pages, linked VIN explainers.

Collaboration

Have similar work in mind?

Let’s have a short chat — no sales pitch.