Commercial product

Tori Market Intelligence

A commercial Tori monitoring service that collects listings from customer-selected categories, keeps them searchable and records price and availability changes. Each subscription is priced individually by the number of tracked categories.

Role
System Architecture & Data Engineering
Period
2023—present
Categories
FrontendBackendInfrastructure
Stack
TypeScriptReactViteFastifyPrismaPostgreSQLMinIOCheerioZodDocker
Sanitized Tori Analytics control room showing selected-category collection and durable pipeline state.

Overview

The product gives customers a repeatable view of the Tori categories they choose to monitor instead of relying on manually saved listing links. Collected listings remain searchable, and later refreshes show what changed, what disappeared and how each price developed.

Each subscription defines the monitored categories. Their category and search URLs become bounded discovery partitions, and a durable scheduler revisits those partitions and every discovered listing according to current activity and source behavior.

Challenge

A live marketplace page is not a stable dataset. Listings are edited, removed and republished, while the source HTML and extraction paths can also change. Keeping only the latest parsed fields would make earlier states impossible to reproduce or reprocess.

Monitoring a changing set of categories cannot be treated as one unbounded request loop. Discovery must be partitioned, resumable and paced, and temporary throttling or server failures must delay work without losing its place or duplicating state.

Listing images repeat across pages and versions and can consume more storage than the normalized records themselves. Raw HTML is also untrusted input, so it must be retained for replay without ever being rendered directly in the operations interface.

Solution

I modelled each subscribed category and its search targets as discovery partitions that collectively cover the customer's selected scope. A PostgreSQL-backed scheduler claims due targets and listings with short leases, allowing interrupted work to become eligible again without clearing queue state.

The worker applies adaptive polling, randomized pacing, retry and backoff for retriable source responses, and conditional requests with ETag and If-Modified-Since during listing refresh. Every meaningful response can produce an immutable raw HTML snapshot by content hash alongside a normalized current listing, listing versions and a separate price timeline.

Images are downloaded into MinIO as S3-compatible, SHA-256-addressed media objects and linked back to listings in display order, so identical bytes are stored once. A Fastify API exposes the normalized catalogue and operational controls to a React/Vite dashboard for crawl management, filtering and change inspection.

Responsibilities

  • Define the product workflow for subscription-scoped discovery, refresh and history.
  • Design target partitions that keep selected-category ingestion bounded, resumable and observable.
  • Define individual subscription pricing around the number of tracked categories.
  • Implement the Fastify API, parser and PostgreSQL-backed scheduler with durable crawl state.
  • Persist reproducible raw snapshots, normalized listing records, immutable versions and price history.
  • Build SHA-256 media deduplication and S3-compatible image storage in MinIO.
  • Create the React/Vite operations dashboard and Docker-based local and production runtime.

Architecture

Interface

  • React operations dashboard

Services

  • PostgreSQL scheduler

  • Fetch + parse worker

  • Fastify API

Data

  • Raw HTML snapshots

  • Normalized listings

  • Versions + price history

  • SHA-256 media objects

Automation

  • Tori marketplace catalogue

  • Discovery partitions

Infrastructure

  • Docker runtime

  1. Catalogue pages into discovery partitions

    Tori marketplace catalogue -> Discovery partitions

  2. Partitions enter the durable schedule

    Discovery partitions -> PostgreSQL scheduler

  3. Due work is claimed with short leases

    PostgreSQL scheduler -> Fetch + parse worker

  4. Reproducible HTML snapshots

    Fetch + parse worker -> Raw HTML snapshots

  5. Parsed normalized listing state

    Fetch + parse worker -> Normalized listings

  6. Stored responses support parser replay

    Raw HTML snapshots -> Normalized listings

  7. Semantic versions and price observations

    Normalized listings -> Versions + price history

  8. Downloaded bytes are hashed and stored

    Fetch + parse worker -> SHA-256 media objects

  9. Searchable catalogue read model

    Normalized listings -> Fastify API

  10. Version and price timelines

    Versions + price history -> Fastify API

  11. Stored listing image references

    SHA-256 media objects -> Fastify API

  12. Typed operations and catalogue requests

    Fastify API -> React operations dashboard

  13. Crawl controls and pipeline state

    PostgreSQL scheduler -> React operations dashboard

  14. Worker production runtime

    Fetch + parse worker -> Docker runtime

  15. API production runtime

    Fastify API -> Docker runtime

  16. Dashboard production runtime

    React operations dashboard -> Docker runtime

