Skip to content
Dylan San GabrielDylan
Get in touch

GET /api/work/internal-erp/channel-sync

Six channels, one product master

Live · · sole engineer

Six websites, each with its own version of the truth about our 32,000 products. One catalog, owned by people, that none of them is allowed to overwrite.Order intake and listing sync for six channels, with the identity rules that keep 49,000 marketplace listings from ever overwriting the product catalog.

  • TypeScript
  • PostgreSQL
  • eBay API
  • Amazon SP-API
  • Shopify
  • Walmart
  • Net32
  • Gmail API
sales channels
6
channel listings
49K
orders ingested
300K
syncs that write to master
0
GET/api/work/{id}

Parameters

Request

curl "https://portfolio.dylansg0318.workers.dev/api/work/internal-erp/channel-sync"
source: src/content/projects/{id}

Response200 OK · 2.8 KB

{
  "id": "internal-erp/channel-sync",
  "title": "Six channels, one product master",
  "status": "live",
  "year": 2026,
  "category": "work",
  "role": "sole engineer",
  "summary": "Order intake and listing sync for six channels, with the identity rules that keep 49,000 marketplace listings from ever overwriting the product catalog.",
  "summary_plain": "Six websites, each with its own version of the truth about our 32,000 products. One catalog, owned by people, that none of them is allowed to overwrite.",
  "stack": [
    "TypeScript",
    "PostgreSQL",
    "eBay API",
    "Amazon SP-API",
    "Shopify",
    "Walmart",
    "Net32",
    "Gmail API"
  ],
  "metrics": {
    "sales_channels": "6",
    "channel_listings": "49K",
    "orders_ingested": "300K",
    "syncs_that_write_to_master": "0"
  },
  "parent": {
    "id": "internal-erp",
    "title": "The internal ERP that runs a dental supply company",
    "page": "https://portfolio.dylansg0318.workers.dev/projects/internal-erp"
  },
  "problem": "Every marketplace has its own idea of what a product and an order are. eBay's SKU is per-transaction. Net32 splits one cart into two orders under our two storefronts and gives them the same number. Amazon closes access to the buyer's address after thirty days. The old setup let each feed write straight into the catalog, so a bad sync could rename thousands of products overnight, and did.",
  "unique": "The master catalog is sacred: a channel sync may create or update a listing row, never a product. Listings bind to products. Humans own the products. Order identity is the channel's own id plus the storefront, never our internal number. And every payload from every marketplace is stored raw before it's parsed, so when the question changes next quarter the answer is already in the database instead of behind an API that has since expired the data.",
  "ai": "The channel APIs were documented into the repo first (endpoints, quirks, the traps we'd hit), and the agents worked from those docs. The rules (master is sacred, identity is channel id + storefront, store raw) were mine, written before the code. The agents' job was to make the code obey them, and hooks enforce that they still do.",
  "learned": [
    "A join against a vendor's mirror that matches zero rows is almost always your key format, not their data. A non-breaking space in a SKU cost a day.",
    "Failing open is the default failure mode of a filter. A status check written as <> 'void' matched everything because the value was 'voided'. Every list now has a canonical filter helper, and the helper has a test.",
    "Storing the raw payload is the cheapest insurance you can buy. Three features shipped months later from data we'd already captured and never had to re-fetch."
  ],
  "page": "https://portfolio.dylansg0318.workers.dev/projects/internal-erp/channel-sync"
}

Live: this is the answer from this site’s API, not a mock. Send re-runs the request with your parameters; if the answer names a page, it opens.

The problem

Every marketplace has its own idea of what a product and an order are. eBay's SKU is per-transaction. Net32 splits one cart into two orders under our two storefronts and gives them the same number. Amazon closes access to the buyer's address after thirty days. The old setup let each feed write straight into the catalog, so a bad sync could rename thousands of products overnight, and did.

What was unique

The master catalog is sacred: a channel sync may create or update a listing row, never a product. Listings bind to products. Humans own the products. Order identity is the channel's own id plus the storefront, never our internal number. And every payload from every marketplace is stored raw before it's parsed, so when the question changes next quarter the answer is already in the database instead of behind an API that has since expired the data.

Where AI fit in

