STALL.ONE
<< Back to Listia
WHITE_PAPER // SYSTEM DESIGN

Listia Architecture

Multi-tenant ecommerce chassis under SnapNgo: host shell and remotes over an API gateway to ownership-clear services — Mongo catalog as the product write model, channel overrides for marketplaces, async AI enrichment, and a supervised path from photo to publish.

Project
Listia
Audience
Lead eng · architects
Stack
SnapNgo · Catalog · Agents · Mongo · Gateway
WHITE_PAPER SNAPNGO CATALOG AGENTS MARKETPLACE
┌──────────────────────────────────────────────────┐
│                    Host Shell                    │
│              (auth context · MF load)            │
│  ┌──────────┐ ┌───────────┐ ┌────────────┐       │
│  │ Auth MFE │ │Billing MFE│ │Catalog MFE │       │
│  └────┬─────┘ └─────┬─────┘ └─────┬──────┘       │
└───────┼─────────────┼─────────────┼──────────────┘
        │             │             │
        └─────────┐   │   ┌─────────┘
                  ▼   ▼   ▼
┌──────────────────────────────────────────────────┐
│                   API Gateway                    │
│         JWT · rate-limit · x-user-* inject       │
└───┬─────────┬──────────┬──────────┬──────────────┘
    │         │          │          │
    ▼         ▼          ▼          ▼
┌───────┐ ┌───────┐ ┌────────┐ ┌─────────┐
│ Auth  │ │Billing│ │ Media  │ │ Catalog │ ··· inventory / orders (next)
│  MS   │ │  MS   │ │ blob   │ │  MS     │
└───┬───┘ └───┬───┘ └───┬────┘ └────┬────┘
    │         │         │           │
    │ Postgres│         │      MongoDB
    │         │         │   products · categories
    └─────────┴─────────┴───► S3 / R2
                    tenants/{storeId}/products/...

packages (import, not deploy):
  @snapngo/shared · @snapngo/observability
  product UI tokens (host + remotes)

Two layers

Platform chassis — gateway, auth (orgs / seats / roles), SaaS billing entitlement, tenant-scoped media with presigned uploads. Same multi-tenant SaaS spine as other SnapNgo products.

Ecommerce domain — catalog first (products, categories, channel deltas); inventory and orders as separate services when stock and cart matter. Do not force “vault = catalog.” Lift the chassis; swap the vertical.

SnapNgo layers

  • Host shell — session, active store/org, app frame; loads remotes
  • MFEs — Auth, Billing, Catalog (admin lab), later storefront surfaces
  • API gateway — single public door; JWT; injects x-user-*; blocks internal routes
  • Auth / Billing / Media / Catalog — identity, SaaS sub, blobs, products

Mental model (services)

  • gateway — edge only; no product or membership source of truth
  • auth — users, orgs (stores), members, roles; internal membership API for other MS
  • billing — merchant pays you (SaaS plans / limits), not shopper cart charges
  • media / blob — membership + optional entitlement → presign under tenants/{storeId}/…
  • catalog — Mongo product write model; never joins auth tables for membership
  • inventory / orders — stock and cart later; soft-ref product ids, no shared catalog SQL
