# Aaron Spindler Blog Corpus

Generated: 2026-07-09T21:06:27.225757+00:00
Format version: agent-blog-post-v2
Canonical blog index: https://aaronspindler.com/api/posts/agent/

This file concatenates the machine-readable Markdown packages for all published Aaronspindler.com blog posts.
Use each article's canonical URL for citation.

## Included posts

- [0011 How My Homelab Grew Up](https://aaronspindler.com/b/tech/0011_How_My_Homelab_Grew_Up/index.md)
- [0009 Be Your Own First Customer](https://aaronspindler.com/b/personal/0009_Be_Your_Own_First_Customer/index.md)
- [0010 Career Timeline](https://aaronspindler.com/b/personal/0010_Career_Timeline/index.md)
- [0008 Personal Site LLM Chat with RAG](https://aaronspindler.com/b/projects/0008_Personal_Site_LLM_Chat_with_RAG/index.md)
- [0007 The Results Of Cutting Out The Bloat](https://aaronspindler.com/b/tech/0007_The_Results_Of_Cutting_Out_The_Bloat/index.md)
- [0006 iMessageLLM](https://aaronspindler.com/b/projects/0006_iMessageLLM/index.md)
- [0005 Knowledge Graph](https://aaronspindler.com/b/tech/0005_Knowledge_Graph/index.md)
- [0004 This Blog Aint Perfect](https://aaronspindler.com/b/personal/0004_This_Blog_Aint_Perfect/index.md)
- [0003 ActionsUptime Build and Walk](https://aaronspindler.com/b/projects/0003_ActionsUptime_Build_and_Walk/index.md)
- [0002 Cut Out The Bloat](https://aaronspindler.com/b/tech/0002_Cut_Out_The_Bloat/index.md)
- [0001 What Even Is This](https://aaronspindler.com/b/personal/0001_What_Even_Is_This/index.md)

---

<!-- post: tech/0011_How_My_Homelab_Grew_Up hash:f5fde98c330800e0534f13b654cd9eae04d88461628f986246638be98128de88 -->

# 0011 How My Homelab Grew Up

> Problem: Instead of discovering a problem because a service feels slow or someone tells me something is down, I can look at resource usage, uptime, logs, and host behavior from one place. Approach: The cheapest way to get a lot of compute was a used Dell C1100: two CPUs, 72 GB of memory, and enough fan noise to make the entire house aware of my choices. Outcome: The homelab works better now because responsibilities...

## Agent Digest

- Problem: Instead of discovering a problem because a service feels slow or someone tells me something is down, I can look at resource usage, uptime, logs, and host behavior from one place.
- Aaron's position: That was probably the first time I learned that infrastructure is physical before it is software.
- Approach: The cheapest way to get a lot of compute was a used Dell C1100: two CPUs, 72 GB of memory, and enough fan noise to make the entire house aware of my choices.
- Outcome: The homelab works better now because responsibilities are clearer.
- Audience fit: Best for readers and agents researching Aaron Spindler's tech writing on 0011 How My Homelab Grew Up.
- Why it matters: How My Homelab Grew Up My homelab did not start with Kubernetes, dashboards, or a carefully planned rack diagram.
- Best for:
  - Understanding the main argument of 0011 How My Homelab Grew Up.
  - Finding citable details from Aaron's tech writing.
  - Answering questions about How My Homelab Grew Up.
  - Answering questions about TL;DR.
  - Answering questions about Mining.
- Not for:
  - Authoritative third-party documentation.
  - A complete substitute for the canonical article body.
- Core claims:
  - That was probably the first time I learned that infrastructure is physical before it is software. (How My Homelab Grew Up)
  - The community era made uptime visible because other people were using the services. (TL;DR)
  - AMD HD 7970 mining rigs turn one gaming PC into a lesson about power, heat, uptime, and dirty real-world environments. (Mining)
- Evidence:
  - My homelab did not start with Kubernetes, dashboards, or a carefully planned rack diagram. (How My Homelab Grew Up)
  - It started in 2012 with a gaming computer, an AMD HD 7970, and the kind of curiosity that makes a teenager ask a dangerous question: what if this machine just ran all the time? (How My Homelab Grew Up)
  - Eventually there were more than six four-GPU systems running in my dad's warehouse, where power was effectively included with the lease. (How My Homelab Grew Up)
  - That was probably the first time I learned that infrastructure is physical before it is software. (How My Homelab Grew Up)
- Caveats:
  - My homelab grew in eras: mining taught physical constraints, game servers made reliability social, Plex made the hardware useful to my family, UniFi made the network part of the platform, Home Assistant made the house programmable, and Proxmox, TrueNAS, Dokploy, Cloudflare, monitoring, Git, and code agents turned it... (TL;DR)
  - The warehouse moved the noise somewhere else, but it introduced harder lessons. (Mining: The First Always-On System)
  - The mining era was messy, but it gave me the first real taste of running systems that did not get to shut off just because I was done using them. (Mining: The First Always-On System)
  - I wrote more about that early community-server era in my career timeline, but the homelab lesson was simple: real users make uptime real. (Game Servers And Real Users)

## Metadata

- Canonical URL: https://aaronspindler.com/b/tech/0011_How_My_Homelab_Grew_Up/
- Markdown URL: https://aaronspindler.com/b/tech/0011_How_My_Homelab_Grew_Up/index.md
- JSON URL: https://aaronspindler.com/b/tech/0011_How_My_Homelab_Grew_Up/index.json
- Category: tech
- Published: 2026-07-08T12:49:58+00:00
- Updated: 2026-07-08T16:58:36.252241+00:00
- Word count: 3109
- Content hash: f5fde98c330800e0534f13b654cd9eae04d88461628f986246638be98128de88
- AI written: no
- Format version: agent-blog-post-v2

## Takeaways

- That was probably the first time I learned that infrastructure is physical before it is software. (How My Homelab Grew Up)
- The community era made uptime visible because other people were using the services. (TL;DR)
- AMD HD 7970 mining rigs turn one gaming PC into a lesson about power, heat, uptime, and dirty real-world environments. (Mining)
- Minecraft, Counter-Strike: Source, Team Fortress 2, Terraria, and Teamspeak make uptime matter to a real community. (Game servers)
- A Dell C1100 with 72 GB of RAM delivers cheap compute and reliable service at the cost of noise and heat. (Enterprise hardware)
- The lab becomes something my family can actually use, not just a collection of loud machines. (Plex)

## Questions Answered

- What problem does 0011 How My Homelab Grew Up address?
- What position does Aaron take in 0011 How My Homelab Grew Up?
- How does 0011 How My Homelab Grew Up approach the problem?
- What evidence or outcomes does 0011 How My Homelab Grew Up provide?
- What does the article explain about How My Homelab Grew Up?
- What does the article explain about TL;DR?

## Agent Queries

- Aaron Spindler "0011 How My Homelab Grew Up"
- 0011 How My Homelab Grew Up tech Aaron Spindler
- 0011 How My Homelab Grew Up Instead of discovering a problem because a service feels slow or someone tells me something is down, I can look at resource usage, uptime, logs, and host behavior from one place.
- 0011 How My Homelab Grew Up How My Homelab Grew Up
- 0011 How My Homelab Grew Up TL;DR

## Follow-Up Questions

- What changed after the approach in 0011 How My Homelab Grew Up was applied?
- What tradeoffs or constraints remain after 0011 How My Homelab Grew Up?
- What setup is required before applying the ideas in 0011 How My Homelab Grew Up?
- How would How My Homelab Grew Up change in a different environment?

## Outline

-   How My Homelab Grew Up
-   TL;DR
-     Mining
-     Game servers
-     Enterprise hardware
-     Plex
-     UniFi
-     NAS and VMs
-     Home Assistant
-     Private cloud
-   Mining: The First Always-On System
-   Game Servers And Real Users
-   Plex Made It Legible
-   The Network Became Infrastructure
-   From Spare Machines To A Small Cluster
-   Home Assistant Made The House Programmable
-   Dokploy And The Private Cloud Arc
-   From Vibes To Signals
-   Git History For The House
-   The Current Shape
-   What Changed

## Body

## How My Homelab Grew Up

My homelab did not start with Kubernetes, dashboards, or a carefully planned rack diagram. It started in 2012 with a gaming computer, an AMD HD 7970, and the kind of curiosity that makes a teenager ask a dangerous question: what if this machine just ran all the time?

At first that meant Bitcoin mining from my bedroom. Then one GPU became more GPUs. One machine became several machines. Eventually there were more than six four-GPU systems running in my dad's warehouse, where power was effectively included with the lease. It was clever until the warehouse reminded me it was not a clean data center. Dust, metal shavings, dirty power, heat, noise, and failing hardware all became part of the lesson.

That was probably the first time I learned that infrastructure is physical before it is software. A service is not just a process. It is power, airflow, cables, fans, storage, dust, the room it lives in, and whoever else has to tolerate it.

## TL;DR

My homelab grew in eras: mining taught physical constraints, game servers made reliability social, Plex made the hardware useful to my family, UniFi made the network part of the platform, Home Assistant made the house programmable, and Proxmox, TrueNAS, Dokploy, Cloudflare, monitoring, Git, and code agents turned it into a small private cloud and home operations platform.

- The early lesson was physical: always-on hardware creates heat, noise, dust, and failure modes.
- The community era made uptime visible because other people were using the services.
- The useful-service era started when Plex gave the homelab a clear purpose beyond tinkering.
- The current era is more production-minded: repeatable config, monitoring, safer access, and real hosted services.
- It is still not finished. The storage layer is the part I most want to make more resilient.

The timeline is not perfectly chronological. A homelab rarely grows in a straight line. It grows at pressure points: when something breaks, becomes useful, gets annoying, or starts mattering to someone other than you.

1. 2012 Mining AMD HD 7970 mining rigs turn one gaming PC into a lesson about power, heat, uptime, and dirty real-world environments.
2. 2012-2014 Game servers Minecraft, Counter-Strike: Source, Team Fortress 2, Terraria, and Teamspeak make uptime matter to a real community.
3. Early lab Enterprise hardware A Dell C1100 with 72 GB of RAM delivers cheap compute and reliable service at the cost of noise and heat.
4. Useful Plex The lab becomes something my family can actually use, not just a collection of loud machines.
5. Network UniFi Cheap routers stop being enough, and the network becomes part of the homelab control plane.
6. Storage NAS and VMs Spare mining and gaming hardware turns into storage, file sharing, VM hosting, Plex, game servers, and Frigate.
7. 2022 Home Assistant After buying a house, automation slowly moves from dashboard tinkering into daily routines.
8. Now Private cloud Proxmox, tiny PCs, TrueNAS, Dokploy, Cloudflare, monitoring, Git, and agents turn the lab into something closer to a small production platform.

## Mining: The First Always-On System

![Open Desktop PC Tower Interior](https://api.photonfolio.com/api/v1/photonfolio/public/embeds/a393f81a-f897-4d24-bf0e-430071b22a87/image/) ![Custom PC Tower on Wooden Table](https://api.photonfolio.com/api/v1/photonfolio/public/embeds/4c074d8d-0b38-4749-8ad0-ecd54a376b88/image/) Mining started as one desktop and grew into dedicated always-on machines.

The Bitcoin mining phase was not elegant. It was consumer hardware doing server-shaped work. The first card ran in my gaming computer, which immediately made heat and noise impossible to ignore. The room got hotter. The fans got louder. The machine needed attention.

Then it grew: more GPUs, more machines, more 24/7 runtime. The warehouse moved the noise somewhere else, but it introduced harder lessons. Dust got everywhere. Metal shavings were a real concern. Dirty power created weird failure modes. Debugging became environmental, not just software.

That was useful. Not comfortable, but useful. It made the invisible parts of infrastructure obvious. Power quality matters. Airflow matters. Physical placement matters. A system that is stable for a gaming session is not automatically stable for weeks of continuous load.

That lesson still shows up in the current homelab. I care about power draw now. I care about heat. I care about whether a service is worth the operational burden it creates. The mining era was messy, but it gave me the first real taste of running systems that did not get to shut off just because I was done using them.

![Stacked Graphics Cards on Workbench](https://api.photonfolio.com/api/v1/photonfolio/public/embeds/3ceaa9d3-0bd4-4ca6-84d6-f8ac8f650720/image/)

## Game Servers And Real Users

My first real "production" users came from gaming, not work. I started with Minecraft because I wanted my own world and my own rules. Then I wanted better plugins, better uptime, better performance, and a server that did not vanish every time I took my MacBook out of the house.

That eventually led me to retired enterprise hardware. The cheapest way to get a lot of compute was a used Dell C1100: two CPUs, 72 GB of memory, and enough fan noise to make the entire house aware of my choices. It was fast, reliable, and wildly inappropriate for a normal room. It was also a great teacher.

![Home Server Rack in a Windowed Room](https://api.photonfolio.com/api/v1/photonfolio/public/embeds/2471e27e-73ef-4c0d-8398-6bc7e4065d60/image/) ![Home Computer Equipment Room](https://api.photonfolio.com/api/v1/photonfolio/public/embeds/71eb3390-fd08-48fe-94cf-89e41e48f6c2/image/) The game-server years made the room itself part of the system: rack gear, heat, fans, and cables.

The game server era grew beyond Minecraft. I ran Counter-Strike: Source, Team Fortress 2, Terraria, and Teamspeak. Falcon Gaming came out of that world too. I had been part of other gaming communities that slowly fell apart because the leadership was flaky or the infrastructure stopped being dependable. I wanted to build somewhere more stable.

That changed the stakes. If the server was slow, people complained. If Teamspeak was down, people noticed. If a game server crashed, it was not an abstract failure on a dashboard. It interrupted whatever people were doing together.

I wrote more about that early community-server era in my [career timeline,](https://aaronspindler.com/b/personal/0010_Career_Timeline/#early-spark) but the homelab lesson was simple: real users make uptime real. You can tell yourself something is just a hobby until other people depend on it. Then reliability becomes part of the feature set.

## Plex Made It Legible

Plex was running around the same time as the gaming services, but it played a different role. Game servers made the homelab useful to friends and online communities. Plex made it useful to my family.

That mattered. Up to that point, a lot of the lab looked like an electric hurricane in my room. Machines were on all the time. Fans were loud. Heat was real. Plex gave me something concrete to show my parents and brother. This is what the hardware does. This is why it is running. This is not just noise for the sake of noise.

That changed how I thought about the lab. It was not enough for something to be technically interesting. The best services earned their place by being used. Plex was one of the first services that felt durable because it had a simple, visible value: people could watch things without caring about the messy infrastructure behind it.

That is still one of my filters. A homelab can absorb infinite complexity if you let it. The question is whether that complexity pays rent. Plex did. A lot of experiments did not.

## The Network Became Infrastructure

![Network infrastructure](https://api.photonfolio.com/api/v1/photonfolio/public/embeds/dd756532-1da6-4306-bff7-32f5a8bf6902/image/)

For a while, the network was whatever the ISP or a cheap Linksys router could tolerate. Eventually that stopped working. The routers would just fall over. Wi-Fi performance and range were poor. Administrative control was almost nonexistent.

That was the point where networking stopped being a background utility and became part of the homelab. The first UniFi gear I bought was a UniFi Security Gateway. Since it was just a wired router, I also had to add access points and switching to make the whole setup usable.

The immediate win was control: visibility, management, and a network I could reason about instead of something I rebooted when it misbehaved.

I am deliberately keeping the network details high-level here. The important part is not the exact topology. It is the shift in mindset. Once you run services at home, the network is not separate from the application. It is the path users take to everything else. If the network is unreliable, the whole homelab feels unreliable.

## From Spare Machines To A Small Cluster

When GPU mining stopped being profitable for me, it left behind a lot of still-useful hardware. Those old mining machines and decommissioned gaming parts became the next stage of the homelab. Instead of buying purpose-built infrastructure, I repurposed what I already had.

The old gaming machine became a storage and VM host. At different points, that class of hardware ran game servers, Plex, NAS workloads, file sharing, and Frigate. It was a practical bridge between the loud enterprise server era and the more intentional setup I have now.

The current storage layer is still built around old personal hardware: TrueNAS on my old gaming machine with an i7-3930K, 32 GB of RAM, five 10 TB hard drives, and a 512 GB SSD cache. It works, and it has been useful, but it is also the part of the system I trust the least from a resilience perspective. There is still a single point of failure there that I want to remove.

Compute has moved in a different direction. Instead of one bigger box doing everything, the lab now uses a distributed set of tiny PCs managed by Proxmox. They handle almost everything except storage. The appeal is straightforward: more compute, better failover options, less power, and cleaner separation between roles.

Frigate also changed the hardware mix. Camera processing made acceleration matter, and that eventually led to a macOS device in the homelab so I could use the NPU and GPU on an M4 chip. That is a very homelab kind of outcome: one workload exposes a constraint, and suddenly the architecture shifts around it.

## Home Assistant Made The House Programmable

Home Assistant entered the picture when I bought my house in 2022. At first it sat quietly in the background with hand-designed automations and dashboards. It was useful, but it had not yet become central.

The automation that made it feel real was my morning routine. When I press a button on my nightstand, or when my phone exits sleep mode, the house starts moving with me. The espresso machine turns on and starts heating. The sound machines throughout the bedrooms turn off. The bathroom light comes on dimly. Heating or cooling adjusts depending on the season and the temperature outside.

Morning automation flow Trigger Nightstand button or phone exits sleep Home Assistant One routine fans out into the house Espresso heats Sound machines stop Bathroom light turns on dimly Climate adjusts

That is the kind of automation that changes how you think about the system. It is not a dashboard anymore. It is part of a daily rhythm. The house becomes programmable in a way that is useful, not theatrical.

It also creates new reliability concerns. My current Home Assistant setup still has a weakness: I use a USB Zigbee antenna, which means the VM is effectively tied to a specific Proxmox host. It does not fail over cleanly the way I would like. That is not catastrophic, but it is exactly the kind of constraint that starts bothering you once the automations become part of normal life.

That is another recurring homelab pattern. A setup can be perfectly acceptable when it is an experiment and suddenly feel fragile when it becomes part of the house.

## Dokploy And The Private Cloud Arc

The hosted-services side of the homelab went through its own maturity curve. CapRover was useful for a while because it made self-hosting apps feel approachable. But over time it became harder to trust. I ran into random hiccups that could lock up everything on the VM. Scaling horizontally was not pleasant. DNS validation did not play nicely with Cloudflare. Basic features needed workarounds.

At some point familiarity stopped being a good reason to keep it. Dokploy replaced that layer and became the foundation for hosting my own cloud services. Today it hosts public-facing projects like Photonfolio, Spindlers, AaronSpindler, and Repotopo.

That shift matters because the homelab is not only serving internal tools now. Some of the things running on it are live services. That raises the standard: not just making something work on my LAN, but making it recoverable, understandable, and boring enough to trust.

Cloudflare is a big part of the current architecture. Tunnels give me easy access and tight control without opening public inbound connections into my local network. No port forwarding, less pressure on the UniFi firewall, and fewer exposed edges. The goal is not to pretend the homelab is the same as a cloud region. It is to be honest about what is running at home and put a controlled access layer in front of it.

Access path Public DNS Visitors reach hosted project domains Cloudflare Policy, TLS, and tunnel entry Tunnel Outbound connection from home Dokploy apps Services stay on local hosts The important boundary is directional: public entry lives at Cloudflare; home services connect out.

## From Vibes To Signals

Monitoring is the line between "I think it is fine" and "I can see what is happening." The current homelab has crossed that line.

I use the monitoring stack to keep track of tools hosted across Proxmox, self-hosted GitHub Actions runners, uptime across local and public services, and a central place for logs. The names are familiar: Grafana, Uptime Kuma, Alloy, Prometheus, Loki, UniFi, and Frigate all play a part.

Signals loop Systems Hosts, apps, network, cameras Signals Metrics, uptime, logs, events Views Grafana, Kuma, Loki, Frigate Action Fix, simplify, remove, document feedback to systems Feedback from action returns to systems.

That does not mean everything is perfect. It means failures are less mysterious. Instead of discovering a problem because a service feels slow or someone tells me something is down, I can look at resource usage, uptime, logs, and host behavior from one place. The homelab becomes less dependent on memory and vibes.

This era also includes experiments that did not survive. A local artifact cache and Nexus-apt setup were useful ideas. I wanted faster, more controlled dependency and package flows. But the operational complexity was too high for the value I was getting. Removing something is not failure if it leaves the system clearer.

That is a lesson I keep relearning. A homelab is a great place to try production patterns, but not every production pattern belongs at home. Sometimes the right move is to delete the clever thing.

## Git History For The House

The private repo that manages the homelab exists for three reasons: reproducibility, visibility for agents, and history.

Manual configuration works when the system is small. Click something in a UI, change a value, restart a container, move on. But eventually the question becomes uncomfortable: what changed, why did it change, and could I rebuild this if I had to?

Putting more of the homelab into Git gives me a better answer. It creates a record. It gives code agents something inspectable. It makes reviews and diffs possible. It also forces me to decide what should be treated as configuration, what should stay private, and what should be documented enough that future me can recover it.

Agents now help with a lot of the work around the edges: debugging, investigation, tinkering, docs, validation, upgrades, service maps, and Home Assistant changes. They are not the main character of the homelab story, but they changed what feels maintainable. A repo-managed homelab is much easier to reason about when an agent can inspect the shape of the system, propose a narrow change, and leave a history trail.

That does not remove judgment. It raises the value of good boundaries. Secrets stay secret. Network details do not need to be public. Validation matters. The point is not to automate recklessly. The point is to make repeatable work more repeatable.

## The Current Shape

![Network Equipment Rack in Cabinet](https://api.photonfolio.com/api/v1/photonfolio/public/embeds/631ee3a7-21a5-4799-912d-afb25c33c6da/image/)

The current homelab is a blend of small private cloud, home operations platform, and learning lab. That sounds grander than it feels day to day. In practice, it is a collection of systems that each earned their place because they solve a real problem.

Current system map Access Cloudflare tunnels and DNS Network UniFi routing, wireless, and switching Compute Proxmox-managed tiny PCs Storage TrueNAS and persistent data Apps Dokploy and hosted services Home ops Home Assistant and Frigate Observability Metrics, uptime, logs, and cameras Control plane Git, validation scripts, and code agents

| Layer | Role | Examples |
| --- | --- | --- |
| Access | Controlled public and remote entry points | Cloudflare tunnels and DNS |
| Network | Home routing, wireless, and service connectivity | UniFi |
| Compute | Distributed service hosts | Proxmox-managed tiny PCs |
| Storage | Central persistent data layer | TrueNAS |
| Home operations | Automation, cameras, and daily routines | Home Assistant and Frigate |
| Apps | Public and private hosted services | Dokploy, Photonfolio, Spindlers, AaronSpindler, Repotopo |
| Observability | Uptime, metrics, logs, and host visibility | Grafana, Uptime Kuma, Prometheus, Loki, Alloy |
| Control plane | Repeatable configuration and assisted maintenance | Git, validation scripts, code agents |

The table is intentionally sanitized. The useful idea is the layering, not the exact topology. The homelab works better now because responsibilities are clearer. Storage is storage. Compute is compute. Access is controlled. Monitoring is centralized. Configuration has history.

## What Changed

Maturity ladder Possibility Could I host this? Dependence Other people use it. Operation I can observe and recover it. Resilience I am designing out fragile center points.

The biggest change is that I no longer think of the homelab as a pile of services. I think of it as an operated system.

That does not mean it is enterprise-grade. It means I am applying more of the same instincts I would bring to production work: make it observable, reduce weird manual state, document enough to recover, keep access controlled, and remove things whose complexity no longer pays for itself.

The old homelab optimized for possibility. Could I host this? Could I make that work? Could I squeeze one more service onto this box? The current homelab optimizes for recoverability and uptime. I am hosting real services now, so the architecture has to make those goals realistic.

There is still a lot to improve. The storage layer is the obvious one. Right now it is too central and not resilient enough. If I were building it again from scratch, I would design storage redundancy earlier instead of letting one machine become the center of gravity.

That is probably the most honest place to end this version of the story. The homelab grew up, but it is not finished. It keeps changing because the constraints keep changing. A new workload appears. A service becomes important. A tool gets too fragile. A failure mode becomes visible. The next iteration comes from there.

So the open question is the same one that started the whole thing, just with better judgment now: what should I try next?

## Links

- [career timeline,](https://aaronspindler.com/b/personal/0010_Career_Timeline/#early-spark) (internal)

## Related Posts

- [0010 Career Timeline](https://aaronspindler.com/b/personal/0010_Career_Timeline/)

---

<!-- post: personal/0009_Be_Your_Own_First_Customer hash:e7a49d5bceb1e7375f1724781503c80be56b9d7019591bac469fee0857918f3b -->

# 0009 Be Your Own First Customer

> Problem: I needed a system for the whole roommate lifecycle: finding people, managing the household, and reducing the awkward manual coordination that happens when money and shared space are involved. Approach: The first version was not built because I thought monitoring was an untouched market. Outcome: I knew whether it helped me catch failures faster.

## Agent Digest

- Problem: I needed a system for the whole roommate lifecycle: finding people, managing the household, and reducing the awkward manual coordination that happens when money and shared space are involved.
- Aaron's position: I already know, because I am the one hitting it every day.
- Approach: The first version was not built because I thought monitoring was an untouched market.
- Outcome: I knew whether it helped me catch failures faster.
- Audience fit: Best for readers and agents researching Aaron Spindler's personal writing on 0009 Be Your Own First Customer.
- Why it matters: Be your own first customer I keep starting businesses the same way: I run into something frustrating in my own life, live with it long enough to understand the...
- Best for:
  - Understanding the main argument of 0009 Be Your Own First Customer.
  - Finding citable details from Aaron's personal writing.
  - Answering questions about Be your own first customer.
  - Answering questions about TL;DR.
  - Answering questions about RoomScout.
- Not for:
  - Authoritative third-party documentation.
  - A complete substitute for the canonical article body.
- Core claims:
  - I already know, because I am the one hitting it every day. (Be your own first customer)
  - RoomScout came from needing to find and manage roommates. (TL;DR)
  - After that came all the smaller pieces that do not sound exciting but matter a lot when you live with other people: splitting bills, keeping things transparent, coordinating chores, and making sure everyone understood what was going on. (RoomScout)
- Evidence:
  - I keep starting businesses the same way: I run into something frustrating in my own life, live with it long enough to understand the sharp edges, and then build the product I wish already existed. (Be your own first customer)
  - After that came all the smaller pieces that do not sound exciting but matter a lot when you live with other people: splitting bills, keeping things transparent, coordinating chores, and making sure everyone understood what was going on. (RoomScout)
  - I was not starting with a market map or a pitch deck. (RoomScout)
  - I was starting with a real house, real rooms, real bills, and a real need to make the situation less painful. (RoomScout)
- Caveats:
  - After that came all the smaller pieces that do not sound exciting but matter a lot when you live with other people: splitting bills, keeping things transparent, coordinating chores, and making sure everyone understood what was going on. (RoomScout)
  - Remote work is great in a lot of ways, but it can make coworkers feel strangely flat. (Team Bio)
  - That is useful, but it is not the whole picture. (ActionsUptime)
  - But when the same friction keeps coming back, and when the solution starts to feel useful beyond me, that is usually when I start taking it seriously. (The pattern)

## Metadata

- Canonical URL: https://aaronspindler.com/b/personal/0009_Be_Your_Own_First_Customer/
- Markdown URL: https://aaronspindler.com/b/personal/0009_Be_Your_Own_First_Customer/index.md
- JSON URL: https://aaronspindler.com/b/personal/0009_Be_Your_Own_First_Customer/index.json
- Category: personal
- Published: 2026-05-21T17:50:14.636182+00:00
- Updated: 2026-07-08T16:59:15.977712+00:00
- Word count: 1067
- Content hash: e7a49d5bceb1e7375f1724781503c80be56b9d7019591bac469fee0857918f3b
- AI written: no
- Format version: agent-blog-post-v2

## Takeaways

- I already know, because I am the one hitting it every day. (Be your own first customer)
- RoomScout came from needing to find and manage roommates. (TL;DR)
- After that came all the smaller pieces that do not sound exciting but matter a lot when you live with other people: splitting bills, keeping things transparent, coordinating chores, and making sure everyone understood what was going on. (RoomScout)
- They were answers to something I personally felt: remote teams need lightweight ways to build familiarity before every relationship has to be forced through another meeting. (Team Bio)
- The first version was not built because I thought monitoring was an untouched market. (ActionsUptime)
- Outdoor trips, hikes, family events, random experiments, and all the almost-good shots that pile up around the good ones. (Photonfolio)

## Questions Answered

- What problem does 0009 Be Your Own First Customer address?
- What position does Aaron take in 0009 Be Your Own First Customer?
- How does 0009 Be Your Own First Customer approach the problem?
- What evidence or outcomes does 0009 Be Your Own First Customer provide?
- What does the article explain about Be your own first customer?
- What does the article explain about TL;DR?

## Agent Queries

- Aaron Spindler "0009 Be Your Own First Customer"
- 0009 Be Your Own First Customer personal Aaron Spindler
- 0009 Be Your Own First Customer I needed a system for the whole roommate lifecycle: finding people, managing the household, and reducing the awkward manual coordination that happens when money and shared space are involved.
- 0009 Be Your Own First Customer Be your own first customer
- 0009 Be Your Own First Customer TL;DR

## Follow-Up Questions

- What changed after the approach in 0009 Be Your Own First Customer was applied?
- What tradeoffs or constraints remain after 0009 Be Your Own First Customer?
- What setup is required before applying the ideas in 0009 Be Your Own First Customer?
- How would Be your own first customer change in a different environment?

## Outline

-   Be your own first customer
-   TL;DR
-   RoomScout
-   Team Bio
-   ActionsUptime
-   Photonfolio
-   The pattern

## Body

## Be your own first customer

I keep starting businesses the same way: I run into something frustrating in my own life, live with it long enough to understand the sharp edges, and then build the product I wish already existed.

The first customer is usually me. That has become one of the clearest filters I have for deciding what is worth building. I do not have to invent a fake user story or guess where the friction is. I already know, because I am the one hitting it every day.

## TL;DR

A lot of the businesses I have started came from my own problems first. RoomScout came from needing to find and manage roommates. Team Bio came from wanting remote coworkers to feel less like names in Slack. ActionsUptime came from needing better visibility into GitHub Actions and service uptime. Photonfolio came from needing a better way to manage, edit, and share my growing photo library.

## RoomScout

[RoomScout](https://github.com/aaronspindler/RoomScout) started because I had rooms to fill.

When I was in university, I rented a whole house and had to figure out how to find roommates. Finding people was only the first part. After that came all the smaller pieces that do not sound exciting but matter a lot when you live with other people: splitting bills, keeping things transparent, coordinating chores, and making sure everyone understood what was going on.

I did not need another classified ad. I needed a system for the whole roommate lifecycle: finding people, managing the household, and reducing the awkward manual coordination that happens when money and shared space are involved.

So I built RoomScout. It became a roommate marketplace and management platform because that was what I wished existed. I was not starting with a market map or a pitch deck. I was starting with a real house, real rooms, real bills, and a real need to make the situation less painful.

## Team Bio

[Team Bio](https://github.com/aaronspindler/Team.Bio) came from a different kind of gap.

Remote work is great in a lot of ways, but it can make coworkers feel strangely flat. You might work with someone every week, trust their judgment, and still know almost nothing about them outside of tickets, meetings, and Slack messages.

When I joined a fully remote company, I felt that gap quickly. I wanted an easier way to understand who people were, what they cared about, where they were coming from, and what might make it easier to connect with them as actual people.

Team Bio was my attempt to build that missing layer. Profiles, trivia, coffee chats, and shared context were not features for the sake of features. They were answers to something I personally felt: remote teams need lightweight ways to build familiarity before every relationship has to be forced through another meeting.

## ActionsUptime

ActionsUptime came from a recurring operational annoyance: I had a lot of projects, a lot of GitHub Actions, and not enough confidence that I would notice when something quietly failed.

Normal uptime monitoring tells you whether a site is reachable. That is useful, but it is not the whole picture. A project can look fine from the outside while deployments, scheduled jobs, tests, backups, or other automation are failing behind the scenes.

I wanted one place to see both service uptime and the health of the automation keeping those services moving. That became [ActionsUptime](https://aaronspindler.com/b/projects/0003_ActionsUptime_Build_and_Walk/) .

The first version was not built because I thought monitoring was an untouched market. It was built because I had my own mess of repos and services, and I wanted fewer surprises. When a tool starts that way, the requirements are sharper. I did not need to guess whether the dashboard was useful. I knew whether it helped me catch failures faster.

## Photonfolio

[Photonfolio](https://photonfol.io) is the current version of this pattern.

My photo library keeps growing. Outdoor trips, hikes, family events, random experiments, and all the almost-good shots that pile up around the good ones. Managing that gets messy fast. Editing is one part. Sorting is another. Finding the right photo later is another. Sharing albums from a trip without turning it into a chore is another.

I wanted a system that could help me manage and edit that library, while also making it easy to share photos from outdoor trips in a way that felt clean and intentional. Not just dump a folder somewhere. Not just send a handful of compressed images in a chat. I wanted a place where the archive, the editing workflow, and the sharing experience could all fit together.

That is why Photonfolio exists. It is not abstract for me. I have the photos. I have the backlog. I have the trips I want to share. I know the pain of looking through too many similar shots and trying to turn them into something people can actually enjoy.

## The pattern

The thread across all of these is pretty simple: I trust problems I have personally lived with.

That does not mean every personal annoyance should become a company. A lot of them should stay as scripts, notes, or tiny tools that make life easier. But when the same friction keeps coming back, and when the solution starts to feel useful beyond me, that is usually when I start taking it seriously.

Being your own first customer gives you a few advantages. You get immediate feedback. You can tell when the product is pretending to solve the problem versus actually solving it. You are less likely to get distracted by features that sound good but do not change the daily experience. You also have to face the uncomfortable question of whether you would use the product if nobody was watching.

That last part is important. It is easy to build something that looks like a product. It is harder to build something that earns its way into your own routine.

I keep coming back to this because it makes the work more honest. RoomScout had to help me manage a real house. Team Bio had to make remote work feel a little more human. ActionsUptime had to tell me when my own systems were failing. Photonfolio has to make my photo library easier to live with.

If I can make something useful enough for myself first, then I have a real starting point. Not a guarantee, but a starting point. And for me, that has always been the best place to build from.

## Links

- [RoomScout](https://github.com/aaronspindler/RoomScout) (external)
- [Team Bio](https://github.com/aaronspindler/Team.Bio) (external)
- [ActionsUptime](https://aaronspindler.com/b/projects/0003_ActionsUptime_Build_and_Walk/) (internal)
- [Photonfolio](https://photonfol.io) (external)

## Related Posts

- [0003 ActionsUptime Build and Walk](https://aaronspindler.com/b/projects/0003_ActionsUptime_Build_and_Walk/)

---

<!-- post: personal/0010_Career_Timeline hash:af0e7999a08fc244f3f0a5eecd48ead2eaa47b95755a02fbdce44cd285aa0fb6 -->

# 0010 Career Timeline

> Problem: I started with Minecraft, mostly because I wanted my own world and my own rules. Approach: In 2020, I joined the Canada Border Services Agency through an innovation program focused on government proofs of concept. Outcome: I got into it because I liked figuring out how stuff worked, then tinkering with it until it worked better.

## Agent Digest

- Problem: I started with Minecraft, mostly because I wanted my own world and my own rules.
- Aaron's position: I got into it because I liked figuring out how stuff worked, then tinkering with it until it worked better.
- Approach: In 2020, I joined the Canada Border Services Agency through an innovation program focused on government proofs of concept.
- Outcome: I got into it because I liked figuring out how stuff worked, then tinkering with it until it worked better.
- Audience fit: Best for readers and agents researching Aaron Spindler's personal writing on 0010 Career Timeline.
- Why it matters: I got into it because I liked figuring out how stuff worked, then tinkering with it until it worked better.
- Best for:
  - Understanding the main argument of 0010 Career Timeline.
  - Finding citable details from Aaron's personal writing.
  - Answering questions about Article introduction.
  - Answering questions about TL;DR.
  - Answering questions about The Early Spark: Game Servers and Community.
- Not for:
  - Authoritative third-party documentation.
  - A complete substitute for the canonical article body.
- Core claims:
  - I got into it because I liked figuring out how stuff worked, then tinkering with it until it worked better. (Article introduction)
  - University: studied Computer Science at Ontario Tech University, then learned school was not always the best fit for how I learn. (TL;DR)
  - I started with Minecraft, mostly because I wanted my own world and my own rules. ([The Early Spark: Game Servers and Community](#early-spark))
- Evidence:
  - I got into it because I liked figuring out how stuff worked, then tinkering with it until it worked better. (Article introduction)
  - I started by playing, then got curious about hosting my own server, then ran one from my MacBook until I discovered the practical problem: if I took the laptop out of the house, the server went down. (Article introduction)
  - That eventually led to a retired 1U enterprise server with 72 GB of RAM, which felt completely ridiculous at the time. (Article introduction)
  - My first real experience with "production" systems came from gaming, not school. ([The Early Spark: Game Servers and Community](#early-spark))
- Caveats:
  - It had 72 GB of RAM, which felt unreal, but it also made the kind of noise that makes everyone else in the house question your choices. ([The Early Spark: Game Servers and Community](#early-spark))
  - That was probably my first honest lesson in infrastructure tradeoffs. ([The Early Spark: Game Servers and Community](#early-spark))
  - I originally joined because I was interested in programming, but I ended up loving the camaraderie and the challenge just as much. ([Learning Through Building: Robotics and the Value of Iteration](#robotics))
  - I was writing code, but I was also wiring controllers, dealing with sensors, and working closely with the mechanical side. ([Learning Through Building: Robotics and the Value of Iteration](#robotics))

## Metadata

- Canonical URL: https://aaronspindler.com/b/personal/0010_Career_Timeline/
- Markdown URL: https://aaronspindler.com/b/personal/0010_Career_Timeline/index.md
- JSON URL: https://aaronspindler.com/b/personal/0010_Career_Timeline/index.json
- Category: personal
- Published: 2026-05-21T17:45:41.629188+00:00
- Updated: 2026-07-08T16:59:19.286690+00:00
- Word count: 2297
- Content hash: af0e7999a08fc244f3f0a5eecd48ead2eaa47b95755a02fbdce44cd285aa0fb6
- AI written: no
- Format version: agent-blog-post-v2

## Takeaways

- I got into it because I liked figuring out how stuff worked, then tinkering with it until it worked better. (Article introduction)
- University: studied Computer Science at Ontario Tech University, then learned school was not always the best fit for how I learn. (TL;DR)
- I started with Minecraft, mostly because I wanted my own world and my own rules. ([The Early Spark: Game Servers and Community](#early-spark))
- I originally joined because I was interested in programming, but I ended up loving the camaraderie and the challenge just as much. ([Learning Through Building: Robotics and the Value of Iteration](#robotics))
- I learn best when I can connect the material to a real system, a real need, or something I actually need to build. ([Formal Education: Computer Science Foundations](#formal-education))
- In 2014 I started consulting under Spindlers, and I've worked with businesses to design, build, and maintain web and desktop applications across the full SDLC. (Consulting and Early Products)

## Questions Answered

- What problem does 0010 Career Timeline address?
- What position does Aaron take in 0010 Career Timeline?
- How does 0010 Career Timeline approach the problem?
- What evidence or outcomes does 0010 Career Timeline provide?
- What does the article explain about Article introduction?
- What does the article explain about TL;DR?

## Agent Queries

- Aaron Spindler "0010 Career Timeline"
- 0010 Career Timeline personal Aaron Spindler
- 0010 Career Timeline I started with Minecraft, mostly because I wanted my own world and my own rules.
- 0010 Career Timeline Article introduction
- 0010 Career Timeline TL;DR

## Follow-Up Questions

- What changed after the approach in 0010 Career Timeline was applied?
- What tradeoffs or constraints remain after 0010 Career Timeline?
- What setup is required before applying the ideas in 0010 Career Timeline?
- How would Article introduction change in a different environment?

## Outline

-   TL;DR
-   [The Early Spark: Game Servers and Community](#early-spark)
-   [Learning Through Building: Robotics and the Value of Iteration](#robotics)
-   [Formal Education: Computer Science Foundations](#formal-education)
-   Consulting and Early Products
-   [Automation at Scale: Canada Border Services Agency](#cbsa)
-   [ShippingTree: Reliability, Tests, and Delivery](#shippingtree)
-   [Pearl Health: Building for Primary Care](#pearl-health)
-     [Team Bio](#team-bio)
-   Achievements
-   Continuous Learning
-   How I Think About the Work
-   Lessons Learned
-   What I Want To Build Next

## Body

I didn't get into tech because it was trendy. I got into it because I liked figuring out how stuff worked, then tinkering with it until it worked better. The first version of that was Minecraft. I started by playing, then got curious about hosting my own server, then ran one from my MacBook until I discovered the practical problem: if I took the laptop out of the house, the server went down.

That eventually led to a retired 1U enterprise server with 72 GB of RAM, which felt completely ridiculous at the time. It was fast, loud, and absolutely not appropriate to run in a normal house. It also taught me a lot. The stakes got bigger over time: robotics, school, consulting, government systems, startups, and now healthcare payments. The thread is still the same: build the system, run it, find the weak spots, and make it better.

## TL;DR

I got hooked on tech through Minecraft servers, then kept following that curiosity through robotics, computer science, consulting, startup work, government automation, and healthtech. Over time I started caring less about hype and more about whether a system is clear, observable, and dependable. These days I work on payment and performance systems in healthcare, focused on automation, specs, and getting the data right.

- Early years: hosted game servers from a MacBook, then from a very loud retired enterprise server.
- High school: joined robotics for programming and stayed for the camaraderie, pressure, and challenge.
- University: studied Computer Science at Ontario Tech University, then learned school was not always the best fit for how I learn.
- Career: consulting and product work, plus automation and platform roles at CBSA, ShippingTree, and Pearl Health.
- Now: Senior Software Engineer at Pearl Health, focused on payments, performance, and trustworthy data.

Experience

Calendar years

1.
2.
3.
4.
5.
6.
7.
8.
9.
10.

<a id="early-spark"></a>

## The Early Spark: Game Servers and Community

My first real experience with "production" systems came from gaming, not school. From 2012 to 2014, I ran and maintained game servers for a community called Falcon Gaming. I started with Minecraft, mostly because I wanted my own world and my own rules. Then I wanted plugins, better uptime, better performance, and a server that did not disappear every time I left the house with my laptop.

The retired 1U server solved one problem and created others. It had 72 GB of RAM, which felt unreal, but it also made the kind of noise that makes everyone else in the house question your choices. That was probably my first honest lesson in infrastructure tradeoffs. Performance, uptime, cost, heat, noise, and real users all mattered at the same time.

I was also moderating the community and building tools to keep things fair. It was the first time I felt the full loop: build it, run it, fix it, improve it. If the server was down or lagging, nobody cared how clever the plugin was. That lesson has followed me into every serious system I've worked on since.

<a id="robotics"></a>

## Learning Through Building: Robotics and the Value of Iteration

In 2015, I became the lead programmer for Team 3710 Cyber Falcons. I originally joined because I was interested in programming, but I ended up loving the camaraderie and the challenge just as much. Robotics had a way of making software feel physical. A bug was not just a stack trace. Sometimes it was a robot turning the wrong way, missing a sensor reading, or failing right when everyone was watching.

We designed, built, and programmed robots for sports-themed competitions across Ontario. The team placed first at a regional event, and students I mentored went on to win a World Championship. Robotics taught me how to think in systems, work with a team, and ship under real-time pressure. When competition day is coming, you don't get to debate architecture for three weeks. You build, you test, you fix it, and you go again.

This was also where I first got comfortable working across disciplines. I was writing code, but I was also wiring controllers, dealing with sensors, and working closely with the mechanical side. That habit of crossing boundaries has helped me in almost every job since.

<a id="formal-education"></a>

## Formal Education: Computer Science Foundations

I studied Computer Science at Ontario Tech University from 2015 to 2017. At the time, it felt like the default right path. I liked computers, I liked programming, and school seemed like the official way to turn that into a career. The truth was more complicated. I did not love school, and I had a hard time with subjects that did not catch my interest.

I didn't finish the degree, but that time still mattered. It gave me more theory than I had before: algorithms, data structures, and a better way to think about complexity. It also taught me something useful about myself. I learn best when I can connect the material to a real system, a real need, or something I actually need to build.

Even during school, I kept working on real systems. That mix of theory and practice became a pattern pretty early: learn the fundamentals, but stay grounded in shipping systems that actually work.

## Consulting and Early Products

Spindlers started as my first real exposure to the overlap between business and programming. The short version is that I got grounded, and the only way I could touch a computer was if I built my dad's company a new website. That was an effective loophole. It also showed me that software was not just a personal hobby. It could make a business look more serious, bring in customers, and solve real operational problems.

In 2014 I started consulting under Spindlers, and I've worked with businesses to design, build, and maintain web and desktop applications across the full SDLC. Consulting forced me to get pragmatic fast. It's easy to over-engineer when you're building for yourself. Client work doesn't let you hide there for long. You learn to care about clear requirements, reliable delivery, and getting feedback early.

In 2019 I started RoomScout.ca, a roommate management and marketplace platform. It came from a problem I already had during university: I had rented a whole house and needed to find and manage roommates. The pain was not just finding people. It was bill splitting, transparency, household coordination, and small recurring chores like garbage and recycling schedules.

RoomScout became a full product from idea to production, built with Django, Redis, Celery, and AWS. I added automated moderation using AWS Rekognition and sentiment analysis, which made me think harder about the gap between a feature that sounds smart and one that actually saves operational time. RoomScout taught me how fast product complexity and technical debt show up when you're moving quickly and making real tradeoffs.

<a id="cbsa"></a>

## Automation at Scale: Canada Border Services Agency

In 2020, I joined the Canada Border Services Agency through an innovation program focused on government proofs of concept. It was my first real look at public-sector systems at scale, and it was eye-opening. There were too many workflows where skilled people were spending their time clicking through web interfaces instead of doing higher-value work.

One of the major projects was an automation application that reduced processing time for permit holders at points of entry by 93%, creating more than $10M in annual savings. The part that stuck with me is how small the team was. A developer in Victoria, BC, and I were able to help remove a huge amount of repetitive manual work. That was a big moment for me. It showed me how software can directly improve real outcomes for people and institutions, not just make a dashboard look nicer.

Technically, it was a deep dive into asynchronous processing and automation. The system used task queues and tools like Selenium and BeautifulSoup to automate workflows, and the surrounding work included CI/CD, training, and agency-wide API standards. It wasn't glamorous, but it worked. That job really cemented my belief that boring tech, done well, can have a huge impact.

<a id="shippingtree"></a>

## ShippingTree: Reliability, Tests, and Delivery

From 2021 to 2022 I worked at ShippingTree. I joined a startup because government work moved slowly and did not pay very well. I also learned that third-party fulfillment was not the most fulfilling industry for me, which is a joke I am legally obligated to make at least once.

The work itself still taught me a lot. I led design and development for new features and integrations, increased test coverage from 5% to 67%, and put a CI/CD pipeline in place. That immediately improved reliability and release velocity. It also reinforced something I still believe strongly: investing in quality systems early pays off.

I also helped reduce technical debt during a migration from an MVT architecture to a microservices architecture using Django REST Framework and React. Those transitions can get messy fast, but sometimes they're the right call for scale. The main lesson I took from it was simple: migrate with intent, and don't forget the people who have to maintain the system after you're done.

<a id="pearl-health"></a>

## Pearl Health: Building for Primary Care

I joined Pearl Health in January 2022 as a Software Engineer and became a Senior Software Engineer in June 2023. I wanted to work at a healthtech startup with a promising goal: make healthcare better and help reduce Medicare cost inflation. That mission mattered to me because the software is connected to something much bigger than the software itself.

I currently focus on payment and financial performance systems for primary care. A big part of that work has been revamping payment validations and related workflows so providers get paid correctly and on time each month. The stakes are clear: if the system fails, people don't get paid and trust disappears fast.

I also write technical specifications for integrating financial performance and care quality indicators into client-facing applications. That means taking messy complexity and turning it into something clear and useful for providers, while keeping the underlying systems auditable. The work spans Python, Django, TypeScript, PostgreSQL, dbt, AWS, and infrastructure automation, and it has included payment architecture handling more than $55M across 500+ providers. It's a mix of backend systems, data engineering, and product impact, and honestly it's the best blend of complexity and meaning I've found so far.

<a id="team-bio"></a>

### Team Bio

I founded Team Bio in 2023 and led strategy and product delivery for the venture through 2025 alongside my main career track. The idea came from joining Pearl in a fully remote environment and realizing how hard it was to learn about coworkers as actual people. You could work with someone every week and still know almost nothing about them outside of meetings and tickets.

Team Bio was my attempt to solve that problem while also learning new tools. The product combined profiles, trivia, coffee chats, and shared context, with integrations using OpenAI, Mapbox, Stripe, and Google APIs. Working on it pushed me further into product thinking, and it gave me a useful way to explore AI without treating it like a gimmick.

## Achievements

- Reduced permit-holder processing time by 93% in a CBSA automation project, generating more than $10M in annual savings.
- Increased test coverage from 5% to 67% and implemented CI/CD at ShippingTree.
- Led payment architecture and validation improvements at Pearl Health across more than $55M in payments for 500+ providers.
- Built RoomScout.ca end-to-end to solve real roommate-management problems like bill splitting, transparency, and household scheduling.
- Founded Team Bio and shipped an early product that combined profiles, trivia, coffee chats, and API integrations into one workflow.
- Led a robotics team to a regional win and mentored students who later won a World Championship.

## Continuous Learning

I try to keep learning a little outside my day-to-day work too. Some recent certifications and courses that have shaped how I think:

- Understanding Neural Networks
- The Definitive Guide to Celery and Django
- Mastering Django
- The Complete Self-Driving Car
- Python for Financial Analysis and Algorithmic Trading

## How I Think About the Work

Over the years, I've moved away from chasing frameworks and more toward chasing outcomes. The early server days were a blunt version of that lesson. Nobody cared what the setup looked like if the server was down, slow, or too loud to live with. The same idea applies to bigger systems. If a system handles money, people, or time, it needs to be clear, observable, and durable.

I also think strong specs and good defaults are underrated. A clear technical spec is often worth more than rushing into code. It gets people aligned, cuts down on rework, and makes quality visible earlier. That mindset has helped me deliver more predictable outcomes, especially in messy domains like healthcare.

## Lessons Learned

- Reliability is a feature. If the system is down, nothing else matters.
- Automation is leverage. You can scale impact without scaling headcount.
- Tests are a speed multiplier. They enable you to move faster, not slower.
- Simple does not mean easy. It means fewer moving parts and clearer ownership.
- Real constraints teach fast. A MacBook server, a loud rack server, a robot, and a payment system all make different tradeoffs visible.
- Shipping beats theorizing. Real users are the ultimate test.

## What I Want To Build Next

I want to keep working on systems where accuracy and trust matter. Payments, healthcare outcomes, data pipelines, and decision support are all areas where good software can make life better for people. My focus is to keep building systems that are dependable, scalable, and understandable: software that quietly does its job well for a long time.

If you want more context on how I think about building on the web, I have already written a bit about it in [my first post](https://aaronspindler.com/b/personal/0001_What_Even_Is_This/) , [Cut Out The Bloat](https://aaronspindler.com/b/tech/0002_Cut_Out_The_Bloat/) , and [Be Your Own First Customer](https://aaronspindler.com/b/personal/0009_Be_Your_Own_First_Customer/) .

## Links

- [my first post](https://aaronspindler.com/b/personal/0001_What_Even_Is_This/) (internal)
- [Cut Out The Bloat](https://aaronspindler.com/b/tech/0002_Cut_Out_The_Bloat/) (internal)
- [Be Your Own First Customer](https://aaronspindler.com/b/personal/0009_Be_Your_Own_First_Customer/) (internal)

## Related Posts

- [0001 What Even Is This](https://aaronspindler.com/b/personal/0001_What_Even_Is_This/)
- [0002 Cut Out The Bloat](https://aaronspindler.com/b/tech/0002_Cut_Out_The_Bloat/)
- [0009 Be Your Own First Customer](https://aaronspindler.com/b/personal/0009_Be_Your_Own_First_Customer/)

---

<!-- post: projects/0008_Personal_Site_LLM_Chat_with_RAG hash:186da7c89fb252863590ecd12da6503a7bf219870729123897ee32173e4de820 -->

# 0008 Personal Site LLM Chat with RAG

> Problem: Personal sites are easy to browse but awkward to query. Approach: I added a retrieval-grounded chat interface to /chat on this site. Outcome: The combined score is weighted and thresholded, then top results are used to build prompt context.

## Agent Digest

- Problem: Personal sites are easy to browse but awkward to query.
- Aaron's position: If you're building similar "small but real" AI features, this is the stuff that actually matters.
- Approach: I added a retrieval-grounded chat interface to /chat on this site.
- Outcome: The combined score is weighted and thresholded, then top results are used to build prompt context.
- Audience fit: Best for readers and agents researching Aaron Spindler's projects writing on 0008 Personal Site LLM Chat with RAG.
- Why it matters: Personal Site LLM Chat with RAG: Architecture, Tradeoffs, and Production Behavior I added a retrieval-grounded chat interface to /chat on this site.
- Best for:
  - Understanding the main argument of 0008 Personal Site LLM Chat with RAG.
  - Finding citable details from Aaron's projects writing.
  - Answering questions about Personal Site LLM Chat with RAG: Architecture, Tradeoffs, and Production Behavior.
  - Answering questions about Problem Definition and Constraints.
  - Answering questions about Request Lifecycle (Both Paths).
- Not for:
  - Authoritative third-party documentation.
  - A complete substitute for the canonical article body.
- Core claims:
  - If you're building similar "small but real" AI features, this is the stuff that actually matters. (Personal Site LLM Chat with RAG: Architecture, Tradeoffs, and Production Behavior)
  - I set a few hard constraints up front: Grounded only: no open-ended model answers without retrieval context. (Problem Definition and Constraints)
  - On miss, load retrieval context (with its own cache keyed by normalized question). (Request Lifecycle (Both Paths))
- Evidence:
  - I set a few hard constraints up front: Grounded only: no open-ended model answers without retrieval context. (Problem Definition and Constraints)
  - On miss, load retrieval context (with its own cache keyed by normalized question). (Request Lifecycle (Both Paths))
  - This gives me one reliable SSR baseline and one richer streaming path without splitting product behavior into two different systems. (Request Lifecycle (Both Paths))
  - Retrieval comes from SemanticDocument and uses hybrid ranking (Postgres text rank + vector similarity). (Retrieval Pipeline: Hybrid Search, Not Guessing)
- Caveats:
  - If you're building similar "small but real" AI features, this is the stuff that actually matters. (Personal Site LLM Chat with RAG: Architecture, Tradeoffs, and Production Behavior)
  - Personal sites are easy to browse but awkward to query. (Problem Definition and Constraints)
  - I set a few hard constraints up front: Grounded only: no open-ended model answers without retrieval context. (Problem Definition and Constraints)
  - Default TTLs: RAG_CACHE_TTL_SECONDS = 3600 RAG_QUERY_CONTEXT_CACHE_TTL_SECONDS = 900 RAG_EMBED_QUERY_CACHE_TTL_SECONDS = 3600 The important design choice is that context reuse is allowed across conversation states, but final answer reuse is not unless history matches. (Caching Strategy: Split by Responsibility)

## Metadata

- Canonical URL: https://aaronspindler.com/b/projects/0008_Personal_Site_LLM_Chat_with_RAG/
- Markdown URL: https://aaronspindler.com/b/projects/0008_Personal_Site_LLM_Chat_with_RAG/index.md
- JSON URL: https://aaronspindler.com/b/projects/0008_Personal_Site_LLM_Chat_with_RAG/index.json
- Category: projects
- Published: 2026-03-26T17:04:55+00:00
- Updated: 2026-07-08T16:59:20.862261+00:00
- Word count: 1043
- Content hash: 186da7c89fb252863590ecd12da6503a7bf219870729123897ee32173e4de820
- AI written: no
- Format version: agent-blog-post-v2

## Takeaways

- If you're building similar "small but real" AI features, this is the stuff that actually matters. (Personal Site LLM Chat with RAG: Architecture, Tradeoffs, and Production Behavior)
- I set a few hard constraints up front: Grounded only: no open-ended model answers without retrieval context. (Problem Definition and Constraints)
- On miss, load retrieval context (with its own cache keyed by normalized question). (Request Lifecycle (Both Paths))
- The combined score is weighted and thresholded, then top results are used to build prompt context. (Retrieval Pipeline: Hybrid Search, Not Guessing)
- Source entries missing title or URL are filtered out before rendering, so the UI never emits dead citation links. (Prompt Contract and Citation Semantics)
- Streaming responses are sent as text/event-stream with explicit event types: event: token data: {"text":"partial answer"} event: sources data: [{"title":"...", "url":"...", "snippet":"..."}] event: error data: {"message":"..."} event: done data: {} The frontend state machine starts with a "Thinking..." assistant... (Streaming Protocol with SSE)

## Questions Answered

- What problem does 0008 Personal Site LLM Chat with RAG address?
- What position does Aaron take in 0008 Personal Site LLM Chat with RAG?
- How does 0008 Personal Site LLM Chat with RAG approach the problem?
- What evidence or outcomes does 0008 Personal Site LLM Chat with RAG provide?
- What does the article explain about Personal Site LLM Chat with RAG: Architecture, Tradeoffs, and Production Behavior?
- What does the article explain about Problem Definition and Constraints?

## Agent Queries

- Aaron Spindler "0008 Personal Site LLM Chat with RAG"
- 0008 Personal Site LLM Chat with RAG projects Aaron Spindler
- 0008 Personal Site LLM Chat with RAG Personal sites are easy to browse but awkward to query.
- 0008 Personal Site LLM Chat with RAG Personal Site LLM Chat with RAG: Architecture, Tradeoffs, and Production Behavior
- 0008 Personal Site LLM Chat with RAG Problem Definition and Constraints

## Follow-Up Questions

- What changed after the approach in 0008 Personal Site LLM Chat with RAG was applied?
- What tradeoffs or constraints remain after 0008 Personal Site LLM Chat with RAG?
- What setup is required before applying the ideas in 0008 Personal Site LLM Chat with RAG?
- How would Personal Site LLM Chat with RAG: Architecture, Tradeoffs, and Production Behavior change in a different environment?

## Outline

- Personal Site LLM Chat with RAG: Architecture, Tradeoffs, and Production Behavior
-   Problem Definition and Constraints
-   Request Lifecycle (Both Paths)
-   Retrieval Pipeline: Hybrid Search, Not Guessing
-   Prompt Contract and Citation Semantics
-   Streaming Protocol with SSE
-   Caching Strategy: Split by Responsibility
-   State and Persistence Model
-   Security and Guardrails
-   Observability and Cost Tracking
-   Tradeoffs I Chose Deliberately
-   What I Want to Improve Next
-   Try It

## Body

# Personal Site LLM Chat with RAG: Architecture, Tradeoffs, and Production Behavior

I added a retrieval-grounded chat interface to [/chat](https://aaronspindler.com/chat/) on this site. The assistant answers only from indexed public content (posts, projects, books, albums, photos, resume when enabled, and public semantic notes), and it links citations back to real pages when sources have URL-backed entries.

This write-up is the technical version: concrete request flow, retrieval/scoring behavior, caching design, SSE protocol, persistence model, and failure handling. If you're building similar "small but real" AI features, this is the stuff that actually matters.

This is part of the same direction I started in [What Even Is This](https://aaronspindler.com/b/0001_what_even_is_this/) : I am constantly improving the site and building new experiments, and I will keep writing blog posts about how these features evolve.

## Problem Definition and Constraints

Personal sites are easy to browse but awkward to query. I wanted users to ask normal questions like "What did you write about Django performance?" and get a grounded answer fast.

I set a few hard constraints up front:

- **Grounded only:** no open-ended model answers without retrieval context.
- **Progressive enhancement:** JavaScript should improve UX, not define core functionality.
- **Operational visibility:** latency and cache behavior need to be inspectable in logs.
- **Honest failure modes:** explicit errors instead of silent retries or fake confidence.

## Request Lifecycle (Both Paths)

The chat feature has two endpoints:

- `GET/POST /chat/` for server-rendered page + non-streaming submit path.
- `POST /api/chat/stream/` for token streaming via server-sent events (SSE).

High-level flow:

1. Gate checks: feature flag, Turnstile (when enabled), semantic search enabled.
2. Try answer cache (question + conversation history signature).
3. On miss, load retrieval context (with its own cache keyed by normalized question).
4. Build prompt from bounded conversation history + bounded source blocks.
5. Call model (streaming or non-streaming), then persist session + DB + cache.
6. Render citations as links when URL-backed sources exist.

This gives me one reliable SSR baseline and one richer streaming path without splitting product behavior into two different systems.

## Retrieval Pipeline: Hybrid Search, Not Guessing

Retrieval comes from `SemanticDocument` and uses hybrid ranking (Postgres text rank + vector similarity). The combined score is weighted and thresholded, then top results are used to build prompt context.

```text
combined_score = (rank * SEMANTIC_SEARCH_RANK_WEIGHT)
               + (vector_score * SEMANTIC_SEARCH_VECTOR_WEIGHT)

default tuning:
- RAG_TOP_K = 8
- RAG_MAX_CONTEXT_CHARS = 9000
- SEMANTIC_SEARCH_RANK_WEIGHT = 0.45
- SEMANTIC_SEARCH_VECTOR_WEIGHT = 0.55
```

Context is assembled into numbered blocks so citations can map directly:

```text
[1] Title
URL: https://example
Content: snippet...
```

If no relevant documents are found, the assistant says that directly rather than fabricating an answer.

## Prompt Contract and Citation Semantics

The system prompt is intentionally strict: answer using only provided sources, and explicitly say "I don't know" when sources do not support the answer.

Citation markers are constrained to direct quotes. That keeps the output readable and avoids noisy "[1][2][3]" citation spam on every sentence.

Both backend and frontend linkify citation markers like `[1]` . Source entries missing title or URL are filtered out before rendering, so the UI never emits dead citation links.

## Streaming Protocol with SSE

Streaming responses are sent as `text/event-stream` with explicit event types:

```text
event: token
data: {"text":"partial answer"}

event: sources
data: [{"title":"...", "url":"...", "snippet":"..."}]

event: error
data: {"message":"..."}

event: done
data: {}
```

The frontend state machine starts with a "Thinking..." assistant bubble, appends token chunks, then retrofits citation anchors after the `sources` event arrives. It also handles stream failures with a visible error message while preserving the rest of the conversation UI.

One detail I like: cached answers are still emitted as chunked `token` events (fixed-size chunks), so repeated queries feel consistent instead of jarringly instant.

## Caching Strategy: Split by Responsibility

I use separate caches for answer generation and retrieval context:

- **Answer cache:** key = SHA-256(question + normalized history signature). This preserves conversational correctness.
- **Context cache:** key = SHA-256(normalized question). This avoids paying retrieval cost repeatedly for the same query text.

Default TTLs:

- `RAG_CACHE_TTL_SECONDS = 3600`
- `RAG_QUERY_CONTEXT_CACHE_TTL_SECONDS = 900`
- `RAG_EMBED_QUERY_CACHE_TTL_SECONDS = 3600`

The important design choice is that context reuse is allowed across conversation states, but final answer reuse is not unless history matches. That avoids stale conversational answers while still reducing retrieval overhead.

## State and Persistence Model

Conversation state is intentionally dual-layer:

- **Session:** lightweight working history used for fast continuation.
- **Database:** durable records in `ChatConversation` and `ChatMessage` .

`ChatConversation` can attach to session key, authenticated user, and request fingerprint; `ChatMessage` stores role, text, sources JSON, and an `is_error` flag. History is trimmed to a bounded turn window so prompt size stays predictable.

## Security and Guardrails

There are multiple explicit gates before model calls:

- **Feature flag:** `aaronspindler_chat_interface` can disable the surface immediately.
- **Turnstile:** enforced when the feature flag is enabled and a secret key is configured, with direct user-facing error messages; verification API failures fail open.
- **Semantic search gate:** refuses to answer if retrieval is disabled.
- **Provider exceptions:** normalized to "try again later" messages; streaming requests persist error turns, while non-stream requests render inline errors without appending history.

Failure messaging is aligned across both request paths, while persistence differs by design between streaming and non-streaming flows.

## Observability and Cost Tracking

Every request emits structured latency logs including cache lookup, retrieval, prompt build, provider timing, first-token timing, total duration, source count, and error classification.

OpenAI usage is also persisted (input/output tokens) for both embeddings and chat calls. This makes it possible to analyze cost and latency regressions from real traffic instead of guesses.

## Tradeoffs I Chose Deliberately

- **SSE over websockets:** simpler infra and sufficient for one-way token streams.
- **Bounded context over max recall:** predictable latency/cost beats trying to stuff every possible source.
- **Strict grounding over creativity:** better trust for site-specific Q&A.
- **Explicit errors over hidden retries:** easier to operate and easier for users to understand.

Those choices are intentionally conservative. This is a content-grounded assistant, not a general chatbot product.

## What I Want to Improve Next

- Retrieval quality evaluation set (fixed queries + expected sources) to measure ranking changes.
- Better follow-up/coreference handling ("that post", "the second one").
- UI-level source confidence and rank visibility.
- Intent-gap analytics for unanswered but high-frequency query classes.

## Try It

Open [/chat](https://aaronspindler.com/chat/) and ask:

- Which posts mention performance bottlenecks?
- What projects involve automation pipelines?
- What did you write about cutting frontend bloat?

If it works correctly, you'll get fast responses with grounded citations, and clear failure messages when the system has insufficient context. That's the contract: useful, traceable, and honest.

## Links

- [/chat](https://aaronspindler.com/chat/) (internal)
- [What Even Is This](https://aaronspindler.com/b/0001_what_even_is_this/) (internal)

## Related Posts

- [0001 What Even Is This](https://aaronspindler.com/b/personal/0001_What_Even_Is_This/)

---

<!-- post: tech/0007_The_Results_Of_Cutting_Out_The_Bloat hash:6fdc07ba39b97d7bbe02b510199c4f998b7a922ba4fa7721509e2bb68494212c -->

# 0007 The Results Of Cutting Out The Bloat

> Problem: Now that this blog has been running for a while, I wanted to share the actual results of that philosophy - including what complexity I kept and what I avoided. Approach: But the foundation is solid because it's built on boring, proven technology with smart optimization where it counts.

## Agent Digest

- Problem: Now that this blog has been running for a while, I wanted to share the actual results of that philosophy - including what complexity I kept and what I avoided.
- Aaron's position: Now that this blog has been running for a while, I wanted to share the actual results of that philosophy - including what complexity I kept and what I avoided.
- Approach: But the foundation is solid because it's built on boring, proven technology with smart optimization where it counts.
- Outcome: Now that this blog has been running for a while, I wanted to share the actual results of that philosophy - including what complexity I kept and what I avoided.
- Audience fit: Best for readers and agents researching Aaron Spindler's tech writing on 0007 The Results Of Cutting Out The Bloat.
- Why it matters: The Results of Cutting Out The Bloat A while back, I wrote about cutting out the bloat in web development.
- Best for:
  - Understanding the main argument of 0007 The Results Of Cutting Out The Bloat.
  - Finding citable details from Aaron's tech writing.
  - Answering questions about The Results of Cutting Out The Bloat.
  - Answering questions about Setting the Record Straight.
  - Answering questions about The Numbers Don't Lie.
- Not for:
  - Authoritative third-party documentation.
  - A complete substitute for the canonical article body.
- Core claims:
  - Now that this blog has been running for a while, I wanted to share the actual results of that philosophy - including what complexity I kept and what I avoided. (The Results of Cutting Out The Bloat)
  - Just server-rendered HTML with targeted JavaScript where needed. (Setting the Record Straight)
  - This Django site with server-side rendering and smart optimization regularly hits perfect or near-perfect scores. (The Numbers Don't Lie)
- Evidence:
  - Now that this blog has been running for a while, I wanted to share the actual results of that philosophy - including what complexity I kept and what I avoided. (The Results of Cutting Out The Bloat)
  - Spoiler alert: the results are excellent. (The Results of Cutting Out The Bloat)
  - Before diving into metrics, let me be clear about what "cutting the bloat" actually means for this site. (Setting the Record Straight)
  - I'm not running some minimalist setup with zero tooling. (Setting the Record Straight)
- Caveats:
  - But it's reliable, scalable, and maintainable because each service has a clear purpose and the deployment is automated. (Deployment: Multiple Services, Simple Orchestration)
  - No transpiled code, no framework magic, no "this error occurred during server-side rendering but actually happened on the client after hydration." The CI/CD logs are equally clear. (Debugging: Clear Stack Traces)
  - Here's what I gave up by avoiding the modern framework ecosystem: Complex client-side interactions: No infinite scroll, no optimistic updates, no complex state management Component reusability across projects: I use Django templates, not portable React components Hot module replacement: I refresh the browser manually... (What I Actually Sacrificed)
  - With this approach: Fewer dependencies to update: I have 47 Python packages, not 1,000+ npm packages Standard technologies: HTML, CSS, Python, and PostgreSQL aren't going anywhere No framework churn: I'm not rewriting everything every 2 years because the ecosystem moved on Easier onboarding: Any developer who knows... (Maintenance and Technical Debt)

## Metadata

- Canonical URL: https://aaronspindler.com/b/tech/0007_The_Results_Of_Cutting_Out_The_Bloat/
- Markdown URL: https://aaronspindler.com/b/tech/0007_The_Results_Of_Cutting_Out_The_Bloat/index.md
- JSON URL: https://aaronspindler.com/b/tech/0007_The_Results_Of_Cutting_Out_The_Bloat/index.json
- Category: tech
- Published: 2026-03-26T17:04:55+00:00
- Updated: 2026-07-08T16:59:22.396495+00:00
- Word count: 1755
- Content hash: 6fdc07ba39b97d7bbe02b510199c4f998b7a922ba4fa7721509e2bb68494212c
- AI written: no
- Format version: agent-blog-post-v2

## Takeaways

- Now that this blog has been running for a while, I wanted to share the actual results of that philosophy - including what complexity I kept and what I avoided. (The Results of Cutting Out The Bloat)
- Just server-rendered HTML with targeted JavaScript where needed. (Setting the Record Straight)
- This Django site with server-side rendering and smart optimization regularly hits perfect or near-perfect scores. (The Numbers Don't Lie)
- The performance metrics translate to real benefits: First Contentful Paint: Under 0.5 seconds - Users see content almost instantly Time to Interactive: Under 1 second - The page is fully usable immediately Total Page Size: Under 200KB for most pages - Uses less data than a single high-res image on most modern sites... (What Users Actually Experience)
- Here's the interesting part: building without frontend framework bloat doesn't mean avoiding all complexity. (The Developer Experience: Where Complexity Belongs)
- My CSS build process uses PostCSS, PurgeCSS for removing unused styles, automatic minification, and generates both Gzip and Brotli compressed versions. (Build Pipeline: Sophisticated but Fast)

## Questions Answered

- What problem does 0007 The Results Of Cutting Out The Bloat address?
- What position does Aaron take in 0007 The Results Of Cutting Out The Bloat?
- How does 0007 The Results Of Cutting Out The Bloat approach the problem?
- What evidence or outcomes does 0007 The Results Of Cutting Out The Bloat provide?
- What does the article explain about The Results of Cutting Out The Bloat?
- What does the article explain about Setting the Record Straight?

## Agent Queries

- Aaron Spindler "0007 The Results Of Cutting Out The Bloat"
- 0007 The Results Of Cutting Out The Bloat tech Aaron Spindler
- 0007 The Results Of Cutting Out The Bloat Now that this blog has been running for a while, I wanted to share the actual results of that philosophy - including what complexity I kept and what I avoided.
- 0007 The Results Of Cutting Out The Bloat The Results of Cutting Out The Bloat
- 0007 The Results Of Cutting Out The Bloat Setting the Record Straight

## Follow-Up Questions

- What changed after the approach in 0007 The Results Of Cutting Out The Bloat was applied?
- What tradeoffs or constraints remain after 0007 The Results Of Cutting Out The Bloat?
- What setup is required before applying the ideas in 0007 The Results Of Cutting Out The Bloat?
- How would The Results of Cutting Out The Bloat change in a different environment?

## Outline

- The Results of Cutting Out The Bloat
-   Setting the Record Straight
-   The Numbers Don't Lie
-   What Users Actually Experience
-   The Developer Experience: Where Complexity Belongs
-     Build Pipeline: Sophisticated but Fast
-     CI/CD: Comprehensive and Reliable
-     Deployment: Multiple Services, Simple Orchestration
-     Debugging: Clear Stack Traces
-   What I Actually Sacrificed
-   Where I DO Use Build Tools (And Why)
-   Maintenance and Technical Debt
-   The Psychological Benefits
-   When You DO Need the Frameworks
-   Lessons Learned
-   The Bigger Picture
-   Conclusion

## Body

# The Results of Cutting Out The Bloat

A while back, I wrote about [cutting out the bloat](https://aaronspindler.com/b/0002_cut_out_the_bloat/) in web development. The core idea: stop reaching for React/Vue/Next.js for everything and use server-side rendering where it makes sense. Now that this blog has been running for a while, I wanted to share the actual results of that philosophy - including what complexity I kept and what I avoided. Spoiler alert: the results are excellent.

## Setting the Record Straight

Before diving into metrics, let me be clear about what "cutting the bloat" actually means for this site. I'm not running some minimalist setup with zero tooling. In fact, the infrastructure is fairly sophisticated:

- **CI/CD Pipeline:** GitHub Actions with parallel test execution, security scanning, linting, type checking, and multi-service deployment
- **Build Process:** PostCSS, PurgeCSS, JavaScript minification with Terser, image optimization, and Brotli compression
- **Infrastructure:** Four Docker containers (web, Celery worker, Celery Beat scheduler, Flower monitoring), deployed to Dokploy
- **Backend Services:** Django, PostgreSQL, Redis, Playwright for server-side screenshots
- **Testing:** Comprehensive test suite with Docker Compose, parallel execution, and coverage reporting

So what did I actually cut out? **The frontend framework bloat.** No React. No Vue. No 3MB JavaScript bundle. No client-side routing. No hydration. Just server-rendered HTML with targeted JavaScript where needed.

This is an important distinction: sophisticated backend infrastructure for optimization and reliability is good. Shipping megabytes of JavaScript to render blog posts is not.

## The Numbers Don't Lie

With [automated nightly Lighthouse audits](https://aaronspindler.com/lighthouse/history/) tracking performance over time, this site consistently scores in the high 90s across all four metrics:

- **Performance:** 95-100
- **Accessibility:** 95-100
- **Best Practices:** 95-100
- **SEO:** 95-100

For context, the average website scores around 50-70 on performance. Many popular blogging platforms struggle to break 80. This Django site with server-side rendering and smart optimization regularly hits perfect or near-perfect scores.

You can [view the complete Lighthouse audit history](https://aaronspindler.com/lighthouse/history/) to see these metrics tracked over the past 30 days, including graphs showing consistency and trends.

But scores are just numbers. What do they actually mean in the real world?

## What Users Actually Experience

The performance metrics translate to real benefits:

- **First Contentful Paint:** Under 0.5 seconds - Users see content almost instantly
- **Time to Interactive:** Under 1 second - The page is fully usable immediately
- **Total Page Size:** Under 200KB for most pages - Uses less data than a single high-res image on most modern sites
- **Zero JavaScript frameworks:** No React, no Vue, no Next.js bloat to download and parse

Compare this to the typical blog built with modern frameworks: 2-3MB of JavaScript, 3-5 second load times, and a spinning loader while the framework bootstraps. Is that really necessary to display text and images?

## The Developer Experience: Where Complexity Belongs

Here's the interesting part: building without frontend framework bloat doesn't mean avoiding all complexity. It means putting complexity where it provides value.

### Build Pipeline: Sophisticated but Fast

My CSS build process uses PostCSS, PurgeCSS for removing unused styles, automatic minification, and generates both Gzip and Brotli compressed versions. The result? CSS that's heavily optimized with features like automatic prefixing and color optimization.

The difference from a typical Next.js build? This takes seconds, not minutes. There's no webpack configuration hell, no fighting with module resolution, no cache invalidation mysteries. Just run `make static` and it works.

### CI/CD: Comprehensive and Reliable

My CI/CD pipeline is actually quite complex:

- Parallel test execution across multiple test groups
- Security scanning with Safety and pip-audit
- Code quality checks: Ruff, Black, isort, Pylint, MyPy
- Docker image builds for four different services
- Automated deployment to Dokploy with health checks
- Coverage reporting and tracking

This runs on every push. Tests complete in minutes, not the 10-20 minutes I've seen in typical React projects.

The key difference? I'm testing Python code and database interactions, not waiting for TypeScript to compile and Jest to instrument thousands of JavaScript files.

### Deployment: Multiple Services, Simple Orchestration

The production environment runs four separate Docker containers:

- **Web:** Gunicorn serving Django
- **Celery:** Background task processing
- **Celery Beat:** Scheduled tasks (nightly Lighthouse audits, cache rebuilds)
- **Flower:** Real-time Celery monitoring

Plus PostgreSQL, Redis, and S3 for storage. This is not a "simple" architecture. But it's reliable, scalable, and maintainable because each service has a clear purpose and the deployment is automated.

Compare this to managing serverless functions, edge middleware, API routes, and client components in a Next.js app. More moving parts, more vendor lock-in, more debugging hell.

### Debugging: Clear Stack Traces

When something breaks (and it will), debugging is straightforward. Django gives me clear stack traces pointing directly to the problem. No transpiled code, no framework magic, no "this error occurred during server-side rendering but actually happened on the client after hydration."

The CI/CD logs are equally clear. When a test fails, I see exactly which test and why, not cryptic webpack errors about module resolution.

## What I Actually Sacrificed

Let's be honest - there are trade-offs. Here's what I gave up by avoiding the modern framework ecosystem:

- **Complex client-side interactions:** No infinite scroll, no optimistic updates, no complex state management
- **Component reusability across projects:** I use Django templates, not portable React components
- **Hot module replacement:** I refresh the browser manually (takes 0.2 seconds)
- **Rich ecosystem of UI libraries:** No shadcn/ui or Material UI - I write CSS

But here's the thing: **I don't actually need any of that for a blog** . Users don't care if I'm using the latest framework. They care if the site loads fast, looks good, and the content is easy to read.

## Where I DO Use Build Tools (And Why)

I'm not anti-tooling. I use build tools where they provide clear value:

- **PostCSS:** Automatic vendor prefixing and optimizations
- **PurgeCSS:** Removes unused CSS (37% size reduction)
- **Terser:** JavaScript minification
- **Brotli:** Better compression than Gzip alone
- **Docker:** Consistent environments from development to production
- **Playwright:** Server-side screenshot generation for the knowledge graph

These tools solve real problems and don't add framework complexity. The entire build pipeline is defined in a Makefile and a few management commands. No `package.json` with 200 dependencies.

## Maintenance and Technical Debt

This is where the benefits really compound over time. Every dependency you add is technical debt. Every framework version is a ticking time bomb of breaking changes.

With this approach:

- **Fewer dependencies to update:** I have 47 Python packages, not 1,000+ npm packages
- **Standard technologies:** HTML, CSS, Python, and PostgreSQL aren't going anywhere
- **No framework churn:** I'm not rewriting everything every 2 years because the ecosystem moved on
- **Easier onboarding:** Any developer who knows Django can contribute
- **Clear upgrade paths:** Django has excellent backwards compatibility and upgrade guides

I spend my time writing content and adding features, not fighting with ESLint configurations and webpack optimization plugins.

## The Psychological Benefits

There's something incredibly liberating about working without framework bloat. I don't have decision fatigue about which state management library to use. I don't worry about client/server boundary issues. I don't spend hours troubleshooting "works on my machine" build problems.

When I want to add a feature, I:

1. Write a Django view
2. Create a template
3. Write some tests
4. Push to GitHub
5. CI/CD handles the rest

No webpack configuration. No "use client" directives. No hydration errors. Just code that runs on the server and sends HTML to the browser.

This mental bandwidth goes into writing better content, designing better features, and actually shipping things.

## When You DO Need the Frameworks

Let me be absolutely clear: frameworks aren't evil. They solve real problems for real applications. If you're building:

- A complex single-page application with lots of client-side state
- Real-time collaborative tools (think Figma or Google Docs)
- Data-heavy dashboards with complex visualizations
- Anything that genuinely needs sophisticated client-side logic

Then yes, use React or Vue or whatever. The frameworks exist for good reasons.

But if you're building a blog, a documentation site, a portfolio, or any primarily content-focused site? You probably don't need them. And you'll be better off without them.

## Lessons Learned

After months of running this stack in production, here are my key takeaways:

- **Backend complexity for optimization is good:** Build tools, CI/CD, and infrastructure automation provide real value
- **Frontend complexity for rendering content is bad:** Shipping React to display blog posts is wasteful
- **Performance is a feature users notice:** Even if they don't consciously realize why, fast sites feel better
- **Simplicity scales:** As this blog grows, the architecture continues to work well
- **Developer happiness matters:** Working without framework bloat is more enjoyable
- **Boring technology works:** Django, PostgreSQL, and Redis are proven, reliable, and maintainable

## The Bigger Picture

This isn't just about my blog. It's about a broader shift happening in web development. More developers are realizing that the "everything in JavaScript" approach has costs that often outweigh the benefits.

Projects like HTMX and Alpine.js show that you can have interactivity without framework overhead. The resurgence of server-side rendering (even in Next.js!) proves that rendering on the server was a good idea all along. DHH's [writings on modern web development](https://world.hey.com/dhh) are resonating because people are tired of the complexity treadmill.

We built great websites before React existed. We can still build great websites without needing a framework for everything - especially when we have sophisticated build tools, testing, and deployment automation in place.

## Conclusion

Cutting out frontend framework bloat delivered real, measurable benefits. The site is faster for users, easier to maintain, simpler to debug, and more enjoyable to work on.

But I didn't sacrifice professional engineering practices. I have a robust CI/CD pipeline, comprehensive testing, automated deployments, and sophisticated build optimization. The difference is that this complexity serves users and developers, not frameworks.

Could I have achieved similar results with Next.js or Remix? Maybe. But it would have required more expertise, more tooling, more debugging, and more ongoing maintenance. The server-side rendering approach just works, and it keeps working without constant attention.

As I mentioned in [this blog framework ain't perfect](https://aaronspindler.com/b/0004_this_blog_aint_perfect/) , there are still rough edges and things I want to improve. But the foundation is solid because it's built on boring, proven technology with smart optimization where it counts.

So if you're starting a new project, ask yourself: do you really need that frontend framework? Or are you reaching for complexity out of habit? Sometimes the best solution is server-side rendering with great infrastructure, not client-side rendering with minimal tooling.

And if you don't believe me, just look at the Lighthouse scores. The numbers speak for themselves.

## Links

- [cutting out the bloat](https://aaronspindler.com/b/0002_cut_out_the_bloat/) (internal)
- [automated nightly Lighthouse audits](https://aaronspindler.com/lighthouse/history/) (internal)
- [view the complete Lighthouse audit history](https://aaronspindler.com/lighthouse/history/) (internal)
- [writings on modern web development](https://world.hey.com/dhh) (external)
- [this blog framework ain't perfect](https://aaronspindler.com/b/0004_this_blog_aint_perfect/) (internal)

## Related Posts

- [0002 Cut Out The Bloat](https://aaronspindler.com/b/tech/0002_Cut_Out_The_Bloat/)
- [0004 This Blog Aint Perfect](https://aaronspindler.com/b/personal/0004_This_Blog_Aint_Perfect/)

---

<!-- post: projects/0006_iMessageLLM hash:b8edbc42c6cae14b1729ffcca9b2049e4ec1f4910bbfb6b54fded35b91837161 -->

# 0006 iMessageLLM

> Problem: disappointing: # Version 1: The naive approach that didn't work messages = parse_html_to_csv(html_file) prompt = f"Analyze these messages: {messages}" # ERROR: Token limit exceeded (500,000+ tokens!) Reality hit hard. Approach: After sharing the tool with friends, I received valuable feedback that shaped the final version: "It's too slow for quick questions" → Added the statistical sampling mode for instant...

## Agent Digest

- Problem: disappointing: # Version 1: The naive approach that didn't work messages = parse_html_to_csv(html_file) prompt = f"Analyze these messages: {messages}" # ERROR: Token limit exceeded (500,000+ tokens!) Reality hit hard.
- Aaron's position: I built iMessage LLM , a powerful Python tool that converts iMessage exports into structured data and provides AI-powered analysis using Large Language Models.
- Approach: After sharing the tool with friends, I received valuable feedback that shaped the final version: "It's too slow for quick questions" → Added the statistical sampling mode for instant responses "I want to compare different time periods" → Built the multi-year comparison feature "The terminal output is hard to read" →...
- Outcome: Analysis results are cached to speed up repeated queries.
- Audience fit: Best for readers and agents researching Aaron Spindler's projects writing on 0006 iMessageLLM.
- Why it matters: iMessage LLM: Transform Your Message History into Analyzable Data with AI Ever wondered what patterns lie hidden in years of iMessage conversations?
- Best for:
  - Understanding the main argument of 0006 iMessageLLM.
  - Finding citable details from Aaron's projects writing.
  - Answering questions about iMessage LLM: Transform Your Message History into Analyzable Data with AI.
  - Answering questions about The Problem.
  - Answering questions about The Solution: iMessage LLM.
- Not for:
  - Authoritative third-party documentation.
  - A complete substitute for the canonical article body.
- Core claims:
  - I built iMessage LLM , a powerful Python tool that converts iMessage exports into structured data and provides AI-powered analysis using Large Language Models. (iMessage LLM: Transform Your Message History into Analyzable Data with AI)
  - We accumulate thousands of messages over years, but they're locked away in an unstructured format. (The Problem)
  - iMessage LLM is a comprehensive toolkit that: Converts HTML exports from imessage-exporter into structured CSV data Intelligently groups messages into conversations using advanced algorithms Provides AI-powered analysis using Ollama with DeepSeek-R1 Offers powerful filtering and search capabilities Maintains... (The Solution: iMessage LLM)
- Evidence:
  - iMessage LLM is a comprehensive toolkit that: Converts HTML exports from imessage-exporter into structured CSV data Intelligently groups messages into conversations using advanced algorithms Provides AI-powered analysis using Ollama with DeepSeek-R1 Offers powerful filtering and search capabilities Maintains... (The Solution: iMessage LLM)
  - The tool doesn't just use simple time gaps - it employs multiple sophisticated signals: # Dynamic time thresholds based on time of day if (current_hour >= 22 or current_hour <= 6): threshold = 3 # Night: shorter gaps elif 6 <= current_hour <= 10: threshold = 8 # Morning: overnight gaps else: threshold = 4 # Day: active... (Smart Conversation Detection)
  - Using Ollama with DeepSeek-R1, you can ask natural language questions about your message history: # Ask about specific years $ python ask_messages.py --year 2017 --question "What happened this year?" # Analyze relationship evolution $ python ask_messages.py --years 2017 2018 2019 --question "How did our relationship... (AI-Powered Analysis)
  - The tool uses DeepSeek's tokenizer for accurate token counting and automatically chunks large conversations to fit within model context limits: # Compact message format to save tokens def compress_message_format(messages): return "\n".join([ f"{msg['date']}|{msg['sender']}|{msg['message']}" for msg in messages ]) (Token Optimization)
- Caveats:
  - We accumulate thousands of messages over years, but they're locked away in an unstructured format. (The Problem)
  - Our digital conversations contain valuable insights about relationships, personal growth, and communication patterns - but we have no way to access them systematically. (The Problem)
  - The tool uses DeepSeek's tokenizer for accurate token counting and automatically chunks large conversations to fit within model context limits: # Compact message format to save tokens def compress_message_format(messages): return "\n".join([ f"{msg['date']}|{msg['sender']}|{msg['message']}" for msg in messages ]) (Token Optimization)
  - disappointing: # Version 1: The naive approach that didn't work messages = parse_html_to_csv(html_file) prompt = f"Analyze these messages: {messages}" # ERROR: Token limit exceeded (500,000+ tokens!) Reality hit hard. (Initial Prototype: The Naive Approach)

## Metadata

- Canonical URL: https://aaronspindler.com/b/projects/0006_iMessageLLM/
- Markdown URL: https://aaronspindler.com/b/projects/0006_iMessageLLM/index.md
- JSON URL: https://aaronspindler.com/b/projects/0006_iMessageLLM/index.json
- Category: projects
- Published: 2026-03-26T17:04:55+00:00
- Updated: 2026-07-08T16:59:24.019148+00:00
- Word count: 2053
- Content hash: b8edbc42c6cae14b1729ffcca9b2049e4ec1f4910bbfb6b54fded35b91837161
- AI written: no
- Format version: agent-blog-post-v2

## Takeaways

- I built iMessage LLM , a powerful Python tool that converts iMessage exports into structured data and provides AI-powered analysis using Large Language Models. (iMessage LLM: Transform Your Message History into Analyzable Data with AI)
- We accumulate thousands of messages over years, but they're locked away in an unstructured format. (The Problem)
- iMessage LLM is a comprehensive toolkit that: Converts HTML exports from imessage-exporter into structured CSV data Intelligently groups messages into conversations using advanced algorithms Provides AI-powered analysis using Ollama with DeepSeek-R1 Offers powerful filtering and search capabilities Maintains... (The Solution: iMessage LLM)
- The tool doesn't just use simple time gaps - it employs multiple sophisticated signals: # Dynamic time thresholds based on time of day if (current_hour >= 22 or current_hour <= 6): threshold = 3 # Night: shorter gaps elif 6 <= current_hour <= 10: threshold = 8 # Morning: overnight gaps else: threshold = 4 # Day: active... (Smart Conversation Detection)
- Using Ollama with DeepSeek-R1, you can ask natural language questions about your message history: # Ask about specific years $ python ask_messages.py --year 2017 --question "What happened this year?" # Analyze relationship evolution $ python ask_messages.py --years 2017 2018 2019 --question "How did our relationship... (AI-Powered Analysis)
- The tool uses DeepSeek's tokenizer for accurate token counting and automatically chunks large conversations to fit within model context limits: # Compact message format to save tokens def compress_message_format(messages): return "\n".join([ f"{msg['date']}|{msg['sender']}|{msg['message']}" for msg in messages ]) (Token Optimization)

## Questions Answered

- What problem does 0006 iMessageLLM address?
- What position does Aaron take in 0006 iMessageLLM?
- How does 0006 iMessageLLM approach the problem?
- What evidence or outcomes does 0006 iMessageLLM provide?
- What does the article explain about iMessage LLM: Transform Your Message History into Analyzable Data with AI?
- What does the article explain about The Problem?

## Agent Queries

- Aaron Spindler "0006 iMessageLLM"
- 0006 iMessageLLM projects Aaron Spindler
- 0006 iMessageLLM disappointing: # Version 1: The naive approach that didn't work messages = parse_html_to_csv(html_file) prompt = f"Analyze these messages: {messages}" # ERROR: Token limit exceeded (500,000+ tokens!) Reality hit hard.
- 0006 iMessageLLM iMessage LLM: Transform Your Message History into Analyzable Data with AI
- 0006 iMessageLLM The Problem

## Follow-Up Questions

- What changed after the approach in 0006 iMessageLLM was applied?
- What tradeoffs or constraints remain after 0006 iMessageLLM?
- What setup is required before applying the ideas in 0006 iMessageLLM?
- How would iMessage LLM: Transform Your Message History into Analyzable Data with AI change in a different environment?

## Outline

- iMessage LLM: Transform Your Message History into Analyzable Data with AI
-   The Problem
-   The Solution: iMessage LLM
-   Smart Conversation Detection
-   AI-Powered Analysis
-   Intelligent Features
-     Token Optimization
-     Smart Caching
-     Conversation History
-   Performance & Scale
-   Real-World Use Cases
-     Relationship Analysis
-     Topic Discovery
-     Memory Lane
-     Communication Patterns
-   Getting Started
-   Technical Architecture
-   Privacy & Local Processing
-   What I Learned
-   The Build Process: From Concept to Reality
-     Initial Prototype: The Naive Approach
-     Challenge #1: Parsing Apple's HTML Export Format
-     Challenge #2: Defining "Conversations"
-     The Breakthrough: Multi-Signal Conversation Detection
-     Challenge #3: Token Management and Context Windows
-     Challenge #4: Performance at Scale
-     The DeepSeek Integration Journey
-     Testing with Real Data: Unexpected Discoveries
-     The Caching System Evolution
-     Lessons from User Feedback
-   Future Enhancements
-   Open Source
-   Conclusion

## Body

# iMessage LLM: Transform Your Message History into Analyzable Data with AI

Ever wondered what patterns lie hidden in years of iMessage conversations? What themes emerged in your relationships? How communication evolved over time? I built **iMessage LLM** , a powerful Python tool that converts iMessage exports into structured data and provides AI-powered analysis using Large Language Models.

## The Problem

We accumulate thousands of messages over years, but they're locked away in an unstructured format. Apple's Messages app doesn't provide meaningful analytics or search capabilities beyond basic keyword matching. Our digital conversations contain valuable insights about relationships, personal growth, and communication patterns - but we have no way to access them systematically.

## The Solution: iMessage LLM

iMessage LLM is a comprehensive toolkit that:

- Converts HTML exports from [imessage-exporter](https://github.com/ReagentX/imessage-exporter) into structured CSV data
- Intelligently groups messages into conversations using advanced algorithms
- Provides AI-powered analysis using Ollama with DeepSeek-R1
- Offers powerful filtering and search capabilities
- Maintains conversation context for meaningful analysis

## Smart Conversation Detection

One of the most innovative features is the intelligent conversation grouping. The tool doesn't just use simple time gaps - it employs multiple sophisticated signals:

```python
# Dynamic time thresholds based on time of day
if (current_hour >= 22 or current_hour <= 6):
    threshold = 3  # Night: shorter gaps
elif 6 <= current_hour <= 10:
    threshold = 8  # Morning: overnight gaps
else:
    threshold = 4  # Day: active conversation time

# Content analysis for conversation boundaries
starters = ['hey', 'hello', 'good morning', ...]
enders = ['goodnight', 'bye', 'talk later', ...]

# Momentum analysis - response times and engagement
avg_response_time = calculate_response_times(messages)
turn_changes = count_turn_exchanges(messages)
engagement_score = calculate_engagement(turn_changes, response_time)
```

The algorithm also detects:

- **Topic changes** using semantic similarity
- **Emotional tone shifts** through emoji and keyword analysis
- **Activity transitions** ("just got to work", "heading home")
- **Conversation momentum** analyzing response patterns

## AI-Powered Analysis

Once your messages are processed, the real magic begins. Using Ollama with DeepSeek-R1, you can ask natural language questions about your message history:

```bash
# Ask about specific years
$ python ask_messages.py --year 2017 --question "What happened this year?"

# Analyze relationship evolution
$ python ask_messages.py --years 2017 2018 2019 --question "How did our relationship evolve?"

# Examine specific conversations
$ python ask_messages.py --conversation 42 --question "What is this conversation about?"

# Filter by conversation characteristics
$ python ask_messages.py --min-messages 50 --question "What are the themes in longer conversations?"
```

## Intelligent Features

### Token Optimization

The tool uses DeepSeek's tokenizer for accurate token counting and automatically chunks large conversations to fit within model context limits:

```python
# Compact message format to save tokens
def compress_message_format(messages):
    return "\n".join([
        f"{msg['date']}|{msg['sender']}|{msg['message']}"
        for msg in messages
    ])
```

### Smart Caching

Analysis results are cached to speed up repeated queries. The cache is invalidated if messages change or you switch models:

```bash
# List cached results
$ python ask_messages.py --list-cache

# Force reprocessing
$ python ask_messages.py --question "Analyze again" --force-reprocess

# Clear all cached results
$ python ask_messages.py --clear-cache
```

### Conversation History

The tool maintains conversation history, allowing you to build on previous analyses:

```bash
# First analysis
$ python ask_messages.py --question "What are the main themes?" --save-conversation themes.json

# Follow-up question using context
$ python ask_messages.py --load-conversation themes.json --question "Tell me more about the second theme"
```

## Performance & Scale

The tool is optimized for large datasets:

- **Streaming HTML parsing** handles multi-gigabyte files efficiently
- **Statistical sampling** for quality analysis on massive datasets
- **Adaptive algorithms** that scale based on dataset size
- **Progress tracking** with real-time metrics and ETA

```text
═══════════════════════ CONVERSATION ANALYSIS ═══════════════════════
                    Analyzing conversation patterns...

✓ Assigned 1,234 conversations in 2.3s

───────────────────── Decision Statistics ─────────────────────
  Time Based: 856 (69.4%)
  Topic Change: 187 (15.2%)
  Starter Based: 98 (7.9%)
  Momentum Based: 93 (7.5%)
```

## Real-World Use Cases

Here are some fascinating ways to use iMessage LLM:

### Relationship Analysis

```bash
$ python ask_messages.py --question "How has our communication style changed over the years?"
```

### Topic Discovery

```bash
$ python ask_messages.py --question "What are the recurring themes in our conversations?"
```

### Memory Lane

```bash
$ python ask_messages.py --year 2020 --question "What were we talking about during the pandemic?"
```

### Communication Patterns

```bash
$ python ask_messages.py --question "When do we have our deepest conversations?"
```

## Getting Started

Setting up iMessage LLM is straightforward:

```bash
# 1. Export your messages with imessage-exporter
$ brew install imessage-exporter
$ imessage-exporter --format html --output ./data/

# 2. Install dependencies
$ pip install -r requirements.txt

# 3. Setup Ollama with DeepSeek
$ ollama serve
$ ollama pull deepseek-r1:14b

# 4. Process your messages
$ python process.py

# 5. Start analyzing!
$ python ask_messages.py --question "What are the main themes in our conversations?"
```

## Technical Architecture

The project consists of several key components:

- **process.py** : Core processing engine with conversation detection algorithms
- **ask_messages.py** : AI analysis interface with caching and history management
- **prompts.py** : Centralized prompt templates for consistent AI interactions
- **formatting_utils.py** : Beautiful terminal output with progress tracking
- **deepseek_tokenizer.py** : Accurate token counting for context management

## Privacy & Local Processing

Everything runs locally on your machine. Your messages never leave your computer - the AI analysis uses Ollama running locally, not cloud services. This ensures complete privacy while still providing powerful insights.

## What I Learned

Building this tool taught me several valuable lessons:

- Conversation boundaries are more nuanced than simple time gaps
- Context windows and token management are crucial for LLM performance
- Smart caching and chunking strategies enable analysis of massive datasets
- Local AI models like DeepSeek-R1 are powerful enough for complex analysis tasks

## The Build Process: From Concept to Reality

The journey of building iMessage LLM was both challenging and enlightening. It started with a simple curiosity: I had years of message history and wanted to understand what patterns and insights were hidden within. Here's how the project evolved from a weekend experiment to a comprehensive analysis toolkit.

### Initial Prototype: The Naive Approach

My first attempt was embarrassingly simple - just dump all messages into a CSV and throw them at an LLM. The results were... disappointing:

```python
# Version 1: The naive approach that didn't work
messages = parse_html_to_csv(html_file)
prompt = f"Analyze these messages: {messages}"
# ERROR: Token limit exceeded (500,000+ tokens!)
```

Reality hit hard. Years of messages meant millions of tokens, far exceeding any model's context window. I needed to be smarter about this.

### Challenge #1: Parsing Apple's HTML Export Format

The imessage-exporter tool outputs HTML files with a specific structure that needed careful parsing. Apple's format includes attachments, reactions, and various message types that all required different handling:

```python
# Handling different message types
def parse_message_element(element):
    message_type = element.get('data-type', 'text')

    if message_type == 'attachment':
        return handle_attachment(element)
    elif message_type == 'reaction':
        return handle_reaction(element)
    elif message_type == 'edited':
        return handle_edited_message(element)
    else:
        return extract_text_content(element)
```

The biggest surprise? Emojis and special characters. They required special handling to prevent encoding issues when converting to CSV. I spent an entire evening debugging why certain messages were causing pandas to throw UTF-8 errors.

### Challenge #2: Defining "Conversations"

This was the hardest problem to solve. What exactly constitutes a conversation? My first approach used a simple 30-minute gap rule:

```python
# Version 2: Simple time-based splitting (too simplistic)
def split_conversations_v1(messages):
    conversations = []
    current_convo = []

    for i, msg in enumerate(messages):
        if i > 0:
            time_gap = msg['timestamp'] - messages[i-1]['timestamp']
            if time_gap > timedelta(minutes=30):
                conversations.append(current_convo)
                current_convo = []
        current_convo.append(msg)

    return conversations
```

This worked... poorly. It would split ongoing conversations just because someone took a lunch break, or merge completely unrelated topics just because they happened quickly. I needed something more sophisticated.

### The Breakthrough: Multi-Signal Conversation Detection

After analyzing my own message patterns, I realized conversations have multiple signals beyond just time gaps. This led to the current multi-signal approach:

```python
# Version 3: Multi-signal detection (the breakthrough)
class ConversationDetector:
    def __init__(self):
        self.signals = [
            TimeOfDaySignal(),      # Different thresholds for different times
            ContentSignal(),         # Detect greeting/farewell patterns
            TopicSimilaritySignal(), # Use embeddings for semantic similarity
            MomentumSignal(),        # Analyze response patterns
            EmotionalToneSignal()    # Track emoji usage and sentiment
        ]

    def should_split(self, messages, index):
        votes = [signal.vote(messages, index) for signal in self.signals]
        return self.weighted_decision(votes)
```

Each signal votes on whether to split at a given point, and the final decision uses weighted voting. This dramatically improved conversation quality.

### Challenge #3: Token Management and Context Windows

Even with conversations properly segmented, many were still too large for LLM context windows. I needed intelligent chunking that preserved context:

```python
# Smart chunking that maintains conversation flow
def chunk_conversation(messages, max_tokens=8000):
    chunks = []
    current_chunk = []
    current_tokens = 0

    # Always include conversation metadata
    metadata = create_conversation_summary(messages)
    metadata_tokens = count_tokens(metadata)

    for msg in messages:
        msg_tokens = count_tokens(format_message(msg))

        if current_tokens + msg_tokens > max_tokens - metadata_tokens:
            # Save current chunk with overlap for context
            chunks.append({
                'messages': current_chunk,
                'metadata': metadata,
                'continuation': True
            })
            # Keep last few messages for context continuity
            overlap = get_context_overlap(current_chunk)
            current_chunk = overlap
            current_tokens = count_tokens(overlap)

        current_chunk.append(msg)
        current_tokens += msg_tokens

    return chunks
```

### Challenge #4: Performance at Scale

Processing years of messages (100,000+) was initially taking hours. Profiling revealed the bottlenecks:

```text
Initial Performance Profile:
- HTML Parsing: 45% of runtime (Beautiful Soup)
- Conversation Detection: 30% of runtime (O(n²) similarity checks)
- CSV Writing: 15% of runtime (row-by-row pandas operations)
- Token Counting: 10% of runtime (repeated tokenization)
```

The optimizations that made the biggest difference:

```python
# Optimization 1: Streaming HTML parser
from lxml import etree
parser = etree.iterparse(html_file, events=('start', 'end'))
# 10x faster than Beautiful Soup for large files

# Optimization 2: Batch similarity computations
embeddings = compute_embeddings_batch(messages)  # Vectorized operations
similarities = cosine_similarity_matrix(embeddings)  # NumPy magic

# Optimization 3: Bulk CSV operations
df = pd.DataFrame(all_messages)
df.to_csv('messages.csv', index=False)  # Single write operation

# Final performance: 100,000 messages in ~3 minutes
```

### The DeepSeek Integration Journey

Choosing the right model was crucial. I experimented with several options:

- **GPT-3.5:** Good but expensive for large-scale analysis, privacy concerns
- **LLaMA 2:** Decent but struggled with nuanced conversation analysis
- **Mistral:** Fast but less accurate for relationship insights
- **DeepSeek-R1:** The sweet spot - excellent reasoning, runs locally, great token efficiency

DeepSeek-R1's ability to handle complex reasoning tasks while running entirely locally made it perfect for this privacy-sensitive application. The integration required custom tokenizer implementation:

```python
# Custom DeepSeek tokenizer for accurate token counting
from transformers import AutoTokenizer

class DeepSeekTokenizer:
    def __init__(self):
        self.tokenizer = AutoTokenizer.from_pretrained(
            'deepseek-ai/DeepSeek-R1-Distill-Llama-14B'
        )
        self._cache = {}  # Cache tokenization results

    def count_tokens(self, text):
        if text in self._cache:
            return self._cache[text]

        tokens = len(self.tokenizer.encode(text))
        self._cache[text] = tokens
        return tokens
```

### Testing with Real Data: Unexpected Discoveries

Testing with my own message history revealed fascinating edge cases:

- **Group chats vs. one-on-one:** Required different conversation detection logic
- **Media-heavy conversations:** Needed special handling for photo/video descriptions
- **Time zone changes:** Travel caused conversation splitting issues
- **Language switching:** Multilingual conversations needed special tokenization

Each edge case led to refinements in the algorithm. For example, detecting time zone changes:

```python
# Detect potential timezone changes
def detect_timezone_shift(messages):
    hourly_distribution = defaultdict(int)
    for msg in messages:
        hourly_distribution[msg['timestamp'].hour] += 1

    # Sudden shift in active hours suggests timezone change
    if has_distribution_shift(hourly_distribution):
        return adjust_thresholds_for_timezone()
```

### The Caching System Evolution

Repeated analysis of the same conversations was wasteful. The caching system evolved through three iterations:

```python
# Version 1: Simple file cache (problematic)
cache[question] = answer  # Too simplistic, ignored context

# Version 2: Content-aware cache
cache_key = hash(messages + question + model)  # Better but rigid

# Version 3: Intelligent cache with invalidation
class SmartCache:
    def get_cache_key(self, messages, question, context):
        # Include relevant factors that affect the answer
        factors = {
            'message_hash': self.hash_messages(messages),
            'question_embedding': self.embed_question(question),
            'model_version': self.model_version,
            'context_summary': self.summarize_context(context)
        }
        return self.generate_stable_key(factors)

    def should_invalidate(self, cache_entry):
        return (
            cache_entry['age'] > self.max_age or
            cache_entry['model'] != self.current_model or
            self.messages_updated_since(cache_entry['timestamp'])
        )
```

### Lessons from User Feedback

After sharing the tool with friends, I received valuable feedback that shaped the final version:

- **"It's too slow for quick questions"** → Added the statistical sampling mode for instant responses
- **"I want to compare different time periods"** → Built the multi-year comparison feature
- **"The terminal output is hard to read"** → Created the beautiful formatted output system
- **"Can it remember previous analyses?"** → Implemented conversation history management

The most rewarding feedback was from a friend who used it to analyze conversations with a deceased relative - finding patterns and memories they had forgotten about. This reinforced the importance of building tools that help preserve and understand our digital memories.

## Future Enhancements

Some ideas for future development:

- Support for more messaging platforms (WhatsApp, Telegram, Discord)
- Visualization dashboards for conversation patterns
- Sentiment analysis over time
- Export capabilities for research or archival purposes
- Multi-language support for international conversations

## Open Source

iMessage LLM is open source and available on [GitHub](https://github.com/aaronspindler/iMessageLLM) . Feel free to contribute, suggest features, or adapt it for your own use cases. The codebase is well-documented and modular, making it easy to extend or customize.

## Conclusion

Our digital conversations are a treasure trove of memories and insights. iMessage LLM unlocks this data, transforming years of messages into analyzable, searchable, and understandable information. Whether you're interested in relationship dynamics, personal growth, or simply want to revisit old conversations, this tool provides the infrastructure to explore your digital communication history meaningfully.

Give it a try - you might be surprised by what patterns emerge from your message history!

## Links

- [imessage-exporter](https://github.com/ReagentX/imessage-exporter) (external)
- [GitHub](https://github.com/aaronspindler/iMessageLLM) (external)

## Related Posts

- No related posts linked from this article.

---

<!-- post: tech/0005_Knowledge_Graph hash:f53f359223202edb118befbbaebf4095a9cf41983c18c7f7a7993cd20aaa12eb -->

# 0005 Knowledge Graph

> Problem: These are links between my own blog posts, like when I reference cutting out the bloat or discuss what this blog even is . Approach: Since parsing HTML and building graphs can be expensive, I implemented a caching system that: Stores parsed link data for 24 hours Only re-parses when files actually change Provides an API endpoint for the frontend to consume Outcome: The system works by parsing every blog post...

## Agent Digest

- Problem: These are links between my own blog posts, like when I reference cutting out the bloat or discuss what this blog even is .
- Aaron's position: I've been working on something that I think is pretty cool: a knowledge graph that automatically maps the connections between my blog posts and external resources.
- Approach: Since parsing HTML and building graphs can be expensive, I implemented a caching system that: Stores parsed link data for 24 hours Only re-parses when files actually change Provides an API endpoint for the frontend to consume
- Outcome: The system works by parsing every blog post and extracting two types of links:
- Audience fit: Best for readers and agents researching Aaron Spindler's tech writing on 0005 Knowledge Graph.
- Why it matters: Building a Knowledge Graph for My Blog I've been working on something that I think is pretty cool: a knowledge graph that automatically maps the connections...
- Best for:
  - Understanding the main argument of 0005 Knowledge Graph.
  - Finding citable details from Aaron's tech writing.
  - Answering questions about Building a Knowledge Graph for My Blog.
  - Answering questions about What is a Knowledge Graph?.
  - Answering questions about How It Works.
- Not for:
  - Authoritative third-party documentation.
  - A complete substitute for the canonical article body.
- Core claims:
  - I've been working on something that I think is pretty cool: a knowledge graph that automatically maps the connections between my blog posts and external resources. (Building a Knowledge Graph for My Blog)
  - In my case, it's a visual map showing: How my blog posts link to each other What external resources I reference Which posts are most connected The overall structure of knowledge I'm building Think of it like a mind map, but generated automatically by analyzing the actual links in my content. (What is a Knowledge Graph?)
  - The system works by parsing every blog post and extracting two types of links: (How It Works)
- Evidence:
  - Let me show you how this works with some examples from my existing content: (Sample Links to Demonstrate Functionality)
  - The system uses BeautifulSoup to parse HTML content and extract links with context. (1. Link Parsing)
  - Using D3.js for visualization, the system creates a force-directed graph where: Blog posts are represented as blue nodes External domains are represented as orange nodes Connections are shown as lines between nodes Node sizes reflect how connected each post is (2. Graph Building)
  - Looking at the knowledge graph, I can see patterns I wouldn't have noticed otherwise: Hub Posts: Some posts like my first one act as central nodes with many connections Knowledge Clusters: Programming posts tend to cluster together, while philosophy and productivity posts form their own groups External Influences: I... (What the Graph Reveals)
- Caveats:
  - In my case, it's a visual map showing: How my blog posts link to each other What external resources I reference Which posts are most connected The overall structure of knowledge I'm building Think of it like a mind map, but generated automatically by analyzing the actual links in my content. (What is a Knowledge Graph?)
  - It's like having a map of a vast library where you can see not just what's on each shelf, but how the books relate to each other. (Conclusion)

## Metadata

- Canonical URL: https://aaronspindler.com/b/tech/0005_Knowledge_Graph/
- Markdown URL: https://aaronspindler.com/b/tech/0005_Knowledge_Graph/index.md
- JSON URL: https://aaronspindler.com/b/tech/0005_Knowledge_Graph/index.json
- Category: tech
- Published: 2026-03-26T17:04:55+00:00
- Updated: 2026-07-08T16:59:25.470001+00:00
- Word count: 957
- Content hash: f53f359223202edb118befbbaebf4095a9cf41983c18c7f7a7993cd20aaa12eb
- AI written: no
- Format version: agent-blog-post-v2

## Takeaways

- I've been working on something that I think is pretty cool: a knowledge graph that automatically maps the connections between my blog posts and external resources. (Building a Knowledge Graph for My Blog)
- In my case, it's a visual map showing: How my blog posts link to each other What external resources I reference Which posts are most connected The overall structure of knowledge I'm building Think of it like a mind map, but generated automatically by analyzing the actual links in my content. (What is a Knowledge Graph?)
- The system works by parsing every blog post and extracting two types of links: (How It Works)
- The system automatically detects these using a pattern like /b/NNNN_title/ and creates connections between posts. (Internal Links)
- Let me show you how this works with some examples from my existing content: (Sample Links to Demonstrate Functionality)
- In my post about cutting out the bloat , I reference the philosophy of keeping things simple. (Internal Blog Connections)

## Questions Answered

- What problem does 0005 Knowledge Graph address?
- What position does Aaron take in 0005 Knowledge Graph?
- How does 0005 Knowledge Graph approach the problem?
- What evidence or outcomes does 0005 Knowledge Graph provide?
- What does the article explain about Building a Knowledge Graph for My Blog?
- What does the article explain about What is a Knowledge Graph?

## Agent Queries

- Aaron Spindler "0005 Knowledge Graph"
- 0005 Knowledge Graph tech Aaron Spindler
- 0005 Knowledge Graph These are links between my own blog posts, like when I reference cutting out the bloat or discuss what this blog even is .
- 0005 Knowledge Graph Building a Knowledge Graph for My Blog
- 0005 Knowledge Graph What is a Knowledge Graph?

## Follow-Up Questions

- What changed after the approach in 0005 Knowledge Graph was applied?
- What tradeoffs or constraints remain after 0005 Knowledge Graph?
- What setup is required before applying the ideas in 0005 Knowledge Graph?
- How would Building a Knowledge Graph for My Blog change in a different environment?

## Outline

- Building a Knowledge Graph for My Blog
-   What is a Knowledge Graph?
-   How It Works
-     Internal Links
-   Sample Links to Demonstrate Functionality
-     Internal Blog Connections
-   Technical Implementation
-     1. Link Parsing
-     2. Graph Building
-     3. Caching and Performance
-   What the Graph Reveals
-   Future Enhancements
-   Why This Matters
-   Try It Out
-   Lessons Learned
-   Conclusion

## Body

# Building a Knowledge Graph for My Blog

I've been working on something that I think is pretty cool: a knowledge graph that automatically maps the connections between my blog posts and external resources. It's not just a fancy visualization—it's a tool that helps me (and hopefully you) understand how ideas connect across different posts and see the broader web of knowledge I'm building.

## What is a Knowledge Graph?

A knowledge graph is essentially a network representation of how different pieces of information relate to each other. In my case, it's a visual map showing:

- How my blog posts link to each other
- What external resources I reference
- Which posts are most connected
- The overall structure of knowledge I'm building

Think of it like a mind map, but generated automatically by analyzing the actual links in my content.

## How It Works

The system works by parsing every blog post and extracting two types of links:

### Internal Links

These are links between my own blog posts, like when I reference [cutting out the bloat](https://aaronspindler.com/b/0002_cut_out_the_bloat/) or discuss [what this blog even is](https://aaronspindler.com/b/0001_what_even_is_this/) . The system automatically detects these using a pattern like `/b/NNNN_title/` and creates connections between posts.

## Sample Links to Demonstrate Functionality

Let me show you how this works with some examples from my existing content:

### Internal Blog Connections

In my post about [cutting out the bloat](https://aaronspindler.com/b/0002_cut_out_the_bloat/) , I reference the philosophy of keeping things simple. This creates a connection to my post about [why this blog framework isn't perfect](https://aaronspindler.com/b/0004_this_blog_aint_perfect/) , where I discuss the trade-offs of simplicity.

Similarly, my [first blog post](https://aaronspindler.com/b/0001_what_even_is_this/) sets the foundation that connects to everything else, creating a hub of connections.

## Technical Implementation

Building this wasn't just about creating a pretty visualization. I had to solve several technical challenges:

### 1. Link Parsing

The system uses BeautifulSoup to parse HTML content and extract links with context. It's smart enough to distinguish between internal blog links and external URLs, and it captures the surrounding text to provide context for each connection.

### 2. Graph Building

Using D3.js for visualization, the system creates a force-directed graph where:

- Blog posts are represented as blue nodes
- External domains are represented as orange nodes
- Connections are shown as lines between nodes
- Node sizes reflect how connected each post is

### 3. Caching and Performance

Since parsing HTML and building graphs can be expensive, I implemented a caching system that:

- Stores parsed link data for 24 hours
- Only re-parses when files actually change
- Provides an API endpoint for the frontend to consume

## What the Graph Reveals

Looking at the knowledge graph, I can see patterns I wouldn't have noticed otherwise:

- **Hub Posts:** Some posts like my first one act as central nodes with many connections
- **Knowledge Clusters:** Programming posts tend to cluster together, while philosophy and productivity posts form their own groups
- **External Influences:** I can see which external resources I reference most frequently
- **Gaps:** Posts with few connections might indicate areas where I could add more cross-references

## Future Enhancements

This is just the beginning. I'm thinking about adding:

- **Semantic Analysis:** Using NLP to detect conceptual connections beyond just explicit links
- **Time-based Visualization:** Showing how the knowledge graph evolves over time
- **Search Integration:** Using the graph to improve search results and suggest related content
- **Interactive Exploration:** Allowing users to explore connections and discover new content paths

## Why This Matters

Beyond the technical achievement, this knowledge graph serves several important purposes:

- **Content Discovery:** Readers can see how ideas connect and discover related content
- **Quality Assurance:** I can identify posts that need better cross-referencing
- **Knowledge Mapping:** It helps me understand the structure of what I'm building
- **Community Building:** Shows the interconnected nature of knowledge and ideas

## Try It Out

You can see the knowledge graph in action on my [homepage](https://aaronspindler.com/) . It's interactive—you can:

- Hover over nodes to see details
- Click on blog post nodes to navigate to those posts
- Click on external nodes to visit those resources
- Drag nodes around to explore different layouts
- Zoom in and out to see the full picture

The graph updates automatically as I add new posts and links, so it's always current with my latest content.

## Lessons Learned

Building this feature taught me several valuable lessons:

- **Start Simple:** I began with basic link extraction and gradually added complexity
- **Performance Matters:** Caching and optimization are crucial for user experience
- **Visualization is Powerful:** Seeing connections visually reveals patterns that text alone can't show
- **Automation is Key:** The system works without manual intervention, making it sustainable

## Conclusion

This knowledge graph represents more than just a technical feature—it's a reflection of how I think about knowledge and learning. Knowledge isn't isolated facts; it's a network of interconnected ideas that build upon each other.

By making these connections visible, I hope to help readers (and myself) better understand the relationships between different concepts and discover new paths of exploration. It's like having a map of a vast library where you can see not just what's on each shelf, but how the books relate to each other.

As I continue to write and build this blog, the knowledge graph will grow and evolve, becoming an increasingly valuable tool for navigating the ideas and insights I'm sharing. It's a living, breathing representation of the intellectual journey I'm on, and I'm excited to see where it leads.

If you're interested in the technical details or want to build something similar, feel free to explore the code or reach out. The beauty of building in public is that we can all learn from each other's experiments and discoveries.

Now, go explore the knowledge graph and see what connections you can discover!

## Links

- [cutting out the bloat](https://aaronspindler.com/b/0002_cut_out_the_bloat/) (internal)
- [what this blog even is](https://aaronspindler.com/b/0001_what_even_is_this/) (internal)
- [why this blog framework isn't perfect](https://aaronspindler.com/b/0004_this_blog_aint_perfect/) (internal)
- [first blog post](https://aaronspindler.com/b/0001_what_even_is_this/) (internal)
- [homepage](https://aaronspindler.com/) (internal)

## Related Posts

- [0002 Cut Out The Bloat](https://aaronspindler.com/b/tech/0002_Cut_Out_The_Bloat/)
- [0001 What Even Is This](https://aaronspindler.com/b/personal/0001_What_Even_Is_This/)
- [0004 This Blog Aint Perfect](https://aaronspindler.com/b/personal/0004_This_Blog_Aint_Perfect/)

---

<!-- post: personal/0004_This_Blog_Aint_Perfect hash:b2f1ee14fa5ba9f8fce055469765b4600508b2d00ed67d93dc83c02fa1312b13 -->

# 0004 This Blog Aint Perfect

> Problem: No paywall Despite these imperfections, it aligns perfectly with my philosophy about cutting out the bloat in web development. Approach: I have been writing with this blog framework that I built for a few days now. Outcome: This Blog Framework Ain't Perfect I have been writing with this blog framework that I built for a few days now.

## Agent Digest

- Problem: No paywall Despite these imperfections, it aligns perfectly with my philosophy about cutting out the bloat in web development.
- Aaron's position: I have been writing with this blog framework that I built for a few days now.
- Approach: I have been writing with this blog framework that I built for a few days now.
- Outcome: This Blog Framework Ain't Perfect I have been writing with this blog framework that I built for a few days now.
- Audience fit: Best for readers and agents researching Aaron Spindler's personal writing on 0004 This Blog Aint Perfect.
- Why it matters: This Blog Framework Ain't Perfect I have been writing with this blog framework that I built for a few days now.
- Best for:
  - Understanding the main argument of 0004 This Blog Aint Perfect.
  - Finding citable details from Aaron's personal writing.
  - Answering questions about This Blog Framework Ain't Perfect.
  - Answering questions about Why is it not perfect?.
  - Answering questions about Why is it almost perfect?.
- Not for:
  - Authoritative third-party documentation.
  - A complete substitute for the canonical article body.
- Core claims:
  - I have been writing with this blog framework that I built for a few days now. (This Blog Framework Ain't Perfect)
  - There is not a good way of getting a created/updated date for blog posts because of the way git handles the files on push to the server. (Why is it not perfect?)
  - No paywall Despite these imperfections, it aligns perfectly with my philosophy about cutting out the bloat in web development. (Why is it almost perfect?)
- Evidence:
  - I have been writing with this blog framework that I built for a few days now. (This Blog Framework Ain't Perfect)
  - This causes ordering of blog posts to be random. (Why is it not perfect?)
  - No paywall Despite these imperfections, it aligns perfectly with my philosophy about cutting out the bloat in web development. (Why is it almost perfect?)
  - Simple, fast, and gets the job done without unnecessary complexity. (Why is it almost perfect?)

## Metadata

- Canonical URL: https://aaronspindler.com/b/personal/0004_This_Blog_Aint_Perfect/
- Markdown URL: https://aaronspindler.com/b/personal/0004_This_Blog_Aint_Perfect/index.md
- JSON URL: https://aaronspindler.com/b/personal/0004_This_Blog_Aint_Perfect/index.json
- Category: personal
- Published: 2026-03-26T17:04:55+00:00
- Updated: 2026-07-08T16:59:26.862008+00:00
- Word count: 259
- Content hash: b2f1ee14fa5ba9f8fce055469765b4600508b2d00ed67d93dc83c02fa1312b13
- AI written: no
- Format version: agent-blog-post-v2

## Takeaways

- I have been writing with this blog framework that I built for a few days now. (This Blog Framework Ain't Perfect)
- There is not a good way of getting a created/updated date for blog posts because of the way git handles the files on push to the server. (Why is it not perfect?)
- No paywall Despite these imperfections, it aligns perfectly with my philosophy about cutting out the bloat in web development. (Why is it almost perfect?)

## Questions Answered

- What problem does 0004 This Blog Aint Perfect address?
- What position does Aaron take in 0004 This Blog Aint Perfect?
- How does 0004 This Blog Aint Perfect approach the problem?
- What evidence or outcomes does 0004 This Blog Aint Perfect provide?
- What does the article explain about This Blog Framework Ain't Perfect?
- What does the article explain about Why is it not perfect?

## Agent Queries

- Aaron Spindler "0004 This Blog Aint Perfect"
- 0004 This Blog Aint Perfect personal Aaron Spindler
- 0004 This Blog Aint Perfect No paywall Despite these imperfections, it aligns perfectly with my philosophy about cutting out the bloat in web development.
- 0004 This Blog Aint Perfect This Blog Framework Ain't Perfect
- 0004 This Blog Aint Perfect Why is it not perfect?

## Follow-Up Questions

- What changed after the approach in 0004 This Blog Aint Perfect was applied?
- What tradeoffs or constraints remain after 0004 This Blog Aint Perfect?
- How would This Blog Framework Ain't Perfect change in a different environment?
- How would Why is it not perfect change in a different environment?

## Outline

- This Blog Framework Ain't Perfect
-   Why is it not perfect?
-   Why is it almost perfect?

## Body

# This Blog Framework Ain't Perfect

I have been writing with this blog framework that I built for a few days now. It's not perfect, but it's mine.

I will continue to improve it, and will hopefully track it in here by crossing items off the not-perfect list.

## Why is it not perfect?

- There is not a good way of getting a created/updated date for blog posts because of the way git handles the files on push to the server.
  - This causes ordering of blog posts to be random. I cannot easily control the order of the blog posts.
  - Every time I push a change to the server, the created and updated dates are the date of the push on the server. This is not the date of the last change to the file, it's the date of the push.
- There is no built-in commenting system.
- Titles cannot be whatever I want. They must be the name of the file.
  - I cannot easily control the capitalization of the title, since I am using `.title()`
- My local DB is the same as the production DB.

## Why is it almost perfect?

- I get to write in just basic HTML. No complicated templating engine.
- There is built in traceability. Every change to a blog post is tracked on github.
- It is incredibly fast.
- No subscription fees.
- No bullshit analytics.
- No paywall

Despite these imperfections, it aligns perfectly with my philosophy about [cutting out the bloat](https://aaronspindler.com/b/0002_cut_out_the_bloat/) in web development. Simple, fast, and gets the job done without unnecessary complexity.

## Links

- [cutting out the bloat](https://aaronspindler.com/b/0002_cut_out_the_bloat/) (internal)

## Related Posts

- [0002 Cut Out The Bloat](https://aaronspindler.com/b/tech/0002_Cut_Out_The_Bloat/)

---

<!-- post: projects/0003_ActionsUptime_Build_and_Walk hash:e430839f22443edf842a564373ad89cbeeb6f903d36f6f8e9aca0949c9adcded -->

# 0003 ActionsUptime Build and Walk

> Problem: Building ActionsUptime.com: A Treadmill-Powered Journey in One Week Ever wondered what happens when you combine a crazy idea, a treadmill, and a week of intense... Approach: By the end of the week, I had: Built a fully functional version of ActionsUptime.com Walked almost 20KM Improved my focus and productivity Outcome: After the success of this experiment, I'm considering making the treadmill desk a...

## Agent Digest

- Problem: Building ActionsUptime.com: A Treadmill-Powered Journey in One Week Ever wondered what happens when you combine a crazy idea, a treadmill, and a week of intense...
- Aaron's position: Well, buckle up, because I'm about to take you on a wild ride through my journey of building ActionsUptime.com while walking on a treadmill in just one week!
- Approach: By the end of the week, I had: Built a fully functional version of ActionsUptime.com Walked almost 20KM Improved my focus and productivity
- Outcome: After the success of this experiment, I'm considering making the treadmill desk a permanent fixture in my workspace.
- Audience fit: Best for readers and agents researching Aaron Spindler's projects writing on 0003 ActionsUptime Build and Walk.
- Why it matters: Building ActionsUptime.com: A Treadmill-Powered Journey in One Week Ever wondered what happens when you combine a crazy idea, a treadmill, and a week of intense...
- Best for:
  - Understanding the main argument of 0003 ActionsUptime Build and Walk.
  - Finding citable details from Aaron's projects writing.
  - Answering questions about Building ActionsUptime.com : A Treadmill-Powered Journey in One Week.
  - Answering questions about The Inception.
  - Answering questions about Day 1-2: Setting Up and Planning.
- Not for:
  - Authoritative third-party documentation.
  - A complete substitute for the canonical article body.
- Core claims:
  - Well, buckle up, because I'm about to take you on a wild ride through my journey of building ActionsUptime.com while walking on a treadmill in just one week! (Building ActionsUptime.com : A Treadmill-Powered Journey in One Week)
  - It all started with a simple thought: "I have all of these uptime monitors on my websites, but I don't really have a good way to monitor my github actions (which I use for all of my projects, and I have many of them)" I also just got a walking pad, so I thought it would be a fun experiment to see if I could build... (The Inception)
  - I need auth, hosting, a domain, a database, and some way to monitor github actions. (Day 1-2: Setting Up and Planning)
- Evidence:
  - It all started with a simple thought: "I have all of these uptime monitors on my websites, but I don't really have a good way to monitor my github actions (which I use for all of my projects, and I have many of them)" I also just got a walking pad, so I thought it would be a fun experiment to see if I could build... (The Inception)
  - I implemented the core functionality of ActionsUptime.com , including API integrations with GitHub and data processing logic. (Day 3-4: Core Functionality)
  - I also wrote and ran tests, all while clocking in miles on my treadmill. (Day 5-6: User Interface and Testing)
  - On the final day, with sore legs but a clear mind, I put the finishing touches on ActionsUptime.com . (Day 7: Polishing and Launching)
- Caveats:
  - It all started with a simple thought: "I have all of these uptime monitors on my websites, but I don't really have a good way to monitor my github actions (which I use for all of my projects, and I have many of them)" I also just got a walking pad, so I thought it would be a fun experiment to see if I could build... (The Inception)
  - On the final day, with sore legs but a clear mind, I put the finishing touches on ActionsUptime.com . (Day 7: Polishing and Launching)
  - I optimized performance, added some ways to be notified when actions fail, and finally hit the 'deploy' button. (Day 7: Polishing and Launching)
  - The sense of accomplishment was doubled - not only had I built a useful tool, but I'd also walked a marathon's worth of steps! (Day 7: Polishing and Launching)

## Metadata

- Canonical URL: https://aaronspindler.com/b/projects/0003_ActionsUptime_Build_and_Walk/
- Markdown URL: https://aaronspindler.com/b/projects/0003_ActionsUptime_Build_and_Walk/index.md
- JSON URL: https://aaronspindler.com/b/projects/0003_ActionsUptime_Build_and_Walk/index.json
- Category: projects
- Published: 2026-03-26T17:04:55+00:00
- Updated: 2026-07-08T16:59:28.300930+00:00
- Word count: 502
- Content hash: e430839f22443edf842a564373ad89cbeeb6f903d36f6f8e9aca0949c9adcded
- AI written: no
- Format version: agent-blog-post-v2

## Takeaways

- Well, buckle up, because I'm about to take you on a wild ride through my journey of building ActionsUptime.com while walking on a treadmill in just one week! (Building ActionsUptime.com : A Treadmill-Powered Journey in One Week)
- It all started with a simple thought: "I have all of these uptime monitors on my websites, but I don't really have a good way to monitor my github actions (which I use for all of my projects, and I have many of them)" I also just got a walking pad, so I thought it would be a fun experiment to see if I could build... (The Inception)
- I need auth, hosting, a domain, a database, and some way to monitor github actions. (Day 1-2: Setting Up and Planning)
- I implemented the core functionality of ActionsUptime.com , including API integrations with GitHub and data processing logic. (Day 3-4: Core Functionality)
- I designed and implemented the user interface, making sure it was responsive and user-friendly. (Day 5-6: User Interface and Testing)
- On the final day, with sore legs but a clear mind, I put the finishing touches on ActionsUptime.com . (Day 7: Polishing and Launching)

## Questions Answered

- What problem does 0003 ActionsUptime Build and Walk address?
- What position does Aaron take in 0003 ActionsUptime Build and Walk?
- How does 0003 ActionsUptime Build and Walk approach the problem?
- What evidence or outcomes does 0003 ActionsUptime Build and Walk provide?
- What does the article explain about Building ActionsUptime.com : A Treadmill-Powered Journey in One Week?
- What does the article explain about The Inception?

## Agent Queries

- Aaron Spindler "0003 ActionsUptime Build and Walk"
- 0003 ActionsUptime Build and Walk projects Aaron Spindler
- 0003 ActionsUptime Build and Walk Building ActionsUptime.com: A Treadmill-Powered Journey in One Week Ever wondered what happens when you combine a crazy idea, a treadmill, and a week of intense...
- 0003 ActionsUptime Build and Walk Building ActionsUptime.com : A Treadmill-Powered Journey in One Week
- 0003 ActionsUptime Build and Walk The Inception

## Follow-Up Questions

- What changed after the approach in 0003 ActionsUptime Build and Walk was applied?
- What tradeoffs or constraints remain after 0003 ActionsUptime Build and Walk?
- What setup is required before applying the ideas in 0003 ActionsUptime Build and Walk?
- How would Building ActionsUptime.com : A Treadmill-Powered Journey in One Week change in a different environment?

## Outline

- Building ActionsUptime.com : A Treadmill-Powered Journey in One Week
-   The Inception
-   Day 1-2: Setting Up and Planning
-   Day 3-4: Core Functionality
-   Day 5-6: User Interface and Testing
-   Day 7: Polishing and Launching
-   The Results
-   Lessons Learned
-   What's Next?

## Body

# Building [ActionsUptime.com](https://ActionsUptime.com) : A Treadmill-Powered Journey in One Week

Ever wondered what happens when you combine a crazy idea, a treadmill, and a week of intense coding? Well, buckle up, because I'm about to take you on a wild ride through my journey of building [ActionsUptime.com](https://ActionsUptime.com) while walking on a treadmill in just one week!

## The Inception

It all started with a simple thought: "I have all of these uptime monitors on my websites, but I don't really have a good way to monitor my github actions (which I use for all of my projects, and I have many of them)"

I also just got a walking pad, so I thought it would be a fun experiment to see if I could build something while walking on the treadmill.

## Day 1-2: Setting Up and Planning

The first two days were all about setting up the site. I need auth, hosting, a domain, a database, and some way to monitor github actions. I decided to use postgres for the database, and django for the framework. I even setup a [repo to use as a building block](https://github.com/aaronspindler/base_app) for future projects.

## Day 3-4: Core Functionality

By day three, I was finding my rhythm - both in walking and coding. I implemented the core functionality of [ActionsUptime.com](https://ActionsUptime.com) , including API integrations with GitHub and data processing logic. The constant movement seemed to keep my mind sharp and focused.

## Day 5-6: User Interface and Testing

As I entered the latter half of the week, I was practically dancing on the treadmill. I designed and implemented the user interface, making sure it was responsive and user-friendly. I also wrote and ran tests, all while clocking in miles on my treadmill.

## Day 7: Polishing and Launching

On the final day, with sore legs but a clear mind, I put the finishing touches on [ActionsUptime.com](https://ActionsUptime.com) . I optimized performance, added some ways to be notified when actions fail, and finally hit the 'deploy' button. The sense of accomplishment was doubled - not only had I built a useful tool, but I'd also walked a marathon's worth of steps!

## The Results

By the end of the week, I had:

- Built a fully functional version of [ActionsUptime.com](https://ActionsUptime.com)
- Walked almost 20KM
- Improved my focus and productivity

## Lessons Learned

This experiment taught me that movement and coding can go hand in hand. The constant low-intensity exercise kept me alert and focused, leading to more productive coding sessions. It also reinforced the importance of taking breaks and staying active, even when deep in a project.

## What's Next?

After the success of this experiment, I'm considering making the treadmill desk a permanent fixture in my workspace. As for [ActionsUptime.com](https://ActionsUptime.com) , I'm excited to continue developing and improving it. Who knows, maybe the next feature will be built while cycling!

Remember, innovation doesn't just happen in your mind - sometimes, it takes a few steps in the right direction. So, why not give it a try? Your next big project might just be a walk away!

## Links

- [ActionsUptime.com](https://ActionsUptime.com) (external)
- [repo to use as a building block](https://github.com/aaronspindler/base_app) (external)

## Related Posts

- No related posts linked from this article.

---

<!-- post: tech/0002_Cut_Out_The_Bloat hash:cd54618c0c836f42c06fc2c01a28c3f95933b9920ea1a3749cb291d615da7f55 -->

# 0002 Cut Out The Bloat

> Problem: But I wanted to try something new, and I am happy with the results. Approach: I am not innocent of this, I have built many bloated applications. Outcome: There are so many frameworks and tools that are all shouting for attention.

## Agent Digest

- Problem: But I wanted to try something new, and I am happy with the results.
- Aaron's position: But I wanted to try something new, and I am happy with the results.
- Approach: I am not innocent of this, I have built many bloated applications.
- Outcome: There are so many frameworks and tools that are all shouting for attention.
- Audience fit: Best for readers and agents researching Aaron Spindler's tech writing on 0002 Cut Out The Bloat.
- Why it matters: There are so many frameworks and tools that are all shouting for attention. Web dev has become such a bloated mess. There are so many frameworks and tools that...
- Best for:
  - Understanding the main argument of 0002 Cut Out The Bloat.
  - Finding citable details from Aaron's tech writing.
  - Answering questions about Article.
- Not for:
  - Authoritative third-party documentation.
  - A complete substitute for the canonical article body.
- Core claims:
  - But I wanted to try something new, and I am happy with the results. (Article)
- Evidence:
  - And there's nothing wrong with that. (Article)
  - You can do it with just HTML, CSS, and Python. (Article)
  - This site is built with Django, could it have been built with a static site generator? (Article)
  - But I wanted to try something new, and I am happy with the results. (Article)
- Caveats:
  - But we should at least be aware of what we're putting into our projects. (Article)
  - But if you're just building a simple website, you don't need all of that. (Article)
  - But I am trying to cut out the bloat and get back to the basics. (Article)
  - But I wanted to try something new, and I am happy with the results. (Article)

## Metadata

- Canonical URL: https://aaronspindler.com/b/tech/0002_Cut_Out_The_Bloat/
- Markdown URL: https://aaronspindler.com/b/tech/0002_Cut_Out_The_Bloat/index.md
- JSON URL: https://aaronspindler.com/b/tech/0002_Cut_Out_The_Bloat/index.json
- Category: tech
- Published: 2026-03-26T17:04:55+00:00
- Updated: 2026-07-08T16:59:29.713531+00:00
- Word count: 230
- Content hash: cd54618c0c836f42c06fc2c01a28c3f95933b9920ea1a3749cb291d615da7f55
- AI written: no
- Format version: agent-blog-post-v2

## Takeaways

- But I wanted to try something new, and I am happy with the results. (Article)

## Questions Answered

- What problem does 0002 Cut Out The Bloat address?
- What position does Aaron take in 0002 Cut Out The Bloat?
- How does 0002 Cut Out The Bloat approach the problem?
- What evidence or outcomes does 0002 Cut Out The Bloat provide?
- What does the article explain about Article?

## Agent Queries

- Aaron Spindler "0002 Cut Out The Bloat"
- 0002 Cut Out The Bloat tech Aaron Spindler
- 0002 Cut Out The Bloat But I wanted to try something new, and I am happy with the results.
- 0002 Cut Out The Bloat Article

## Follow-Up Questions

- What changed after the approach in 0002 Cut Out The Bloat was applied?
- What tradeoffs or constraints remain after 0002 Cut Out The Bloat?
- What setup is required before applying the ideas in 0002 Cut Out The Bloat?
- How would Article change in a different environment?

## Outline

- No headings extracted.

## Body

Web dev has become such a bloated mess. There are so many frameworks and tools that are all shouting for attention. It's time to cut out the bloat and get back to the basics.

I'm not saying we should all be using just HTML and CSS. That would be ridiculous. But we should at least be aware of what we're putting into our projects. And we should be using the right tool for the job.

For example, there's a lot of talk about using frameworks like React and Vue. And there's nothing wrong with that. But if you're just building a simple website, you don't need all of that. You can do it with just HTML, CSS, and Python.

I am not innocent of this, I have built many bloated applications. But I am trying to cut out the bloat and get back to the basics.

This site is built with Django, could it have been built with a static site generator? Yes. But I wanted to try something new, and I am happy with the results. As I mentioned in [my first blog post](https://aaronspindler.com/b/0001_what_even_is_this/) , I felt that most blogging platforms are very bloated and clunky.

The framework I built for this blog isn't perfect though, as I discuss in [this follow-up post](https://aaronspindler.com/b/0004_this_blog_aint_perfect/) , but it gets the job done without unnecessary complexity.

## Links

- [my first blog post](https://aaronspindler.com/b/0001_what_even_is_this/) (internal)
- [this follow-up post](https://aaronspindler.com/b/0004_this_blog_aint_perfect/) (internal)

## Related Posts

- [0001 What Even Is This](https://aaronspindler.com/b/personal/0001_What_Even_Is_This/)
- [0004 This Blog Aint Perfect](https://aaronspindler.com/b/personal/0004_This_Blog_Aint_Perfect/)

---

<!-- post: personal/0001_What_Even_Is_This hash:c23178aeaba30c3e9ef7e7fbc20b3fe5ae3c1aed6026c4c5fb6df4f5767d2a78 -->

# 0001 What Even Is This

> Problem: I felt that most blogging platforms are very bloated and clunky, so I decided to make my own. Approach: I will be writing here about tech, life, family, memes, and other random stuff that comes to my mind.

## Agent Digest

- Problem: I felt that most blogging platforms are very bloated and clunky, so I decided to make my own.
- Aaron's position: I will be writing here about tech, life, family, memes, and other random stuff that comes to my mind.
- Approach: I will be writing here about tech, life, family, memes, and other random stuff that comes to my mind.
- Outcome: I will be writing here about tech, life, family, memes, and other random stuff that comes to my mind.
- Audience fit: Best for readers and agents researching Aaron Spindler's personal writing on 0001 What Even Is This.
- Why it matters: I will be writing here about tech, life, family, memes, and other random stuff that comes to my mind.
- Best for:
  - Understanding the main argument of 0001 What Even Is This.
  - Finding citable details from Aaron's personal writing.
  - Answering questions about Article.
- Not for:
  - Authoritative third-party documentation.
  - A complete substitute for the canonical article body.
- Core claims:
  - I will be writing here about tech, life, family, memes, and other random stuff that comes to my mind. (Article)
- Evidence:
  - I will be writing here about tech, life, family, memes, and other random stuff that comes to my mind. (Article)

## Metadata

- Canonical URL: https://aaronspindler.com/b/personal/0001_What_Even_Is_This/
- Markdown URL: https://aaronspindler.com/b/personal/0001_What_Even_Is_This/index.md
- JSON URL: https://aaronspindler.com/b/personal/0001_What_Even_Is_This/index.json
- Category: personal
- Published: 2026-03-26T17:04:55+00:00
- Updated: 2026-07-08T16:59:31.166312+00:00
- Word count: 149
- Content hash: c23178aeaba30c3e9ef7e7fbc20b3fe5ae3c1aed6026c4c5fb6df4f5767d2a78
- AI written: no
- Format version: agent-blog-post-v2

## Takeaways

- I will be writing here about tech, life, family, memes, and other random stuff that comes to my mind. (Article)

## Questions Answered

- What problem does 0001 What Even Is This address?
- What position does Aaron take in 0001 What Even Is This?
- How does 0001 What Even Is This approach the problem?
- What evidence or outcomes does 0001 What Even Is This provide?
- What does the article explain about Article?

## Agent Queries

- Aaron Spindler "0001 What Even Is This"
- 0001 What Even Is This personal Aaron Spindler
- 0001 What Even Is This I felt that most blogging platforms are very bloated and clunky, so I decided to make my own.
- 0001 What Even Is This Article

## Follow-Up Questions

- What changed after the approach in 0001 What Even Is This was applied?
- What tradeoffs or constraints remain after 0001 What Even Is This?
- How would Article change in a different environment?

## Outline

- No headings extracted.

## Body

This is my very first blog post. I will be writing here about tech, life, family, memes, and other random stuff that comes to my mind.

I felt that most blogging platforms are very bloated and clunky, so I decided to make my own. I tried out Ghost, Wordpress, and Medium; which all were very clunky and difficult for me to implement exactly what I wanted. This whole system is open source and renders out based on html files that are in a specific directory.

I get to write basically pure HTML, and out comes a blog post. How rad is that!

I am not a writer, so please forgive me if this is hard to read.

Speaking of bloat, I wrote more about this topic in my post about [cutting out the bloat](https://aaronspindler.com/b/0002_cut_out_the_bloat/) in web development.

Feel free to checkout my X for more up to date posts.

## Links

- [cutting out the bloat](https://aaronspindler.com/b/0002_cut_out_the_bloat/) (internal)

## Related Posts

- [0002 Cut Out The Bloat](https://aaronspindler.com/b/tech/0002_Cut_Out_The_Bloat/)
