Skip to content
Dylan San GabrielDylan
Get in touch

← All work

GET /api/work/internal-erp

The internal ERP that runs a dental supply company

Live · · sole engineer · 13 daily users

The company sold on six websites using six different tools that never agreed. I built the one system that runs it all, about 500 orders a day.One system for orders, inventory, repricing, shipping, service and books across six sales channels, built from scratch, about 500 orders a day.

  • TypeScript
  • Node.js
  • Express
  • React
  • PostgreSQL
  • Railway
  • GitHub Actions
  • Docker
orders / day
~520
products
32K
sales channels
6
daily users
13
orders ingested
300K+
price changes logged
138K+
automated tests
2,721
scheduled jobs
180
The ERP's warehouse map: a floor plan of the main building with named rooms and 147 numbered storage locations, a building switcher, a layout editor and a printable map.
The ERP's warehouse map: a floor plan of the main building with named rooms and 147 numbered storage locations, a building switcher, a layout editor and a printable map.
GET/api/work/{id}

Parameters

Request

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

Response200 OK · 5.4 KB

{
  "id": "internal-erp",
  "title": "The internal ERP that runs a dental supply company",
  "status": "live",
  "year": 2026,
  "category": "work",
  "role": "sole engineer · 13 daily users",
  "summary": "One system for orders, inventory, repricing, shipping, service and books across six sales channels, built from scratch, about 500 orders a day.",
  "summary_plain": "The company sold on six websites using six different tools that never agreed. I built the one system that runs it all, about 500 orders a day.",
  "stack": [
    "TypeScript",
    "Node.js",
    "Express",
    "React",
    "PostgreSQL",
    "Railway",
    "GitHub Actions",
    "Docker"
  ],
  "metrics": {
    "orders_day": "~520",
    "products": "32K",
    "sales_channels": "6",
    "daily_users": "13",
    "orders_ingested": "300K+",
    "price_changes_logged": "138K+",
    "automated_tests": "2,721",
    "scheduled_jobs": "180"
  },
  "problem": "RMH3 Dental sells on Net32, eBay, Amazon, Shopify and Walmart, plus direct orders by phone and email. The business ran on off-the-shelf ERP software, a dozen spreadsheets and a shared inbox someone sorted by hand. Every job needed a second tool to finish, the systems disagreed about stock and money, and nobody could say which one was right. Orders were growing and the people weren't.",
  "unique": "I stopped treating it as a dashboard over the data and built it as the place the work actually happens: the order is picked in the page, the return is decided in the page, the bill is matched in the page. Everything hangs off seven daily loops the business runs on — intake, inventory, repricing, service, returns, financials and intelligence — and every job, page and background producer has to trace to one of them. Rules that had cost real money became rules in code.",
  "ai": "Built with AI coding agents doing most of the typing and me doing the deciding: the schema, the loop model, the invariants, what gets a second-model review and what ships on sight. The interesting engineering was making that safe — hooks, a coordination layer, and rule files that auto-load per area. That's its own write-up on this site.",
  "learned": [
    "The most expensive bugs were never crashes. They were plausible numbers, like a 30-day revenue figure overstated by 60% because a query windowed on the date we recorded the order instead of the date it happened. Now the guardrail gets designed before the feature.",
    "A background job that reports success while doing nothing is worse than one that fails. Every scheduled job checkpoints, logs what it touched, and trips a breaker if it keeps failing, because \"it ran\" isn't \"it worked\".",
    "Writing the operating rules down (300-plus documents, 130,000 lines) was the highest-leverage thing I did. It's what let the work scale past one pair of hands without the rules leaking out of people's heads."
  ],
  "subsystems": [
    {
      "id": "internal-erp/repricing",
      "title": "Repricing every minute without a race to the bottom",
      "summary": "Replaced a morning pricing spreadsheet with an engine that reprices every minute, with cooldowns, floors, and a rule to never chase the lowest price.",
      "page": "https://portfolio.dylansg0318.workers.dev/projects/internal-erp/repricing"
    },
    {
      "id": "internal-erp/channel-sync",
      "title": "Six channels, one product master",
      "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.",
      "page": "https://portfolio.dylansg0318.workers.dev/projects/internal-erp/channel-sync"
    },
    {
      "id": "internal-erp/inventory",
      "title": "Counting what sells, not what exists",
      "summary": "A nightly classifier ranked all 27,215 products and made 1,979 of them class A. Ranking only what sells turned a fake Pareto into a real one.",
      "page": "https://portfolio.dylansg0318.workers.dev/projects/internal-erp/inventory"
    },
    {
      "id": "internal-erp/printing",
      "title": "Printing labels when the printer can lie about it",
      "summary": "A thermal-label pipeline and a self-updating Windows print fleet — predicted widths, verified feeds, and an update that rolls itself back.",
      "page": "https://portfolio.dylansg0318.workers.dev/projects/internal-erp/printing"
    },
    {
      "id": "internal-erp/quickbooks",
      "title": "Keeping the books and the app in agreement",
      "summary": "A two-way QuickBooks Desktop integration over the Web Connector, plus a claims ledger so a finance discrepancy is refuted once, not rediscovered monthly.",
      "page": "https://portfolio.dylansg0318.workers.dev/projects/internal-erp/quickbooks"
    },
    {
      "id": "internal-erp/toolbox",
      "title": "The toolbox, extracted",
      "summary": "Nine self-contained pieces of the ERP, lifted out and sanitized — barcode parsing, thermal-print layout, a self-updating device agent, invoice parsers.",
      "page": "https://portfolio.dylansg0318.workers.dev/projects/internal-erp/toolbox"
    }
  ],
  "cover": {
    "url": "https://portfolio.dylansg0318.workers.dev/_astro/warehouse-map.DpCbPiFd.png",
    "width": 1600,
    "height": 1000,
    "alt": "The ERP's warehouse map: a floor plan of the main building with named rooms and 147 numbered storage locations, a building switcher, a layout editor and a printable map."
  },
  "page": "https://portfolio.dylansg0318.workers.dev/projects/internal-erp"
}

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