Browser (host)  ──MF──► catalog_remote
        │  cookie / JWT
        ▼
 api-gateway
        │
        ├── /api/auth/*  ──────────► auth ──► Postgres
        │                               │
        │                               │  GET /internal/memberships
        │                               │  (x-internal-service-secret)
        │                               ▲
        └── /api/v1/catalog/* ──► catalog
                                    ├── MongoDB  products + categories
                                    └── HTTP ──► auth membership

 Gateway returns 404 for /api/v1/internal/*
 (browser must not call service-internal APIs)

Catalog write model

Product data lives in MongoDB, not shared Postgres. Auth keeps users and membership; catalog owns document-shaped products.

  • Unique per tenant: (organizationId, slug), (organizationId, sku)
  • Core — title, sku, brand, status (draft | published | archived), media refs
  • customAttributes — merchant facets (labeled, mergeable); not free-form dump
  • channelOverrides — marketplace deltas (eBay title, Google condition) without forking the base product
  • aiContext — isolated AI output (tags, syndication copy, embeddings, status) so human fields are never silently overwritten

Base product = channel-agnostic source of truth. Channel listing status is separate (ACTIVE | INACTIVE | PENDING | ERROR). PATCH merges overrides by channel unless replace mode is set. GET ?channel=ebay returns resolved channelView.

Two enrichment tracks

Both write into isolated aiContext (and related media), not by overwriting human core fields on save. Product save never waits on an LLM.

  • 1 · Product metadata enrichment — after a product exists (title, SKU, specs already saved), an async worker adds SEO copy, channel syndication text, tags, and text embeddings from the write model.
  • 2 · Photo enrichment (asset gen) — source product photo + operator prompt → gen AI produces new images (studio packshot, lifestyle, social crops, channel-ready variants). Optional vision pass on outputs; human confirm before assets attach to the product.
Merchandiser → catalog-service (sync save → Mongo)
       │  emit ProductCreated / ProductUpdated
       ▼
AI worker (SEO · syndication copy · text tags · text embeddings)
       │  update product.aiContext
       ▼
Search projector (later) ──► text vectors + facets
Source photo ──► [prompt] ──► [gen ai]
   (phone / pack)              studio · lifestyle · social
                                      │
                                      ▼
                               [Vision agent]  (optional QC / tags)
                                      │
                           ┌──────────┴──────────┐
                           ▼                     ▼
                    [Human review]        [Publish gate]
                           │                     │
                           └────── confirm ──────┘
                                      │
                                      ▼
                    media gallery + refs on product
                    (not silent overwrite of primary shot)

Metadata enrichment answers “what is this product in words?” Photo enrichment answers “what selling images can we generate from this shot?” Different inputs, different outputs; both async relative to product save unless a lab admin demo forces sync.

Photo ──► [Vision agent] ──► attributes JSON
              │
              ▼
        [Schema mapper]
        eBay | Amazon | Etsy
              │
              ▼
        [Adapter / DOM]
        session · form fill
              │
       ┌──────┴──────┐
       ▼             ▼
 [Human review]  [Publish gate]
       │             │
       └──── confirm ┘

Listia’s publish path is browser execution, not marketplace seller APIs. Small multi-channel sellers are not on developer programs or advanced sync stacks; adapters drive the same seller UIs they already use. Least-privilege sessions; irreversible publish stays behind a human gate. Syndication copy in aiContext feeds the mapper; empty channel overrides fall back to auto-generated text. Sellers who need full channel APIs and warehouse sync have outgrown this wedge.

Membership boundary

Catalog does not share-query auth’s member table. It calls:

GET /api/v1/internal/memberships?userId=&organizationId=
Header: x-internal-service-secret: …

Roles change per org; JWT claims alone are not the long-term authority for every role map. Internal routes stay off the public gateway surface.

Shopper read model (target)

Merchant admin talks to catalog Mongo. Shopper search and category pages should not fan out to catalog + pricing + inventory on every request.

  • Write services stay systems of record
  • Outbox / change stream → projector → denormalized doc in Elasticsearch / OpenSearch
  • Flatten searchableText and facets at project time
  • Fuzzy stock bands in the index; exact qty at reserve/checkout
  • Every doc scoped by organizationId / storeId

Sequence: catalog write model first → inventory/pricing sources → ES projector when PLP is a product requirement — not ES before you have a product.

SaaS billing vs commerce payments

  • billing-service — merchant subscription to you (limits: products, seats, storage)
  • commerce payments — shopper pays merchant (cart / PaymentIntents / Connect later)

Do not put every shopper charge through the SaaS billing MS. Catalog checks entitlement (e.g. create product allowed / max products) before create.

What each piece refuses to own

  • Gateway — no products, no membership truth
  • Auth — no product documents, no cart
  • Billing — no shopper checkout ledger
  • Catalog — no stock ledger, no auth SQL, no blocking LLM on save
  • Search index — not the product database; rebuild from sources
Outcome

Clean base product, marketplace noise under channel overrides, AI in an isolated context, membership via auth HTTP, gateway as the only public door. Admin path uses the write model; shoppers eventually hit a projected read model. Publish stays supervised when adapters touch real channel UIs.