Pahlavon Numonjonov Case study
plusline
← All work

plusline / keyline

Two Telegram storefronts — phones and keyboards — running on one codebase, with an admin panel in the chat, photo albums and search.

Role
Solo — design, build, run
Timeline
2026 — present
Stack
Python · aiogram 3 · JSON
Status
Running

The problem

A shop selling phones in Tashkent already had customers in Telegram — and a catalogue that lived as a folder of photos and a person retyping specifications into chat all day. Every “what do you have in stock?” cost a human twenty minutes.

They did not need a marketplace. They needed the browsing part automated and the selling part left to a person: categories, models, a card with photos and specs, search, and a button that hands the conversation to a manager.

Two shops, one codebase

The same thing was needed twice: PLUSLINE for phones, KEYLINE for keyboards. Same structure, different texts, categories, and one extra field — phones carry a condition flag, new or used, keyboards do not.

They are one codebase in two copies, not a shared library. For two small shops run by different people, a shared package would mean coordinated releases and a versioning story for a change that takes five minutes. The cost is honest and written down: touch a shared mechanism in one, check whether the other needs it too.

Constraints and tradeoffs

A JSON file instead of a database

The catalogue is a few hundred items edited by one person through the admin panel. A database would add an operational surface — backups, migrations, a process to keep alive — to a bot that otherwise runs from a single file. products.json is the whole store.

The price is that writes must be atomic: a crash mid-write does not corrupt a row, it destroys the catalogue. Every save goes to a temporary file and then replaces the original.

Photos go as an album, not one by one

A product with five photos sent as five messages is unreadable. Telegram media groups solve it — the photos become one swipeable card — but they come with rules that shape the card: at most ten files, and only the first one carries a caption. That single constraint decides how the whole product card is composed.

No cart, no payment

  • No checkout. Payments mean a payment provider, refunds, receipts and legal responsibility. The shop already sells fine through a person; the bot's job is to get the customer to that person knowing what they want.
  • No recommendations. A few hundred items do not need a ranking model. Categories and search cover it.
  • Strings are not in the code. All of them live in texts.py, because the two shops differ mostly by wording and the owners edit it themselves.

Architecture

main.py routers shop.py admin.py common.py catalog.py products.json atomic write render.py media group ≤ 10 keyboards.py texts.py data/photos on disk
Three routers, one catalogue, one place where cards are built.

Screens and states

Phones > Apple (24) > Samsung (31) > Xiaomi (46) > Used ♻ (12)

Categories. Counts come from the catalogue, so an empty category cannot be browsed into.

[album: 5 photos, swipeable] iPhone 13 · 128 GB · ♻ used battery 89% · no scratches —— [ Ask a manager ]

Product card. One media group, caption on the first photo — the format the platform allows.

/add > category? > name? > price? > condition: new | used ♻ > send photos, then /done ✓ saved, catalogue rewritten

Admin. The owner edits the shop from the same chat, no dashboard to log into.

search: 13 pro ✓ 4 matches iPhone 13 Pro 128 iPhone 13 Pro 256 iPhone 13 Pro Max ♻

Search. Plain substring matching over names — enough at this size, and it never surprises anyone.

Results and lessons

2
shops running the same code, phones and keyboards
0
catalogue files lost — every write is atomic
1 file
is the whole database, backed up by copying it

What I would tell myself at the start

  • Write down that the two copies diverge. They already had when I came back to them. The note in the repository — “fixing a shared mechanism? check the other one” — is worth more than any amount of intended-to-be-shared code.
  • Read the media group rules before designing the card. Ten files and a caption only on the first are not details; they are the layout.
  • Atomic writes from the first commit. A JSON catalogue is a fine database right up until the process dies halfway through write().
  • Keep every string in one file. The owners rewrite wording constantly, and none of them are going to open a handler to do it.