# Venture Den

> Where founders pitch ideas, get an AI score, and get upvoted by the community

A platform where founders pitch startup ideas, Gemini scores each pitch on three criteria, and the community upvotes the best ones.

- Type: SaaS App
- Role: Full-stack Developer
- Tech: nextjs, react, typescript, sanity, groq, tailwindcss, motion, react_query, zod, turborepo, pnpm, markdown
- Published: 2025-01-01
- Updated: 2026-10-04
- Live: https://venture-den.aayushbharti.in
- Repo: https://github.com/AayushBharti/VentureDen
- URL: https://aayushbharti.in/projects/venture-den

**TL;DR:** VentureDen is a startup pitch platform where founders sign in with GitHub, submit a pitch, and get it scored by Gemini 2.5 Flash. It is a pnpm and Turborepo monorepo with a Next.js 16 frontend and a Sanity Studio that share generated TypeScript types. The hard part was making model output dependable, using a response schema, Zod validation, a 10-second timeout, and a pitch that still publishes when the analysis fails.

## Highlights

- Written AI feedback and suggestions per pitch
- Structured AI output validated before saving
- Sanity visual editing with live preview
- Fuzzy search across pitches and posts

---
## Next.js 16 with auth, user content and a CMS

I wanted to know how Next.js 16, React 19, the React Compiler and Turbopack behave together once authentication, user-generated content and a CMS are all in the same app. Release notes don't answer that. A toy demo wouldn't either, so I needed a product with all three.

A startup pitch platform had the right shape. Founders submit pitches, other people upvote and comment, and every pitch gets scored by an LLM. That last part gave me a reason to build a structured model pipeline next to a CMS-managed marketing site, inside one monorepo.

---

![VentureDen homepage hero with the headline 'Where Great Startups Get Discovered', Submit Your Pitch and Explore Pitches buttons, and a logo row underneath](/projects/venture-den/screen1.webp)

---

The repo is a pnpm and Turborepo monorepo. `apps/web` is the Next.js frontend and `apps/studio` is the Sanity Studio. Everything they share lives in `packages/`:

```mermaid
---
config:
  treeView:
    showIcons: true
---
treeView-beta
    apps/
        web/ ## Next.js 16 frontend (App Router, React Compiler)
        studio/ ## Sanity Studio and schema types
    packages/
        sanity/ :::highlight ## clients, 38 GROQ queries, generated types, live API
        env/ ## validated environment variables
        ui/ ## shared component kit
        logger/ ## structured logger
        typescript-config/ ## shared tsconfig
    turbo.json ## lint, format, check-types, build
```

A new pitch takes this path. A founder signs in with GitHub OAuth (NextAuth v5); the first sign-in creates their `author` document, which becomes their public profile. They write the pitch in a multi-step form with a Tiptap/Novel rich-text editor. The `createPitch` server action converts the editor's HTML to Markdown with Turndown, validates the fields with Zod, uploads the cover image to Sanity, creates the `startup` document, and then asks Gemini for an analysis.

The marketing surface (homepage hero, logo ticker, top pitches, integrations, FAQ) is a typed Sanity page builder with click-to-edit visual editing and live preview, wired through GROQ and Sanity's live content API. Browsing adds category filters, sorting by recent, upvotes or views, and client-side fuzzy search with Fuse.js over title, category, author name and description.

---

![VentureDen 'Discover Startup Pitches' page with category filter chips, Recent / Most Upvoted / Most Viewed tabs, a search box, and pitch cards showing upvote and view counts](/projects/venture-den/screen2.webp)

---

Instead of dumping the model's prose on the screen, I treated Gemini as a data source with a contract.

The call to `gemini-2.5-flash` sets `responseMimeType: "application/json"` and a `responseSchema` with four required keys: `clarity`, `marketPositioning` and `uniqueness` (each an integer score plus feedback text) and `suggestions`. The system prompt holds the rubric: each dimension is scored 0 to 100, the meaning of 80+ and below 40 is spelled out per dimension, and the model must return two or three suggestions that point at specific parts of the pitch.

A response schema tells the model what to return; it doesn't promise it will. So the parsed JSON goes through a stricter Zod schema: scores must be integers from 0 to 100, feedback 1 to 1,000 characters, each suggestion at most 800 characters, and two or three suggestions in total. Only after that does my code compute the overall score:

```ts
const overallScore = Math.round(
  clarity.score * 0.35 + marketPositioning.score * 0.35 + uniqueness.score * 0.3
);
```

Doing the weighting in code means the model can't get the arithmetic wrong, and changing the weights never touches the prompt.

Two limits keep the call bounded. The pitch body is cut to 5,000 characters before it goes into the prompt, and the request has a 10-second timeout. Any failure (timeout, invalid JSON, a Zod rejection) is logged and returns `null`.

That `null` sets the order of operations. The pitch document is created first, and the analysis is patched onto it as `aiAnalysis` only if one came back. If Gemini is down, the founder loses the score panel and the pitch still publishes. On the detail page each dimension renders as its own scored panel, so a redesign only touches components that read typed fields; no code parses paragraphs.

```mermaid
sequenceDiagram
    actor F as Founder
    participant V as VentureDen
    participant G as Gemini 2.5 Flash
    F->>V: Submit a pitch
    Note over V: Create the pitch document
    V->>G: First 5,000 chars + responseSchema, 10s timeout
    alt Valid JSON that passes Zod
        G-->>V: Three scored dimensions + suggestions
        Note over V: Weight the overall score in code
        Note over V: Patch aiAnalysis onto the pitch
    else Timeout, invalid JSON or Zod rejection
        Note over V: Log it and return null
    end
    V-->>F: The pitch publishes either way
```

