DEV Community

Jay Blur
Jay Blur

Posted on

My First Real Client Was My Dad: What Broke When I Shipped His Website

A story about a leaky first version, a six-hour scraping fight, and a scraper that failed without telling me.

The setup

A while ago my father and I were talking about how entrepreneurship is, at its core, a polite way of stressing about money. By natural flow, he mentioned he wanted a website. He started talking about how hard they must be to build, and I thought: how hard can it be?

This is the story of how that sentence led to a real website for a real client, a scraping pipeline, a self-hosted deployment on a Raspberry Pi, and a lot of mistakes I’m glad I made early. (My client is my father. I call him “the client” in this article because it sounds more professional, but he’s very much the reason this project exists.)

The short version of what I built:

  • A React + Vite frontend
  • A Django REST backend with an admin panel
  • A consultation form, a newsletter, and an articles page
  • An “updates” tab that scrapes the main income tax and GST websites and shows everything in one place
  • Deployment on a Raspberry Pi behind a Cloudflare Tunnel, with a GitHub Actions pipeline

It took about 16 working days of commits, spread across 16 days, around school. The website is hosted right now at https://pisaadvisors.com

The client prefers that the code stay private, so I can’t show it. What I can show is how it works and what went wrong.

Version 1: it works, please don’t look at it

The first version went up in late December 2025. I’d just finished a React frontend in two days, with preboards looming, and my client asked for an appointment-booking feature.

I had the React basics from messing around in 7th grade, so the structure wasn’t the problem. Time was. I used Claude for the chores: writing JSX and building animations, things I knew how to do but couldn’t afford the hours for. The actual design came from a sketch session with my father on a Sunday evening, on paper.

The backend was the questionable part. I didn’t have a real one. When someone clicked the booking button, the frontend sent the form data, including the user’s email address, straight to a Google Sheet through Google’s API.

At that point I didn’t really understand how a frontend and a backend should talk to each other, so it made sense in my head. It didn’t make sense for security.

The lesson here is simple and it’s the one I’d tattoo on a first-year dev: anything the browser can reach, a stranger can reach too. In my case, nobody had signed up yet, so there was no real user data to expose, and I have no evidence that anyone accessed the sheet. But I also had no way of knowing, and that’s the real problem.

I posted an Instagram reel about the site that same night. Very cool, very secure.

The hosting detour

The site was on Hostinger, which my father chose because it was the easiest option. For a static site, it is. But for my workflow, it wasn’t: every small change meant building the project, zipping it, uploading it through the file manager, and extracting it.

I looked into connecting GitHub for automatic deploys, hit what looked like a paywall, and assumed I’d need to pay for a CI/CD setup. I was wrong, and I only found that out later.

So, I decided to try something else. I had a Raspberry Pi lying around.

How hard could it be?

Not very, it turned out. Just very confusing.

The new architecture

User → Cloudflare Tunnel → Nginx → /api → Gunicorn (Django)
                                 → everything else → React build (static files)
Enter fullscreen mode Exit fullscreen mode

Here’s how it works:

  1. Every request first goes through a Cloudflare Tunnel. The Pi makes an outbound connection to Cloudflare, so web traffic reaches it without me opening ports for the website.
  2. Nginx sits in front as a reverse proxy. If the path starts with /api, it hands the request to the backend. Otherwise, it serves the React build directly.
  3. Gunicorn runs the Django app and handles the API requests.

For email, I used the Resend API. Partly because it’s good, and partly because I couldn’t figure out Django’s built-in email setup.

I was using a Raspberry Pi 3. It only has a 1 GB ram limit, so i had to monitor it carefully.

Learning Django from scratch

Before the real backend, I learned Django from scratch: views, URLs, models, and how everything connects through WSGI. It took me about a day, including a small to-do app I built to practice, which is on my GitHub: https://github.com/Jeet-Bubna/Notes-App . In that same time I built the whole backend apart from the scraper debugging, because I already had most of the frontend and porting it to Vite wasn’t hard.

The updates tab, or: the hard part

The client wanted an updates tab that collects announcements from the main income tax and GST sites in one place. It turned out to be the hardest part of the project.

Attempt 1: requests and BeautifulSoup. This works for most scraping. But on an earlier project where I’d tried to scrape the NSE, I’d learned that plain requests often fails on government and financial sites, so I wasn’t surprised.

Attempt 2: a headless browser. I didn’t know Selenium as a tool, but I understood the idea of an automated browser, so I went looking for one. That didn’t work either.

Attempt 3: just read what the page does. I opened Chrome dev tools, went to the Network tab, and looked at the requests the page makes. Most of these sites load their content from an API, so I could call that directly and skip the HTML. This solved most of them.