Technical decisions

Partition each monitored category

Problem
Subscriptions can cover different sets of categories, and each selected scope still has to remain resumable and controlled as listings change.
Decision
Represent the category and search pages in each subscription as managed discovery partitions that feed one normalized listing corpus.
Reason
Each partition can be scheduled, retried and resumed independently while the selected listings remain unified for search and analytics.
Trade-off
Coverage depends on maintaining the partition plan as the marketplace taxonomy and search behavior evolve.

Price subscriptions by monitored coverage

Problem
Every additional category expands discovery, refresh, history and media work, so one flat price would ignore a major difference in operating scope.
Decision
Calculate each subscription individually from the number of Tori categories the customer chooses to track.
Reason
The price follows the clearest driver of collection scope and remains easy to explain before monitoring starts.
Trade-off
Categories differ in size and activity, so category count is a practical pricing basis rather than an exact measure of workload.

Store raw evidence beside normalized data

Problem
Parser changes or extraction mistakes cannot be investigated if only the latest normalized fields survive.
Decision
Persist immutable HTML snapshots by content hash while keeping a separate normalized Listing model for product queries.
Reason
A source response can be replayed and reparsed without fetching the live page again, while the API remains fast and predictable.
Trade-off
Raw snapshots increase storage usage and must be treated as untrusted content rather than rendered directly.

Use PostgreSQL as the durable scheduler

Problem
Target and listing refresh work must survive worker restarts and avoid permanent ownership when a process stops mid-job.
Decision
Persist next-run state in PostgreSQL and claim due work through short database leases.
Reason
Interrupted jobs become eligible again automatically and operational state remains inspectable alongside the listing data.
Trade-off
Lease duration, polling and indexes require deliberate tuning and favor operational simplicity over maximum queue throughput.

Refresh according to source behavior

Problem
Polling every listing at the same fixed interval wastes requests and increases the risk of throttling without improving useful freshness.
Decision
Adapt target and listing intervals to recent changes, respect Retry-After, back off on retriable responses and use conditional request headers.
Reason
Active listings can be revisited sooner while stable or failing work slows down without leaving the durable schedule.
Trade-off
The scheduler carries more state and freshness becomes an explicit policy rather than a single global interval.

Address media by content

Problem
The same image bytes can appear through multiple listing references, versions or source URLs.
Decision
Hash downloaded media with SHA-256, store one object in MinIO and map listings to the shared object and display order.
Reason
Storage identity no longer depends on temporary source URLs and duplicate bytes do not need separate objects.
Trade-off
Every new media response must be downloaded and hashed before deduplication can be confirmed.

Media

Gallery

Screenshot

01 / 03

Sanitized Tori Analytics control room with selected-category collection controls and non-numeric pipeline states.

The real operations dashboard is shown with live counts, page totals, target URLs and scheduling values replaced by safe state labels.

Outcomes

  • The service is sold by subscription, with each quote based on the number of Tori categories a customer chooses to track.
  • Selected-category collection is coordinated through resumable discovery partitions while discovered products feed one searchable catalogue.
  • Current listing state, immutable versions, price observations and raw source evidence remain distinct, making changes inspectable without sacrificing reproducibility.
  • The operations dashboard provides one place to control crawling, inspect scheduler state and open a listing's normalized details and history.

Verified highlights

  • The running system separates the React/Vite dashboard, Fastify API, worker, PostgreSQL and MinIO services.
  • Raw HTML snapshots, normalized listings, listing versions and price history have separate persistence responsibilities.
  • The scheduler uses PostgreSQL-backed due times and short leases for recoverable target and listing work.
  • Refresh behavior includes adaptive intervals, retry/backoff, Retry-After handling and ETag or If-Modified-Since requests.
  • Listing media is stored as SHA-256-addressed objects in S3-compatible MinIO storage.

Learnings

  • For marketplace monitoring, category and search pages work best as discovery partitions inside each selected scope; they should not become separate data silos.
  • A scraper becomes an analytics system only when raw evidence, current records and historical changes have explicit, different ownership.