← Yuriy Chukaev · PortfolioCase 01
Case 01 · Case study

From paper notebooks to a live operations app

A role-based PWA plus an owner-controlled AI intake agent that turned a florist-retail business running on notebooks and chats into one live, 24/7 order system.

React + Vite + TypeScriptFastAPI + async SQLAlchemyPostgreSQLPWA + web-pushKommo (amoCRM) API v4AI intake agent (human-in-the-loop)

I built an on-demand florist-retail business a single operations platform: a role-based PWA that installs like a native app, plus an AI intake agent that talks to customers across three messenger channels around the clock. Orders now flow through one live system from first message to paid delivery, instead of scattering across paper notebooks and chats.

The problem

Customers wrote in across three separate messenger channels at all hours, and every order was worked by hand. Intake lived in paper notebooks and chat threads; managers matched florists and couriers one order at a time. There was no shared view of what was in progress, first response depended on someone being awake, and orders slipped through the cracks between apps. The business needed a single place where an order is captured, assigned, assembled, verified, and paid — and it needed the after-hours conversation to stop being a bottleneck.

Architecture

The system is split into a role-based PWA front end, an async Python API, a two-way CRM integration, and an AI intake layer that the owner controls directly. Each role gets its own cabinet on top of shared RBAC, and the order lifecycle is a single state machine that every role reads and writes.

Role-based PWA client

React + Vite + TypeScript. Installs like a native app via a web app manifest, works offline-tolerant through service workers, and delivers web-push for new-job and status alerts. Separate cabinets for florists, couriers, managers, and accounting, each scoped by RBAC to only what that role may see and do.

Async API backend

FastAPI with async SQLAlchemy over PostgreSQL. Fully non-blocking request path so live-queue reads, "take the job" writes, and CRM sync all run concurrently under the messenger traffic spikes. The order lifecycle — intake, queue, take, assembly, photo confirmation, payment request — is enforced server-side as explicit state transitions.

Shared live queue

A single real-time queue of open orders that all florists watch. A florist takes a job in one tap; the claim is applied optimistically in the UI and reconciled on the server, which resolves concurrent claims so exactly one florist owns a job. From there the order moves to assembly and photo confirmation.

Kommo (amoCRM) integration

Two-way sync of orders and contacts over the CRM's API v4. Orders opened in-app appear in the CRM and vice-versa; contact records stay consistent across both. This keeps the sales/CRM side of the business as the system of record for the customer relationship while the PWA drives operations.

AI intake agent

Answers customers across all three channels, checks live stock and price before it promises anything, upsells relevant add-ons, and opens the order in the system automatically — so an after-hours message becomes a real, assigned order without a human in the loop for first contact.

Owner-editable prompt control

The business owner edits how the assistant talks — tone, offers, and red lines — right in the app. Changes take effect within about a minute. Every edit is versioned and signed, so the business (not the vendor) owns and audits assistant behavior over time.

Key decisions & trade-offs

Decision — PWA over native mobile apps

I shipped one installable PWA instead of native iOS/Android builds. The rejected alternative was per-platform native apps, which would have meant separate codebases and, more importantly, app-store review gating every release for a business whose intake process was still evolving weekly. With a PWA I deploy fixes instantly, avoid store friction entirely, and maintain a single React/TypeScript codebase across four role cabinets. The trade-off I accepted is thinner access to deep native APIs — but for role cabinets, live queue, web-push notifications, and camera-based photo confirmation, the web platform covers the requirements, so the deploy-speed and single-codebase win was decisive.

Decision — shared live queue with optimistic "take" + server-side conflict handling

Multiple florists watch the same queue and can tap the same job within the same second. The naive alternative — a manager assigns each order manually, or the client waits on a server round-trip before the button responds — either reintroduces the exact hand-matching bottleneck we were removing, or makes the app feel slow. I made the "take" optimistic in the UI so it feels instant, and authoritative on the server so concurrency is correct: the API treats a claim as a conditional state transition and only one florist wins; the loser's UI rolls back cleanly and the job returns to the queue. The trade-off is the extra reconciliation logic and rollback UX, which is worth it to get both a self-serve queue and correctness under concurrent claims.

Decision — owner-editable, versioned prompts (business owns the AI, not the vendor)

The assistant's behavior lives in prompts the owner edits in-app, versioned and signed, applying within ~1 minute — rather than being hard-coded and changeable only by me. The rejected approach was vendor-controlled behavior where every tone or offer change is a support ticket and a deploy. That model is faster to build but makes the business dependent on the vendor for its own voice and its own red lines. By making edits first-class, versioned, and attributable, I kept a human in the loop and put control of a customer-facing AI where it belongs — with the business — while keeping a full audit trail of who changed what and when.

Decision — photo verification gates the payment request

The automatic payment request to the customer only fires after the florist submits a photo confirmation of the assembled order. The alternative — requesting payment at "assembly done" or on a manager's word — invites disputes and charges for orders that don't match what the customer expected. Tying the payment step to a concrete photo artifact makes the money event verifiable and defensible, and it slots the verification cleanly into the lifecycle state machine rather than living as an informal chat step.

Stack

FrontendReact, Vite, TypeScript; PWA (service workers, web app manifest, web-push)
BackendFastAPI (Python), async SQLAlchemy
DatabasePostgreSQL
CRM integrationKommo (amoCRM) API v4 — two-way sync of orders and contacts
AI layerMulti-channel intake agent with live stock/price lookup, upsell, auto order creation; owner-editable, versioned, signed prompts
Access controlRBAC with per-role cabinets: florists, couriers, managers, accounting

Outcome

The full order lifecycle now lives in one system — intake, shared queue, one-tap take, assembly, photo confirmation, and automatic payment request — instead of paper notebooks and scattered chats. The AI intake agent gives an instant 24/7 first response across all three channels and opens real orders automatically, so nothing gets lost between messengers and after-hours demand no longer waits for a human. Managers stopped hand-matching orders one by one, and the business owns and audits its own AI behavior through versioned, in-app prompt edits.