The channel APIs were documented into the repo first (endpoints, quirks, the traps we'd hit), and the agents worked from those docs. The rules (master is sacred, identity is channel id + storefront, store raw) were mine, written before the code. The agents' job was to make the code obey them, and hooks enforce that they still do.

GET/api/work/{id}

Parameters

Request

curl "https://portfolio.dylansg0318.workers.dev/api/work/internal-erp/channel-sync?fields=problem%2Cunique%2Cai"
source: src/content/projects/{id}

Response200 OK · 1.3 KB

{
  "problem": "Every marketplace has its own idea of what a product and an order are. eBay's SKU is per-transaction. Net32 splits one cart into two orders under our two storefronts and gives them the same number. Amazon closes access to the buyer's address after thirty days. The old setup let each feed write straight into the catalog, so a bad sync could rename thousands of products overnight, and did.",
  "unique": "The master catalog is sacred: a channel sync may create or update a listing row, never a product. Listings bind to products. Humans own the products. Order identity is the channel's own id plus the storefront, never our internal number. And every payload from every marketplace is stored raw before it's parsed, so when the question changes next quarter the answer is already in the database instead of behind an API that has since expired the data.",
  "ai": "The channel APIs were documented into the repo first (endpoints, quirks, the traps we'd hit), and the agents worked from those docs. The rules (master is sacred, identity is channel id + storefront, store raw) were mine, written before the code. The agents' job was to make the code obey them, and hooks enforce that they still do.",
  "page": "https://portfolio.dylansg0318.workers.dev/projects/internal-erp/channel-sync"
}

Live: this is the answer from this site’s API, not a mock. Send re-runs the request with your parameters; if the answer names a page, it opens.

The story

Selling on six websites at once sounds like six copies of the same job. It isn’t. Each site has its own opinion about what a product is, what an order is, even what our company is. One of them splits a single checkout into two orders under our two store names and gives both the same order number. Get that wrong and you refund the wrong one.

The old setup let each website write its opinions straight into our product catalog. One bad update could rename thousands of products overnight, and once it did.

The fix was a house rule: the catalog belongs to people. The websites each get their own copy to scribble on, and a person approves anything before it touches the real thing. Now when a marketplace sends something weird it’s a ten-minute review instead of a week-long cleanup. And every scrap the marketplaces send us gets kept, which has quietly answered three “can we find out…?” questions months later, for free.

Today that’s about 300,000 orders and 49,000 listings through one set of rules.

The shape of the problem

The catalog is big, 32,000 products, and every marketplace wants a slightly different version of it. Titles cap at 80 characters on one, banned words suspend a listing on another, and the same physical item may be listed three times under shadow SKUs so each storefront can price it on its own.

The old approach let the sync tool own the catalog: whatever the marketplace said, that’s what the product became. It’s the intuitive design and it’s wrong, because the marketplace is downstream of the catalog, not upstream.

Master is sacred

One sentence: a channel sync must not insert or update a product. Channels write to their own listings table. A listing binds to a product, and a human confirms the binding. If a feed shows a SKU we don’t recognise, it becomes an unbound listing in a queue, not a new product. That sounds restrictive and it’s the opposite: a sync can be wrong without being expensive. A bad eBay import makes a hundred unbound listings a person clears in ten minutes, instead of a hundred corrupted products that take a week to find.

Identity

Our internal order number exists nowhere outside this app; nobody can type it into Seller Hub. So every human-facing surface — alerts, emails, printed documents, log lines — names the order the way the marketplace does, with the storefront:

Net32 #65046596 (RMH3 Dental)

The storefront is part of the identity because Net32 splits one checkout across our two storefronts and gives both halves the same number. They’re two orders: separate money, separate stock, separate status. The one thing they may share is a box, the way a warehouse would notice two orders going to one address, and that coupling lives in shipping and nowhere else. Most bugs in a multi-channel system turn out to be identity bugs; the fix is deciding once what a thing is and not letting any path answer differently.

Store the raw thing

Every response from every marketplace is stored as received, next to the parsed row. Amazon closes access to a buyer’s address after about thirty days; when a feature needed those addresses months later, they were already in the database. Carrier invoices, eBay finance transactions, Net32 payout reports: all kept raw, all mined later for things nobody asked for at ingest time.

Pushing back

Stock levels push to every channel so we stop selling what we don’t have; a marketplace that suppressed us for cancelling too many orders was the lesson there. Prices push from the repricing engine. Tracking pushes on ship. Each push is a job with a checkpoint, so a redeploy mid-run resumes instead of restarting.

What I learned

  1. A join against a vendor's mirror that matches zero rows is almost always your key format, not their data. A non-breaking space in a SKU cost a day.
  2. Failing open is the default failure mode of a filter. A status check written as <> 'void' matched everything because the value was 'voided'. Every list now has a canonical filter helper, and the helper has a test.
  3. Storing the raw payload is the cheapest insurance you can buy. Three features shipped months later from data we'd already captured and never had to re-fetch.
GET/api/work/{id}

Parameters

Request

curl "https://portfolio.dylansg0318.workers.dev/api/work/internal-erp/channel-sync?fields=learned"
source: src/content/projects/{id}

Response200 OK · 652 B

{
  "learned": [
    "A join against a vendor's mirror that matches zero rows is almost always your key format, not their data. A non-breaking space in a SKU cost a day.",
    "Failing open is the default failure mode of a filter. A status check written as <> 'void' matched everything because the value was 'voided'. Every list now has a canonical filter helper, and the helper has a test.",
    "Storing the raw payload is the cheapest insurance you can buy. Three features shipped months later from data we'd already captured and never had to re-fetch."
  ],
  "page": "https://portfolio.dylansg0318.workers.dev/projects/internal-erp/channel-sync"
}

Live: this is the answer from this site’s API, not a mock. Send re-runs the request with your parameters; if the answer names a page, it opens.