Case study
Sniffo.ai
Full-stack SaaS that turns Reddit threads, competitor pages, and market signals into scored, actionable opportunities.
Market Intelligence SaaS · May 2026
Visit live siteOverview
Sniffo.ai is a founder-focused market intelligence product: it collects signals from Reddit and tracked competitor pages, normalizes them into one shape, ranks them against a workspace business profile with an explainable score, and supports a tight product loop — find, score, act, track — including AI reply drafts, statuses, and notes.
The hard part was never “adding AI.” It was making outputs grounded in sources, explainable when users disagree, and wired into a workflow they can run daily. I built it end to end: Next.js / App Router surface, PostgreSQL + Prisma data model, ingestion and scoring pipelines, Stripe-backed SaaS billing, and OpenAI-backed drafts with guardrails.
The challenge
Founders drown in unstructured signal. Reddit threads, competitor landing pages, pricing shifts, and feature announcements are the raw material for product decisions — but reading everything manually does not scale, and feeding the same noise into a generic LLM produces confident, ungrounded summaries.
Sniffo had to do three things at once: collect signals reliably, normalize them so downstream logic is not a pile of one-off parsers, and turn them into ranked opportunities where every score is defensible with persisted inputs — not a black box users cannot argue with.
Everything in the codebase exists to keep that loop trustworthy: partial failure during ingestion, decoupled mapping from fetch, versioned scoring, billing that cannot be bypassed from the client, and UI that surfaces explanations without overwhelming a dense feed.
For how I narrow scope and risks before a build like this, see Technical Discovery for Web Apps.
Choosing the stack
I optimized for one deployable on Render with clear module boundaries — ship and iterate, not a distributed topology I had not validated yet.
UI: Next.js 16 App Router, React 19, Tailwind v4, Radix. API: Next.js route handlers. Data: PostgreSQL + Prisma. Auth: JWT via jose, httpOnly cookies, middleware-gated routes. Billing: Stripe Checkout, Customer Portal, and webhooks as the subscription source of truth. Ingestion: Reddit fetchers plus HTML scrape, normalize, map, persist — with Bright Data Web Unlocker when direct fetch hits bot interstitials. AI: OpenAI chat completions with a DB-configurable model. Caching: Next unstable_cache with tag invalidation on ingestion completion.
That maps to folders like lib/ingestion, lib/scoring, lib/billing, and lib/ai — no microservices, no separate worker fleet for v1; scheduled runs use a cron-authenticated trigger instead.
I also wrote about the most expensive mistake in software projects (and why teams miss it), which is especially relevant when the “real product” is workflow and trust, not a thin LLM wrapper.
The architecture
A single Next.js deployment fronts marketing (including an MDX blog), the paywalled dashboard, and /api route handlers. Prisma owns persistence; ingestion and scoring run in-process on the same runtime as the app, with Stripe webhooks reconciling subscription state into local rows.
The schema (Business, Competitor, RedditSource, SourceItem, Opportunity, AIReply, SyncLog, Subscription, SystemSetting, BusinessProfile) is the fastest mental model of the system — if you read one artifact end to end, read that one.
More on how I ship production web apps with this stack: full-stack web development (APIs, data layer, deployment patterns).
What I built
Across ingestion, intelligence, workflow, and SaaS operations, I built:
- Workspace-scoped ingestion runs with SyncLog progress (fetching → scoring → done) and OK / PARTIAL / FAILED completion semantics.
- Reddit and competitor fetchers, HTML normalization, Bright Data fallback, Cheerio field extraction, and SourceItem → Opportunity mapping decoupled from network code.
- Explainable scoring engine with named rules, scoreFactors, SCORING_VERSION on scored rows, subreddit quality priors, and generated explanations.
- Dashboard UX: dense opportunity feed, composite score plus one-line triage reason, on-demand breakdown panel with signed factors.
- OpenAI reply drafts with timeouts, persisted model / promptVersion / tokensUsed on AIReply rows, and admin-configurable model selection.
- Stripe subscriptions, webhook reconciliation, hard paywall in middleware, and multi-dimensional plan quotas (including new sources per month).
- Encrypted SystemSetting storage for vendor keys with admin editing; env-based bootstrap fallback.
- unstable_cache for hot counts with workspace tags, bounded cache registry, and invalidation after ingestion.
- Marketing site plus MDX blog alongside the product surface.
Together, these pieces form one product surface: monitored sources, scored opportunities with evidence, drafts and workflow state, and SaaS limits that match real cost drivers — not a thin wrapper around a chat box.
Performance & engineering decisions
Ingestion is where production systems usually break first, so partial failure is a first-class outcome. A workspace-scoped run records progress on a SyncLog (fetching → scoring → done) and can finish OK, PARTIAL, or FAILED. If one of five competitor sites 403s, the successful sources still persist, the failure is attributed per source in the progress payload, and the next scheduled run retries the straggler — scheduled ingestion stays usable because “partial success” is normal, not a panic path.
Fetchers write SourceItem rows; a separate mapper decides what becomes an Opportunity and applies the business profile. That split lets me re-score history without re-fetching and change scoring without touching network code. Direct fetch with bot-aware headers covers most sites; when Cloudflare-style interstitials appear, the pipeline falls back to Bright Data, then Cheerio extracts title, headings, paragraphs, feature blocks, and pricing snippets so scoring always sees a normalized shape.
Scoring stays explainable: named rules (keywords vs profile, pain-language heuristics, recency decay, competitor boosts, spam penalties, Reddit engagement) each contribute labeled factors in scoreFactors, with a generated explanation string. SCORING_VERSION is stored on every scored row so rule changes never silently rewrite history. Subreddit quality priors down-rank default-sub noise relative to niche communities — deliberately hand-tuned rather than learned while the dataset is still small.
The dashboard collapses explanations for scan speed: list rows show the composite score plus a one-line “why” from the strongest positive factor; a breakdown panel on interaction lists every contributor with sign and weight. Negative factors are visually distinct from positive ones so users do not flatten “spam penalty” and “keyword match” into the same mental bucket. The score panel hydrates on demand because the feed can render many rows at once.
Billing is modeled as Stripe as source of truth and the local Subscription row as cache: webhooks handle customer.subscription.updated, invoice.payment_failed, and friends; route handlers read the DB and avoid synchronous Stripe on hot paths, with a reconciliation pass on dashboard load if a webhook was missed. Middleware enforces a hard paywall — no active subscription, no dashboard — which removes a whole class of client-side bypass bugs. Plans use multi-dimensional quotas (competitors, Reddit sources, manual refresh caps, AI replies per month, and new sources added per month) so “add many sources, scrape, delete, repeat” cannot game a simple active-source cap alone.
Vendor secrets (OpenAI, Bright Data, etc.) can live encrypted in SystemSetting and be rotated through admin tooling instead of redeploys, with env vars as bootstrap fallback. Opportunity counts per workspace are wrapped in unstable_cache with workspace-scoped tags and a bounded registry so a long-lived Node process does not grow cache entries without limit. OpenAI calls use abort timeouts; each AIReply stores model, promptVersion, and tokensUsed for cost accounting and prompt regression audits.
If I were starting over with the same goals but more scale pressure, I would pull ingestion out of the web process first — in-process plus cron is fine until user-triggered runs or multi-minute jobs contend with request threads. I would also add an embedding-based factor to scoreFactors rather than replace rules, preserving explainability while improving recall on semantic similarity to opportunities the user already acted on.
If you are hiring for a similar SaaS surface area, my SaaS development offering covers multi-tenant products, billing-adjacent work, and long-term maintainability.
My role
I built Sniffo end to end: product and UX for the dashboard loop, full-stack web implementation on Next.js, data model and migrations with Prisma, ingestion and scoring engines, Stripe billing and quota enforcement, AI reply generation with versioning, admin settings, deployment on Render (including cron-authenticated ingestion), and operational docs so someone else could run the system without archaeology.
Outcome
Sniffo ships as a live SaaS product with a defensible scoring story, a paywall and quotas aligned to real scrape and AI costs, and documentation for Render, cron secrets, and environment setup.
Add the screenshot files below to public/images/projects/sniffo.ai/case-study/ (filenames must match) so these figures resolve.
Browse the rest of the long-form writeups on the case studies hub.
Tech stack
- Next.js 16 (App Router)
- React 19
- Tailwind CSS v4
- Radix UI
- PostgreSQL
- Prisma
- JWT (jose) + httpOnly cookies + middleware
- Stripe (Checkout, Customer Portal, webhooks)
- OpenAI (chat completions, configurable model)
- Bright Data Web Unlocker (scrape fallback)
- Cheerio (HTML extraction)
- Next.js unstable_cache (tag invalidation)
- Render (hosting + cron-triggered ingestion)
More case studies
The Migration Finished. The Dashboard Still Said Zero.
Re-Architecting Data Integrity
Fighting Online Antisemitism · May 2026
Read case studyRaith Rovers Scouting Platform
Angular & Node.js Case Study
Raith Rovers Football Club · June 2025
Read case studyCollective Memory
React Native, Node.js & AI Case Study
Social content platform · October 2024
Read case study