Retail customer-service platform

In production

A self-hosted platform that runs Facebook customer service for English Home Libya in Libyan Arabic — catalog-true prices, photo-based product recognition, and human handoff.

Role
Solo — architecture, implementation, deployment, operations
Timeframe
2025 – present
Stack
TypeScript · Next.js · PostgreSQL · Kysely · Docker Compose · Caddy · Gemini API · Meta Graph API
Links
Platform repositoryCatalog scraperTrilingual matcherVisual matcher

The problem

Built and operated for English Home Libya, a retail business serving customers in Libya. Its Facebook page — a public audience of roughly 61,000 followers (public page count, July 2026) — receives a constant stream of product questions in Libyan Arabic, most of them “how much is this?” with a photo attached. Answering them means knowing exactly which of more than 4,700 catalog products (as of July 2026) is in the picture, and never inventing a price. As the business’s Digital Systems & Automation Specialist, I had spent two years handling those messages myself, which is where the requirements came from.

The system

Customers message the page; the platform batches bursts of messages, works out what’s being asked, identifies the product (by code, by text in three languages, or from a photo), answers with prices read only from the live catalog, and hands the conversation to a human the moment intent moves to orders, refunds, or complaints. A bilingual, RTL-first admin console covers inbox, analytics, campaigns, image review, and runtime AI controls.

The pipeline is fed by two subsystems published as standalone repositories: a resumable catalog scraper (real Chrome over CDP) that assembled an image catalog exceeding 11 GB during the documented catalog build, and a deterministic trilingual matcher that decides which scraped Turkish product is the same physical item as a priced Arabic/English catalog entry — only high-confidence matches attach automatically; everything else queues for human review. Photo recognition follows the same “never guess” rule: an empty candidate pool returns none, not the least-bad product, and human-confirmed corrections teach the matcher.

Decisions that came from real incidents

Each of these guards exists because something actually went wrong once:

From free tier to owned infrastructure

The platform originally leaned on a hosted free-tier database — until the provider paused the project and took the system down with it. I migrated everything to a single self-managed VPS: Postgres 16, the Next.js app, and Caddy under Docker Compose, with nightly pg_dump backups and a written deployment runbook. The repository preserves that migration as fourteen ordered SQL migrations.

Verification and limits

Pure-logic behavior (sanitizer, policy, hashing) is covered by assertion scripts; the extracted visual matcher runs its six-test suite in CI. What I don’t claim: uptime numbers or traffic statistics — the repository contains no benchmarks, and this page doesn’t invent any. Operational metrics will be published when they can be sourced.

Credits

Solo project, built with modern tooling. It runs on Gemini and Meta’s Graph API; everything else — including the matching engines — is in the linked repositories.

Platform architecture: Caddy in front of the Next.js admin app and Postgres, integrations core connecting Meta webhooks and Gemini
Diagram — deployment architecture (from the repository; placeholder domains).
Message flow: webhook, burst batching, catalog tools, sanitizer, delivery guard, human handoff
Diagram — customer-message flow through the safety guards (from the repository).