Then I got to the Income Tax website, which had no such shortcut. I ended up reading its source for far longer than I’d like to admit. I think it was a word press file with code obfuscation. My vacation ended with the problem unsolved.

The rethink

Weeks later, on a bus ride to school, I thought of a different approach: don’t parse the page myself. Scrape the page, hand the raw HTML to an AI model with a prompt asking for structured JSON, then loop over the result and save it to my Django models.

I used the free tier of Gemini Flash Lite. It sometimes fails under heavy traffic, so the scraper skips those pages and tries again later. The scheduling runs on plain Linux cron jobs. systemd timers are the more modern option, but for a project this small, cron means fewer moving parts. When the model returns a non-compatible answer, it is immediately filtered out.

It worked on my first test, so I moved on to the other pieces: email automation, environment files, and the rest. This is where I should have been more suspicious. Something working once is not the same as it working.

The silent failure

During my Dussehra break, I decided to finish deployment. By Sunday I had almost everything on the Pi: SSH, aliases, Gunicorn, Nginx, and a GitHub runner. The only step left was connecting my domain through the Cloudflare Tunnel.

And right before that last step, I noticed the scraper had stopped working. I have no idea for how long it must've been broken, but it was quite sometime.

I hadn’t noticed because of how I’d written it. The scraper only added new data. If nothing new arrived, nothing complained, because the old data was still in the database. The site looked healthy. It was just slowly going stale.

A system that fails silently is worse than one that crashes. A crash tells you something is wrong. Silence tells you nothing. To my logger, I just added how many records it processed, and based on that we can tell if something is wrong.

I was in a hurry to finish, so I debugged directly on the Pi with vim. I couldn’t reproduce the behavior properly on Windows, because Playwright behaves differently in headless Linux. It isn’t good practice, but it was the only way to test the real environment.

Six hours with a government website

Here’s what I tried, in order:

  1. BeautifulSoup, Playwright, and requests. The server returned 200 OK, but the page just said to enable JavaScript.
  2. Enabling JavaScript in Playwright. No change.
  3. A Playwright stealth plugin. This should have worked. It didn’t.
  4. Asking an AI for help. It suggested running a Windows virtual machine inside the Pi. I did not do that.

That last one is a good reminder about AI tools. I use them, including inside this project, but I’d rather know what I’m building than follow advice I can’t evaluate. Because I understood enough to know a Windows VM on a Pi was a strange answer, I went and read instead.

After about an hour of research, I had a theory. Bot-detection scripts often compare what your HTTP headers claim against what your browser actually is. My headers said I was a Windows browser, but I was running on Linux, and details like navigator.platform and other browser characteristics disagreed with the headers. I think the site saw a mismatch, treated it as automated traffic, and returned the “enable JavaScript” page. This is my best explanation, since the site never told me.

Plain headers weren’t enough. I had to configure the browser context (viewport size and related properties) so it looked consistent, and inject a small script before the page’s own JavaScript runs to hide the markers that give away automation. Instead of relying solely on playwright-stealth (which can sometimes fail silently or have version mismatches on Linux binaries), we injected raw JavaScript before the page even loaded. This explicitly forced navigator.webdriver = false, faked the Chrome runtime object, and mocked browser plugins so the firewall's detection scripts couldn't flag it as automation. After that, the scrapes finally worked.

I’m deliberately not sharing the exact code here.

The pipeline

Once the scraper worked, I set up CI/CD properly with GitHub Actions. When I push to the main branch, a workflow runs and deploys the new version. It runs on a GitHub-hosted runner that SSHs in using repository secrets.

Compare that with zipping and extracting files by hand, and you can see why I was happy.

What I’d do differently

  1. Never put secrets in frontend code. Everything in the browser is public.
  2. Match your hosting to your workflow. The tool wasn’t bad, it just wasn’t built for what I needed.
  3. Alert on silence. Monitor for things not happening, not only for things failing.
  4. Validate AI output. Treat model responses as untrusted input.
  5. Don’t debug in production. Set up a proper Linux test environment.
  6. A Pi is a single point of failure. One power cut and the site is down.

What I took from it

This project taught me more about networking, deployment, and failure than any tutorial did. Most of that came from mistakes: an exposed endpoint, a silent scraper, a hosting plan that didn’t fit. I’m still working on the next stage, which is hardening things properly: monitoring, tighter SSH, and a clearer pipeline.

Because my father, sorry, the client, would rather the code stayed private, I can’t show it. But I’m happy to explain any part of how it works.

Top comments (0)