RMH3 Dental sells on Net32, eBay, Amazon, Shopify and Walmart, plus direct orders by phone and email. The business ran on off-the-shelf ERP software, a dozen spreadsheets and a shared inbox someone sorted by hand. Every job needed a second tool to finish, the systems disagreed about stock and money, and nobody could say which one was right. Orders were growing and the people weren't.

What was unique

I stopped treating it as a dashboard over the data and built it as the place the work actually happens: the order is picked in the page, the return is decided in the page, the bill is matched in the page. Everything hangs off seven daily loops the business runs on — intake, inventory, repricing, service, returns, financials and intelligence — and every job, page and background producer has to trace to one of them. Rules that had cost real money became rules in code.

Where AI fit in

Built with AI coding agents doing most of the typing and me doing the deciding: the schema, the loop model, the invariants, what gets a second-model review and what ships on sight. The interesting engineering was making that safe — hooks, a coordination layer, and rule files that auto-load per area. That's its own write-up on this site.

GET/api/work/{id}

Parameters

Request

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

Response200 OK · 1.3 KB

{
  "problem": "RMH3 Dental sells on Net32, eBay, Amazon, Shopify and Walmart, plus direct orders by phone and email. The business ran on off-the-shelf ERP software, a dozen spreadsheets and a shared inbox someone sorted by hand. Every job needed a second tool to finish, the systems disagreed about stock and money, and nobody could say which one was right. Orders were growing and the people weren't.",
  "unique": "I stopped treating it as a dashboard over the data and built it as the place the work actually happens: the order is picked in the page, the return is decided in the page, the bill is matched in the page. Everything hangs off seven daily loops the business runs on — intake, inventory, repricing, service, returns, financials and intelligence — and every job, page and background producer has to trace to one of them. Rules that had cost real money became rules in code.",
  "ai": "Built with AI coding agents doing most of the typing and me doing the deciding: the schema, the loop model, the invariants, what gets a second-model review and what ships on sight. The interesting engineering was making that safe — hooks, a coordination layer, and rule files that auto-load per area. That's its own write-up on this site.",
  "page": "https://portfolio.dylansg0318.workers.dev/projects/internal-erp"
}

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.

