How the App Works: A Simple Stack by Design

In part 1 of this series, I explained where the idea for this project came from. Here I’ll go over decisions about the stack and some of the architecture decisions.

Architecture Overview

This project is intentionally boring—in a good way.

There are only three real layers:

  1. Static frontend
  2. A thin ASP.NET Core host
  3. The browser as “state”

No APIs (well, eventaully 2, but we’ll get there). No persistence tier. No user model.

Why ASP.NET Core (Without MVC)

I didn’t need:

  • Razor Pages (or any other frameworks)
  • Controllers (the page is the page is the page)
  • Object-Relational Mappings (ORMs)
  • Request pipelines (your browser is doing the lifting; I’m just giving you the mechanism)

ASP.NET Core works beautifully as a static file host with guard rails:

  • Security headers
  • HTTPS behind a reverse proxy
  • Predictable hosting behavior

The app essentially serves:

  • HTML
  • CSS
  • JavaScript
  • A QR code library bundled locally

Frontend Decisions

I purposely avoided frontend frameworks. This is also why I imported (manually, not at runtime) the QR Code library. Let’s keep this thing self-contained and light.

Why?

  • This isn’t a Single Page Application (SPA) – well, it kinda is, but not for the purposes of this acronym
  • No build pipeline required
  • Inspectable by anyone who knows HTML/JS
  • Zero runtime dependencies

Features implemented client‑side:

  • UTM field management
  • URL parsing and extraction
  • QR code generation
  • localStorage save/load
  • State sharing via query parameters
  • Dark theme!!!

Why localStorage Instead of a Database

Campaign configs live entirely in your browser. Did I mention I don’t like it when other organizations have my data? There’s zero chance anything I put into this web page will be accessible by anyone other than me – in this browser instance – until I clear storage.

This provides:

  • Privacy by default
  • No server storage
  • Instant reuse without accounts

If you clear storage, the data is gone—which is exactly the point.

QR Codes Are Generated Client‑Side

QR generation never touches the server.

Benefits:

  • Predictable performance
  • No image storage
  • No server‑side image processing risks

Even optional overlays (logos) are handled in the browser. This led to an interesting (read “near-project-breaking”) conundrum.

Sharing Without Saving

I’m not going to lie. This one was tricky, but something I really wanted to create so I could collaborate with team-members and it’s actually one of my favorite features. It had to support:

  • Full configurations can be serialized into query parameters
  • Paste a URL → recreate the entire campaign

This keeps the server stateless and scalable. But I also wanted it to support the centralized image. That was more difficult until it wasn’t. At the end of the day, an image shown within a QR Code has a size limitation. It can only be “this big” or the QR Code won’t scan. Part of the code automatically downsizes the image to support this. Instead of sharing the entire image (which could be large), why not share the reduced image. The means a small PNG could be under the limitation on the URL + Parameter size, right? I haven’t run into problems, but that doesn’t mean I won’t.

Up Next: Docker

Lastly, I needed this to be survivable. Noting drastic here, just me wanting a quick recovery in case of failure. That led me to Docker as an option. This turned out to be my first published image in Docker Hub, and that feels like a step in the right direction for a project like this.

In the final post, I’ll explain:

  • Why Docker was the final piece
  • How the container is hardened
  • Why I explicitly didn’t include TLS in the app

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.