Building a Productivity & Focus App #2

After laying the groundwork for my Chrome extension in Day 1, today was all reading the chrome api docs and learning about the data flow — capturing it, validating it, and sending it to the backend. I’ve made solid progress on the technical pipeline, and I’m to get a feel of how the database should be structured

Getting data out of Chrome

Chrome exposes a lot of useful APIs for tracking user activity with webRequest, tabs, window, idle

  • When a new URL loads - onCompleted

  • When the user switches tabs - onActivated

  • When the window focus changes - onFocusChanged

  • When the user goes idle or returns - onStateChanged

These events give me just enough data to reconstruct what I care about: What site am I on? Am I actively using it? How long did I spend? This data also answered my previous questions like, how am I supposed to track user attention? Not knowing they had all of it already figured out for me.

But here’s the tricky part: these events fire constantly, and sometimes the data is noisy or redundant like same documentID resulting in what felt like 100 streams of the same youtube-url firing. So I first passed all of it to a SET making sure i ignore all rednudant data then pass it to a buffer locally in chrome.storage batching them, and then flushing them to my server every few seconds and per max .

Sending data to the backend

Once the data is ready, it’s sent to my Node.js server using fetch call inside the background service worker. The request body contains fields like url, tabId, timestamp.

On the server, I validate the data using Joi before storing it in my PostgreSQL database. This gives me strict control over what goes in — no invalid types, no malformed data. Only clean, typed, expected fields get saved.

Here’s a big learning moment: I realized that SQL injection is prevented by using parameterized queries with pg Instead of injecting raw strings, I pass my values as an array to keeps things secure and efficient.

Managing URL bloat

I discovered that some url-fields were huge. They included tracking parameters, long hashes, and session IDs. This made my logs bloated and hard to work with. The next step was figuring out how to extract just the useful part of the URL — like the hostname — without losing meaningful context. I did this by parsing the data with the filter method before inserting it into the database.

So during this time I have added:

-Chrome background script with multiple event listeners

-Local buffer in chrome.storage

-Setup PostgresDB

-Batched uploads to backend

-Secure PostgreSQL inserts with validation via Joi

I also spent some time thinking about how to structure the database properly. I ended up creating three different tables: one for webrequest since it’s my main source of data; one for focus changes, because I noticed that data was often redundant and repeating each time I unfocused a tab — but still valuable for tracking attention; and a third table for the rest of the event listeners.

I didn’t worry too much about the repetition in the data — like repeating URLs or titles — because I realized that having everything stored explicitly will make it easier to query and analyze later. Also, I know that my database isn’t going to grow much beyond this scope, so I didn’t need to over-optimize at this point.

The idea was to avoid overanalyzing everything upfront. I wanted to keep the momentum going and get a working system. Once everything is up and running and I have real data flowing through it, I’ll be in a much better position to tweak the structure based on how it actually behaves.

Coming Up

I’m going to build out the endpoints to retrieve this data and design a basic UI. Then I’ll go deeper into understanding the difference between idle time and real user activity. Then finally i will add the AI on top of it and having it make decisions based user productivity goals.

0
Subscribe to my newsletter

Read articles from Elias Ekberg Gasper directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Elias Ekberg Gasper
Elias Ekberg Gasper