NewDayKnowledge

How this AI news platform was built, and how it works

Architecture

The big picture

This project is two separate programs that share one database. That split is the single most important thing to understand about this project, because it explains almost every other decision.

YOUR MAC (always-on while you use it)              THE INTERNET
+-------------------------------+                   +------------------------------+
|  Local News Agent             |                    |  Vercel (dashboard hosting)  |
|  (agents/news/run.ts)         |                    |                              |
|                                |                    |  Next.js app:                |
|  1. Fetch RSS feeds           |                    |  - Login page                |
|  2. De-duplicate              |                    |  - Dashboard pages           |
|  3. Ask Ollama to classify    |----writes to------->|  - API routes                |
|  4. Rank by score             |    the SAME        |                              |
|  5. Ask Ollama to write posts |    database        |  Reads the same data the     |
|  6. Save results              |                    |  agent wrote, and lets you   |
|                                |                    |  approve/reject/publish it   |
|  Triggered daily by macOS launchd |                |                              |
+-------------------------------+                    |  Runs 24/7, reachable from   |
              |                                       |  any browser, anywhere       |
              |                                       +------------------------------+
              |                                                      |
              |                     +------------------+             |
              +-------------------->|  Neon Postgres   |<------------+
                                     |  (shared database) |
                                     +------------------+

Why split it this way at all

The honest answer is a hard constraint, not because this is the ideal design in a vacuum. You wanted the AI model, Ollama, to run for free on your own Mac instead of paying for a cloud AI API.

Vercel, where the dashboard is hosted, cannot run Ollama. Vercel only runs short-lived serverless functions and a web app; it cannot run a large, always-loaded language model, and it cannot reach localhost on your Mac from the internet.

So the only way to get free local AI and a dashboard reachable from anywhere in the same project is to split them into two programs that do not talk to each other directly, and instead both talk to a third thing they can both reach: a cloud database, Neon.

The agent writes; the dashboard reads and writes back small things like approvals. Neither program needs to know the other exists.

If you later add a paid AI provider, OpenAI is already built in as an option, you could move the agent itself onto Vercel too, using Vercel Cron instead of launchd, and simplify this into one program. That trade-off, free-but-requires-your-computer versus paid-but-fully-cloud, is worth understanding because it is the kind of decision that shows up constantly in real system design.

The pieces, one at a time

1. The local news agent

The local news agent in agents/news/ is a plain TypeScript script, not a web server. You, or launchd, run it, it does its work, and it exits. It has five stages, each in its own file:

  • fetchFeeds.ts fetches and parses RSS feeds using the rss-parser package.
  • dedupe.ts removes near-duplicate stories using simple text similarity, a fast, free, good-enough heuristic rather than AI.
  • classify.ts sends each article to the AI model, Ollama or OpenAI, asking it to return structured JSON: category, companies, keywords, an importance score, and sentiment.
  • rank.ts is pure logic, no AI. It filters by your configured topics and minimum score, and keeps only the best few.
  • generateContent.ts asks the AI model to write an actual Twitter/X post and LinkedIn post for each surviving article.

run.ts is the conductor: it calls each stage in order, saves the results to Postgres via Prisma, and records a NewsRunLog row so the dashboard can show you what happened on each run.

2. The dashboard

A normal Next.js web app lives in app/. Two kinds of routes live here:

  • Pages in app/dashboard/... are what you actually see in the browser.
  • API routes in app/api/... are small backend endpoints the pages call to read or write data. Every one of these checks that you are logged in before doing anything, through lib/auth.ts.

The dashboard never calls Ollama or OpenAI directly. It only ever reads and writes Postgres. This is why editing a prompt or a topic list in the dashboard does not do anything immediately. It changes a row in the database, and the next time the local agent runs, it reads that row and uses the new value.

3. The shared database

Prisma, in prisma/schema.prisma, defines the shape of the data once, in one file, and both programs use the same generated Prisma Client to talk to it.

Neon is just Postgres, but hosted and reachable from both your Mac and Vercel servers. Functionally it is the same as running your own Postgres server, except you do not have to keep a server running yourself.

One quirk worth understanding: Neon's free tier suspends the database after a few minutes of no activity to save cost, and takes a moment to wake back up on the next query. Because the agent spends several minutes talking only to the AI model before it needs to save results, this occasionally caused the save step to fail with a database connection error.

The fix, lib/db.ts's withDbRetry function, retries a failed database call a few times with a short pause. It is simple, but it is the correct fix for this exact class of problem with any serverless database.

4. Social publishing

Social publishing lives in lib/social/: two small, independent modules, one per platform, that know how to actually post to X/Twitter and LinkedIn, plus a thin publish.ts that picks the right one.

They are only ever called from one place: the publish API route, which only allows publishing content that a human has already marked APPROVED. Nothing in this system posts automatically.

Deployment pipeline

There is no separate CI/CD pipeline in the traditional sense. Instead:

  1. You run git push to GitHub.
  2. You run vercel deploy --prod from the command line, which builds the app on Vercel's servers and points the production URL at the new build.
  3. Vercel is also connected to the GitHub repo, so pushing to main alone is enough to trigger an automatic deploy too, going forward.

Database schema changes are a separate, manual step: edit prisma/schema.prisma, then run npx prisma migrate dev to generate and apply a migration.

This is deliberately not automatic on every deploy, since running unreviewed schema changes automatically against a production database is a common source of real outages.