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.
Platform chassis + ecommerce domain
┌──────────────────────────────────────────────────┐
│ 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
Catalog lab path (built)
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.
1 · Product metadata enrichment
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
2 · Photo enrichment (gen new assets)
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.
Listing agent path (marketplace publish)
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
searchableTextandfacetsat 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
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.