Building LinkedIn Growth OS: A 10,000-Line Personal CRM I Never Planned to Build
LinkedIn's native analytics are broken. Not broken like a bug — broken like they were designed by someone who's never tried to grow an audience. You get a vague impressions number. Maybe a reaction count. No memory. No benchmarking against yourself. No way to know if a post performed well or just triggered a notification spike that decayed in four hours.
So I built a spreadsheet. Then I built a script that pulled data from the spreadsheet. Then I built a dashboard. Then I looked up and it was 10,000 lines of TypeScript.
The Scope Creep Problem
LinkedIn Growth OS has 19 API routes, 7 data models, and 41 React components. None of that was planned. It grew because every time I used the early version, I found something else that was annoying about LinkedIn that I could fix.
The original spec: log a post, record its metrics, see a table. The final product: Day 1/7/30 checkpoint tracking, a normalized scoring engine, four AI features powered by the Claude API, a content ideas vault, a voice recorder for ideas, and a post autopsy system that tells you exactly why something worked or didn't.
The Day 1/7/30 System
Here's the insight that made this worth building: measuring a LinkedIn post once is almost meaningless. Impressions decay. A post might get 80% of its reach in the first 24 hours, or it might keep climbing for a week. You can't tell from a single snapshot.
So every post gets three checkpoints: Day 1 (initial spike), Day 7 (whether it held), Day 30 (long tail). The system flags which checkpoints are overdue and surfaces them on a dashboard. You pull the current number, log it, and move on. Takes 30 seconds per post.
This alone made the tool worth using. I now know which of my posts are evergreen and which peaked immediately. That changes how I think about what to write next.
The Scoring Engine
Absolute impression numbers are misleading — especially when your audience size is changing. A post with 500 impressions when you have 200 followers is better than 500 impressions when you have 2,000. The scoring engine normalizes against your own historical 30-day average, which means a 'good' post is always defined relative to your current baseline.
The formula weights six signals: impressions (35%), reactions (20%), saves (20%), profile viewers driven by the post (15%), comments (5%), and new connections (5%). Saves are weighted heavily because they signal genuine intent — someone found it useful enough to want to come back to it.
The formula is unstable with fewer than 5 posts. I'm saying that plainly. With a small dataset, the 'baseline' is too noisy to normalize against. It gets more useful over time.
The Four AI Features
All four are powered by the Claude API with heavy prompt engineering. I spent more time on the prompts than on the API integration.
- Post body generation: takes a topic, your writing style (derived from past posts), and a target audience. Generates a full post that sounds like you, not like ChatGPT.
- Hook generation: produces four structurally different hooks — curiosity gap, contrarian take, story open, and direct insight. You pick the one that fits the idea.
- Per-post autopsy: given all the metrics for a post, it produces a structured verdict — what worked, what didn't, what to change next time. Stored in the DB and tied to the post.
- Cross-post growth insights: analyzes patterns across your last 30 posts and surfaces themes that correlate with high scores. Results are cached at the DB level so you're not calling the API on every page load.
Engineering Decisions
SQLite over Postgres. This is a single-user tool. There's no reason to stand up a Postgres instance, manage connections, or pay for a hosted database. SQLite runs in the same process, the file is portable, and it's fast enough that I've never hit a query over 20ms.
I put all modal state in a single modals.tsx file. Every modal in the app — and there are a lot — lives there. This is a deliberate violation of co-location orthodoxy. In practice, modals are referenced from many places and having them scattered made refactoring painful. One file, one truth.
In-memory rate limiting for the AI endpoints. 20 lines of code. Resets on server restart. Not suitable for production at scale — but this is a personal tool running locally. Redis would be 200 lines plus a container plus config. The 20-line version handles the one person using it.
The Honest Limitations
No auth. If you deploy this, anyone with the URL can use it. It's designed for local use or behind a private tunnel.
No tests. I should write some. I haven't.
The rate limiter resets on restart. If you restart the server, the window resets. Acceptable for personal use.
The scoring formula is unstable with fewer than 5 posts. Already said this. Saying it again.
I built this for myself. It works. It's the most complete thing I've shipped solo. That's enough.