---

![VentureDen pitch detail page for 'DeployBot' with a collapsed AI Pitch Analysis panel showing a score of 92, an Upvote button, and an 'In this Pitch' table of contents](/projects/venture-den/screen3.webp)

---

## A 2,857-line type file shared by two apps

The Studio owns the schema and the frontend owns the queries. Running `sanity typegen generate` in the Studio writes `packages/sanity/src/sanity.types.ts`, currently 2,857 lines. The 38 GROQ queries in `packages/sanity/src/query.ts` are declared with `defineQuery`, so each query's result type comes from the schema. The web app imports both from the shared `sanity` package.

In practice: rename a field in the Studio, regenerate, and every query and component that reads it fails `check-types` in the web app until it's fixed. Turborepo runs lint, format and `check-types` across both apps and caches the results, so an unchanged package doesn't get checked twice.

Counters live on the same documents. A pitch view is counted by a server component that calls `writeClient.patch(id).inc({ views: 1 })` inside Next's `after()`, so the increment runs after the response and never blocks render. Upvotes use the same atomic `inc` from a POST route at `/api/startups/[id]/upvote`. Because content is fetched through Sanity's live API, the feed and homepage pick up newly published content without a redeploy.

---

## Trade-offs

| Chose | Over | Why | Cost |
|---|---|---|---|
| Sanity with GROQ | A custom Prisma and Postgres layer | Editor-managed content, a visual page builder in the Studio, live queries that publish without a redeploy | Upvotes, views and comments are document writes through the Sanity write client, not database rows |
| Gemini `responseSchema` plus Zod | Free-form model text | Typed fields render as panels and can be rejected when malformed | Two schemas describe the same shape and have to be kept in sync by hand |
| Running the analysis inside `createPitch` | Running it after the response | The score already exists when the founder opens their pitch | Submitting waits on Gemini, up to the 10-second timeout |
| pnpm and Turborepo monorepo | Separate repos for site and Studio | Generated Sanity types flow straight into the frontend | Upfront wiring of Turborepo and the five shared packages |
| Client-side Fuse.js search | A search endpoint | Results update on every keystroke with no request | It only searches the startups already passed to the page |
| Next.js 16, React 19 and the React Compiler | Older, better-documented versions | Learning how they behave together was the point | Sparse docs, edge cases solved by reading source and GitHub discussions, and auth on `next-auth` 5.0.0-beta.30 |

---

## Results

- VentureDen is live at [venture-den.aayushbharti.in](https://venture-den.aayushbharti.in), with the source on [GitHub](https://github.com/AayushBharti/VentureDen).
- Every submitted pitch makes one Gemini call with a 10-second ceiling, and the page receives either a Zod-validated analysis or none.
- The 38 GROQ queries are typed against a 2,857-line generated file, so Studio schema changes surface as type errors in the frontend.
- The web app builds with `reactCompiler: true` and the dev server runs on Turbopack.

---

## What I'd change

The upvote route is the weakest part of the app. It increments the count by one with no session check and no record of who voted, so anyone can POST to it repeatedly and inflate a pitch. I'd require sign-in and store one vote per user per pitch, counting from those records instead of a bare counter.

I'd also take Gemini out of the submit path. Views already use `after()` to do work once the response is sent; the analysis could do the same, with the detail page showing a pending state until `aiAnalysis` lands. Founders would stop waiting on the model after they press submit.

---

## Frequently asked questions

### What is VentureDen?

VentureDen is a startup pitch platform where founders sign in with GitHub, submit pitches, and get each one scored by Google Gemini on clarity, market positioning and uniqueness within a 10-second timeout. Other visitors discover pitches through upvotes, comments, views and fuzzy search.

### How does VentureDen score startup pitches with AI?

VentureDen sends each submitted pitch to Google Gemini, which returns a 0 to 100 score for clarity, market positioning and uniqueness plus two or three suggestions. The response is validated with Zod before it reaches the UI, and the code computes the overall score with weights of 35%, 35% and 30%, so malformed model output can't break the page.

### What stack is VentureDen built with?

VentureDen is built with Next.js 16, React 19, the React Compiler and Turbopack, using Sanity CMS with GROQ for content, NextAuth v5 for GitHub OAuth and Fuse.js for fuzzy search. The frontend and Sanity Studio live in a pnpm and Turborepo monorepo.

### How do founders submit a pitch on VentureDen?

Founders sign in to VentureDen with GitHub OAuth and submit a pitch through a multi-step form with a Tiptap/Novel rich-text editor. Each founder gets a public profile generated automatically on first sign-in.

### Why does VentureDen use Sanity instead of a database like Postgres?

For content-heavy, editor-managed data, Sanity beat a custom Prisma and Postgres layer: GROQ is expressive, the Studio has a visual page builder, and live queries update the feed and homepage the moment content is published, with no redeploy.

### How does VentureDen keep frontend types in sync with the Sanity schema?

VentureDen's Sanity Studio schema generates TypeScript types into a shared package that the Next.js frontend consumes, so a schema change is type-checked end to end. Turborepo caches lint, typecheck and build across both apps.
