Pulse is a page that's different every morning: tech, fintech, and business stories, researched, written, and published before I've had coffee, with a deliberate slice of that being African coverage most news apps skip. No one on my team writes those stories. Nothing gets rehosted from an RSS feed. A scheduled routine goes out, researches, and publishes — and I only touch it to read the result.
That last part is the whole engineering problem: how do you build a content pipeline where the "writer" is an AI agent running on a schedule, and the output has to be trustworthy enough that I'll build my own blog research on top of it? Here's exactly how it's built — stack, hosting, data model, and the ingestion pipeline that's the actual product.
The stack
Pulse is a Next.js 16 app on React 19, with Prisma over Postgres for storage, deployed on Railway. Nothing exotic — the interesting engineering isn't in the framework choice, it's in what happens before anything hits the database.
System Overview
The app itself is a fairly ordinary content site — article pages, category filters, a saved/bookmarks view that's entirely local (no accounts, nothing sent to the server for that), an RSS feed, a sitemap. The part worth explaining in detail is the ingestion side, because that's where the actual product lives.
The data model
Five tables, and each one earns its place:
Data Model
A few decisions here do real work:
sourceUrlis unique. That's the primary defense against duplicate stories — an article can't be ingested twice for the same source link, enforced at the database level, not just in application logic.regionisafricaorinternational, and it's a first-class column, not a tag. That's what makes it possible to guarantee African coverage shows up every day rather than however often it happens to surface.ReferenceLink.kind—primary,related,data,background— exists because a Pulse article is meant to be a jumping-off point, not the final word. Every story ships with the sources behind it, categorized by what kind of source they are, so a reader can go straight to the primary reporting if they want to go deeper.ArticleVoteis keyed on(articleId, clientId), a per-browser random ID generated client-side and never tied to an account — so likes and dislikes work without Pulse needing to know who anyone is.
The ingestion pipeline — the actual product
This is the part that makes Pulse a research assistant instead of an aggregator. Every morning, a scheduled Claude Code routine runs through a defined process:
Daily Ingestion Sequence
Two decisions in that flow are the whole reason this works:
1. African coverage doesn't come from the news APIs — because it can't. I checked this directly while building the pipeline: the Guardian's Africa tag returns general politics and sports, not tech or business; NewsAPI and Finnhub have essentially no African source coverage. So instead of hoping a general feed surfaces something, the routine goes straight to the outlets that actually cover this beat — TechCabal, Techpoint Africa, Disrupt Africa, IT News Africa, Rest of World's Africa desk — via targeted WebSearch, and treats finding 2-3 genuinely strong African stories as a non-negotiable part of every run, not a nice-to-have.
2. The summary is written, not copied. The routine reads the primary source and writes an original ~120-220 word summary — never paraphrased-to-the-point-of-plagiarism, never a re-hosted lede. That's the actual value proposition: Pulse isn't trying to be the news, it's trying to be a trustworthy starting point that hands you the sources to go verify and go deeper yourself.
The ingest API — the boundary that keeps this safe
Everything the scheduled routine produces goes through one authenticated endpoint:
POST /api/ingest/articles
Authorization: Bearer {INGEST_SECRET}
Content-Type: application/json
{ "articles": [ { slug, title, dek, body, category, region, sourceUrl, references: [...] } ] }
This is deliberately the only write path into the Article table from outside the app itself. A few things happen here that matter:
- Schema validation (Zod, matching
ingestArticleSchema) rejects malformed payloads before they ever touch the database — a badcategoryenum or a missingreferencesarray fails loudly instead of silently corrupting a row. - Server-side dedup on
sourceUrl— even if the calling routine forgets its own duplicate check, the database-level unique constraint plus an application-level guard means the same story can't land twice. - The secret is a bearer token, generated fresh per environment — not reused between local dev and production, so a leaked dev credential can't touch the live database.
A second endpoint, /api/cleanup/articles, runs after every ingestion pass and deletes anything older than 30 days. Pulse is deliberately not an archive — it's a rolling window of what's current, which keeps the database small and keeps every visit relevant instead of stale.
Architecture at a glance
Hosting and deploys
Railway, with Nixpacks auto-detecting the Next.js build. The one non-default piece: the start command is prisma migrate deploy && next start, so every deploy applies any pending database migrations automatically before the app boots — no manual SQL step, no drift between what's in prisma/migrations/ and what's actually running in production. migrate deploy only applies migrations it hasn't recorded as applied, so a deploy with no schema changes is a no-op.
Everything environment-specific — the database URL, the ingest secret, the app's public URL that feeds the sitemap and OG tags — comes from environment variables. Nothing is hardcoded, which is what makes it possible to run the exact same codebase locally against a dev database and in production against Railway's Postgres without touching a line of code between them.
Why this shape, not a simpler one
The obvious simpler version of this project is a cron job that hits an RSS feed and republishes it. I didn't build that, on purpose. An RSS republish is exactly the low-value aggregator category Pulse exists to not be — no original synthesis, no African-first editorial priority, no sourcing discipline. The extra engineering — the authenticated ingest boundary, the region-aware research step, the reference-link taxonomy — is what turns "a page that updates" into something I actually trust enough to use as my own research tool before I write.
That's the real test I built this against: would I, personally, rely on this every morning instead of doing the reading myself? Pulse is what it took to get to yes.



