Skip to content
chaninl
th

Build Log

The client asked for a website. The data lived in a 30-year-old POS.

by chaninl ·

The brief sounded easy: a website that shows our products. The kind of thing you nod along to in the first meeting and start sketching pages for.

Then the catch arrived. The product data lived on a computer in the client’s office. It was generated by a script that pulled from a Visual FoxPro system — yes, FoxPro — every hour. And I would never get to touch that source system. I could read what it produced. That was it.

So this was never a website job. It was an integration job wearing a website costume.

The constraints I didn’t own

Before I wrote a line of code, the real shape of the project came from things I had no control over:

  • The data lives on-prem. It sits on the client’s office machine, so my system had to run there too — not on some tidy cloud box I get to manage.
  • The source has no API. Visual FoxPro wasn’t going to hand me a nice endpoint. The only contract I got was the files that script produced.
  • The client’s information is never complete. They told me the data refreshed every hour. It was actually every two. Small detail — big lesson, and I’ll come back to it.

When you can’t change the source, the constraints are the design. My job was to build around them, not to wish them away.

Architecture v1

Here’s where it landed. Everything below runs on the client’s office PC, inside Docker, and reaches the outside world through a single tunnel:

Client's office PC  (Windows + Docker)
┌────────────────────────────────────────────────┐
│  Visual FoxPro ──script──▶ CSV files           │
│                               │                │
│                          file watcher          │
│                               │                │
│                             ETL ──▶ database   │
│                                       │        │
│                                  API server    │
└────────────────────────────────────────┬───────┘
                                         │  Cloudflare Tunnel

                                  Next.js (server-side)


                                     Visitors

Each piece exists because of a constraint, not because it was fun to add:

  • A file watcher → ETL → database because I can only read files, so I watch for new drops, transform them, and keep my own queryable copy.
  • An API server on the client’s machine because that’s where the data is. Plus the usual guards on the API: an API key, CORS, rate limiting.
  • Cloudflare Tunnel to get that on-prem API onto the internet so the website can reach it. This part was almost suspiciously easy to set up — by far the smoothest piece of the whole stack.
  • Next.js for the front end, specifically so the sensitive parts stay server-side and never leak through a client-side API call.

While I was working through these calls, I leaned on AI as a design partner — not just to write code, but to argue trade-offs out loud. I also kept a short Architecture Decision Record in the repo itself, so the why behind each choice lives next to the code. The bonus I didn’t expect: it became context an AI agent could read back later. More on that approach in a future post.

Where reality bit back

Two moments are worth keeping.

“Every hour” was actually every two. Because I’d made the refresh interval a config value from day one — not a number hard-coded somewhere deep — this was a one-line change instead of a hunt. That’s the whole lesson: the client’s assumptions will be wrong, so make every assumption a variable.

Enabling Docker on Windows meant editing the BIOS. I’d tried hard to keep this a remote, no-onsite project. Virtualization was switched off in firmware, so that plan looked dead — but in the end I didn’t drive out. I found another way around it, which I’ll get into another time.

The one thing I learned

Before the first line of code, list everything you don’t own — the source system, where the data lives, what the client hasn’t told you yet. That list is what actually decides your architecture. Everything else is just preference.

Next time

The files from FoxPro turned out to be a contract with no documentation. They wrap text in " but don’t escape an inch-mark inside it — so a normal CSV parser quietly breaks. Reverse-engineering that format is the next post.

← Back to blog