Inside it

  • Repricing every minute without a race to the bottom

    Prices used to be a spreadsheet someone updated every morning. Now a robot checks every product once a minute, and knows better than to start a price war.Replaced a morning pricing spreadsheet with an engine that reprices every minute, with cooldowns, floors, and a rule to never chase the lowest price.

  • Six channels, one product master

    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.

  • Counting what sells, not what exists

    A nightly job decides which products are worth counting and reordering. It had been calling nearly everything important, which is the same as calling nothing important.A nightly classifier ranked all 27,215 products and made 1,979 of them class A. Ranking only what sells turned a fake Pareto into a real one.

  • Printing labels when the printer can lie about it

    Every box needs a label and a slip, printed on machines in a warehouse nobody logs into. This is what prints them, checks they came out, and repairs itself when an update goes bad.A thermal-label pipeline and a self-updating Windows print fleet — predicted widths, verified feeds, and an update that rolls itself back.

  • Keeping the books and the app in agreement

    The sales system and the accounting books have to agree to the penny. This is the connection that keeps them agreeing, and the logbook that ended the false alarms.A two-way QuickBooks Desktop integration over the Web Connector, plus a claims ledger so a finance discrepancy is refuted once, not rediscovered monthly.

  • The toolbox, extracted

    The system itself must stay private, but the useful parts don't have to. Nine pieces pulled out and scrubbed clean — real warehouse code, public and readable.Nine self-contained pieces of the ERP, lifted out and sanitized — barcode parsing, thermal-print layout, a self-updating device agent, invoice parsers.

GET/api/work/{id}

Parameters

Request

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

Response200 OK · 2.3 KB

{
  "subsystems": [
    {
      "id": "internal-erp/repricing",
      "title": "Repricing every minute without a race to the bottom",
      "summary": "Replaced a morning pricing spreadsheet with an engine that reprices every minute, with cooldowns, floors, and a rule to never chase the lowest price.",
      "page": "https://portfolio.dylansg0318.workers.dev/projects/internal-erp/repricing"
    },
    {
      "id": "internal-erp/channel-sync",
      "title": "Six channels, one product master",
      "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.",
      "page": "https://portfolio.dylansg0318.workers.dev/projects/internal-erp/channel-sync"
    },
    {
      "id": "internal-erp/inventory",
      "title": "Counting what sells, not what exists",
      "summary": "A nightly classifier ranked all 27,215 products and made 1,979 of them class A. Ranking only what sells turned a fake Pareto into a real one.",
      "page": "https://portfolio.dylansg0318.workers.dev/projects/internal-erp/inventory"
    },
    {
      "id": "internal-erp/printing",
      "title": "Printing labels when the printer can lie about it",
      "summary": "A thermal-label pipeline and a self-updating Windows print fleet — predicted widths, verified feeds, and an update that rolls itself back.",
      "page": "https://portfolio.dylansg0318.workers.dev/projects/internal-erp/printing"
    },
    {
      "id": "internal-erp/quickbooks",
      "title": "Keeping the books and the app in agreement",
      "summary": "A two-way QuickBooks Desktop integration over the Web Connector, plus a claims ledger so a finance discrepancy is refuted once, not rediscovered monthly.",
      "page": "https://portfolio.dylansg0318.workers.dev/projects/internal-erp/quickbooks"
    },
    {
      "id": "internal-erp/toolbox",
      "title": "The toolbox, extracted",
      "summary": "Nine self-contained pieces of the ERP, lifted out and sanitized — barcode parsing, thermal-print layout, a self-updating device agent, invoice parsers.",
      "page": "https://portfolio.dylansg0318.workers.dev/projects/internal-erp/toolbox"
    }
  ],
  "page": "https://portfolio.dylansg0318.workers.dev/projects/internal-erp"
}

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

RMH3 Dental sells dental supplies to dentists across the country, on Net32, eBay, Amazon, Shopify, Walmart and over the phone. When I joined, each of those lived in its own tool, plus a dozen spreadsheets and a shared inbox someone sorted by hand. The tools disagreed with each other every day, and someone had to work out which one was lying before the trucks left.

I was hired to fix one spreadsheet, the one where prices got updated every morning. Instead, piece by piece, I built the company one system that does all of it. Every order from every website lands in one place. The warehouse picks and packs from one screen. Prices update themselves. Shipping labels get bought and printed on their own. Returns and customer emails live next to the orders they’re about, and the accounting matches the sales with the receipts to prove it.

The number: the company went from about 270 orders a day to about 520, almost double, without hiring anyone new in operations. And when a number looks wrong now, there’s one system to ask, and it shows its work.

The thing I’d point to isn’t a feature. It’s that the expensive mistakes, where a number is believably wrong and nobody notices for a month, now have guardrails, because each one only got to happen once.

