>
B2B SaaS

A Wrapped-style year in review for your self-hosted stack

Spotify Wrapped became a cultural phenomenon because people love seeing their data visualized. Hours listened. Top artists. Genre breakdowns. It turns abstract activity into something tangible and shareable. But here is the thing. Spotify is not the only app that knows what you did this year. Your self-hosted apps know too. And unlike Spotify, your self-hosted apps do not need a marketing team to build a Wrapped page for you.

I built a self-hosted Wrapped dashboard for my homelab in a weekend. It pulls from seven different services I run, aggregates the data into a single HTML page, and renders the result in a way that looks like the kind of year-end summary a SaaS company would send you. The whole thing is under 200 lines of Python and a single HTML template. Here is what I learned.

The data you already have

The first surprise was how much data my self-hosted services were already collecting. I just had not been looking at it.

  • Paperless-ngx logs every document I scanned in 2025. I expected 50. The actual number is 412. Receipts, tax forms, warranty cards, manuals, and a folder full of photos I took of whiteboards at conferences.
  • Actual Budget has every transaction since I started using it. I expected a year of consistent spending. The actual breakdown shows that I spent 23% of my annual food budget in November, which tracks with a houseguest.
  • Mealie has 187 recipes, of which I cooked 64. The most-cooked recipe is a one-pan chicken and rice I have made 11 times. The least-cooked is a complicated pho recipe I imported and never touched.
  • Stirling PDF has processed 89 files for me this year, almost all of them tax documents I had to redact. I have a 12-page PDF of redacted 1099 forms I sent to my accountant in April.

That is four services, four datasets, and I had not connected any of them. The Wrapped page was a way to do that for the first time.

How I built it

I am not a frontend developer, so the page is not fancy. It is a single HTML file with a few sections, each one showing a different stat. The data comes from a Python script that queries the local API of each service, aggregates the results, and writes a JSON file the HTML page reads.

I used a few small libraries to keep the code small. httpx for the HTTP requests. jinja2 for the HTML template. That is it. The whole script is 184 lines including comments. I run it as a cron job on the first of January, and the resulting HTML page goes into a public folder served by Caddy.

The hardest part was the Paperless-ngx integration. The API returns paginated results, and I had to walk the pagination to count documents. The other services all have a single endpoint that returns a count. I spent an hour on Paperless-ngx and ten minutes on the other six services combined.

The Wrapped page itself

The page has seven sections, one per service. Each section has a headline number, a short paragraph explaining what it means, and a small chart or list of supporting details. The Mealie section shows the 11-times-made chicken and rice at the top of the “most cooked” list. The Actual Budget section shows the November spending spike as a horizontal bar chart. The Paperless-ngx section shows the months with the most scans, which line up suspiciously well with tax season.

I have a “share” button on the page that copies a permalink. I have not actually shared the page with anyone. The data is mine, and the people in my life do not need to know that I scanned 412 documents this year. But the page exists, and I look at it on January 2 every year, and that is the point.

What surprised me

Three things stood out when I saw the data for the first time.

Volume was the first surprise. I had no idea I had scanned 412 documents. I thought it was closer to 100. The Wrapped page showed me a category breakdown I would not have looked at otherwise, and the breakdown revealed that 38% of the scans were receipts and another 22% were manuals. The remaining 40% is a long tail of one-off things.

Consistency was the second surprise. The Actual Budget data showed I have been within 5% of my monthly target for nine of the twelve months. The three outlier months were November (houseguest), July (a vacation), and March (a new piece of furniture I had been saving for). Knowing the shape of the year, instead of just the year-end total, changed how I think about the budget.

The gap was the third surprise. The Stirling PDF data showed that 89 of my PDFs were redacted tax documents. That is a lot of redaction. I had not realized how much of my year was spent dealing with paperwork that I would not have to deal with if the US tax code were simpler. The Wrapped page did not fix the problem, but it made the size of the problem visible.

Trade-offs

API stability is the first cost. Every one of these services has its own REST API (a standard way for programs to talk to each other over HTTP), and the APIs change. Paperless-ngx had a breaking API change in 2024 that I missed, and my counter was wrong for two months. I caught it because the Wrapped number did not match my mental model. Without the Wrapped page, I would not have noticed.

Aggregation logic is the second cost. The “most cooked recipe” stat is easy. The “spending category you improved on most” stat is hard, and the math depends on how you define “improved.” I have three different versions of that stat in my code, and I am not sure any of them are right. The Wrapped page is opinionated in ways I have to defend.

The temptation to share is the third cost. A Wrapped page is shareable, and the data is personal. I built mine on a homelab because I did not want Spotify or any other company to have a copy. If I post the page publicly, the data leaves my network. I have not done that, and I am not going to.

For a small homelab with one or two services, this project is overkill. For a homelab with five or more services that each collect their own data, the Wrapped page is a good way to see the year at a glance. The migration took me a weekend. Most of that was reading the API docs for the services I had not integrated before.

What I would tell past me

If I could send a message back to the version of me that started running self-hosted apps in 2019, I would say three things.

  • Your services are collecting more data than you think. I had a year of data in seven services and I had never looked at any of it as a single dataset.
  • Visualization changes the questions you ask. I do not care about the number of documents I scanned. I care that 38% of them were receipts. The breakdown is the insight.
  • Keep the data local. The whole point of self-hosting is that the data does not leave your network. A Wrapped page is a great way to celebrate that, without giving a SaaS company a copy of your year.

If you have a homelab and a weekend, build yourself a Wrapped page. The hardest part is the Paperless-ngx pagination. Everything else is a single API call.

Filed under: #metrics]

Leave a comment