What the business needed

A dental supply distributor runs on thin margins and volume. When I started, about 270 orders a day came in across five marketplaces and a phone line, and each one went through three or four tools on its way to a box. Inventory in one place, prices in another, the books in a third, and when they disagreed, which was daily, someone reconciled by hand at the end of the shift. The brief was to fix the pricing spreadsheet. The real job was everything around it.

How it’s shaped

One TypeScript codebase: an Express API, a React client, one PostgreSQL database, deployed as a container on Railway through GitHub Actions. Nothing unusual there. What’s less usual is the surface one person runs: every order, price, label, return and invoice passes through it, 13 people work in it all day, and 2,721 automated tests sit between a change and production.

Seven loops, not a feature list. Every screen and every one of the 180 scheduled jobs belongs to one of seven daily loops: order intake → fulfilment, inventory and procurement, repricing, customer service, returns, financials, and intelligence. If a feature doesn’t fit a loop, that’s the signal to stop and ask what it’s for.

Seams, not pages. Before building something new, the question is whether an existing seam covers it — the job scheduler, the signals feed, the document-ingestion queue, the audit log. “A page that lists things the user then does something about” almost always wants to be a signal producer, not a page.

Jobs that survive a redeploy. Every long-running job checkpoints. When Railway swaps the container mid-run, the next run picks up from the last checkpoint instead of starting over or quietly skipping the tail.

The rules that stopped the bleeding

The rules that matter most are the ones that cost money before they existed:

  • The product master is sacred. Marketplace syncs write to a listings table, never the catalog. Before this, a bad feed could rename 20,000 products overnight.
  • Business date, not record date. Revenue, velocity and ship-time metrics window on when the order happened, never when it landed in our database. Backfills insert months after the sale, and a plain filter once overstated a month of revenue by 60%.
  • A cart on Net32 is two orders. Net32 splits one checkout across our two storefronts and gives both the same order number. Finance, inventory and service treat them as separate; only the shipper may box them together. Every query on that order number has to say which storefront, or refuse.
  • Humans see the channel’s order number, never ours. Nobody can type “order 1323434” into eBay. Every human-facing surface names the marketplace’s id and the storefront.

None of these are clever. Each one exists because not having it produced a specific, expensive mistake, and each is enforced in code — a query helper, a hook, a schema constraint — not in a wiki nobody reads.

What it runs today

Order intake from six channels with per-channel identity rules. Inventory with lots and expiry, reservations, cycle counts, and stock levels pushed back to every marketplace. A repricing engine that runs every minute. Rate shopping and labels across USPS, UPS and FedEx, thermal printing on the warehouse floor, proof of delivery from carrier polls. A customer-service desk over Gmail and eBay messages, with returns and refund splits. Invoicing, AR and AP, and a two-way QuickBooks Desktop integration. A signals feed that says what a human should look at today.

About 520 orders a day now, roughly double where it started, with the same operations team.

What I learned

  1. The most expensive bugs were never crashes. They were plausible numbers, like a 30-day revenue figure overstated by 60% because a query windowed on the date we recorded the order instead of the date it happened. Now the guardrail gets designed before the feature.
  2. A background job that reports success while doing nothing is worse than one that fails. Every scheduled job checkpoints, logs what it touched, and trips a breaker if it keeps failing, because "it ran" isn't "it worked".
  3. Writing the operating rules down (300-plus documents, 130,000 lines) was the highest-leverage thing I did. It's what let the work scale past one pair of hands without the rules leaking out of people's heads.
GET/api/work/{id}

Parameters

Request

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

Response200 OK · 815 B

{
  "learned": [
    "The most expensive bugs were never crashes. They were plausible numbers, like a 30-day revenue figure overstated by 60% because a query windowed on the date we recorded the order instead of the date it happened. Now the guardrail gets designed before the feature.",
    "A background job that reports success while doing nothing is worse than one that fails. Every scheduled job checkpoints, logs what it touched, and trips a breaker if it keeps failing, because \"it ran\" isn't \"it worked\".",
    "Writing the operating rules down (300-plus documents, 130,000 lines) was the highest-leverage thing I did. It's what let the work scale past one pair of hands without the rules leaking out of people's heads."
  ],
  "page": "https://portfolio.dylansg0318.workers.dev/projects/internal-erp"
}

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.