Skip to content

Venture Den

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

Built

Q1 2025

Updated

Q4 2026

Source

GitHub

Tech Stack

Next.jsReactTypeScriptSanity CMSGROQTailwind CSSMotion.devTanStack QueryZodTurborepopnpmMarkdown

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.

01

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.

02

Two apps and five packages in one workspace

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/:

appswebNext.js 16 frontend (App Router, ReactCompiler)studioSanity Studio and schema typespackagessanityclients, 38 GROQ queries, generated types,live APIenvvalidated environment variablesuishared component kitloggerstructured loggertypescript-configshared tsconfigturbo.jsonlint, format, check-types, build
appswebNext.js 16 frontend (App Router, React Compiler)studioSanity Studio and schema typespackagessanityclients, 38 GROQ queries, generated types, live APIenvvalidated environment variablesuishared component kitloggerstructured loggertypescript-configshared tsconfigturbo.jsonlint, 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.

03

Gemini as a typed API with a 10-second budget

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:

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.

[Timeout, invalid JSON orZod rejection]alt[Valid JSON thatpasses Zod]Submit a pitchFirst 5,000chars +responseSchema,10s timeoutThree scoreddimensions +suggestionsThe pitchpublisheseither wayCreate thepitch documentWeight theoverall score incodePatchaiAnalysis ontothe pitchLog it andreturn nullFounderVentureDenGemini 2.5FlashFounderVentureDenGemini 2.5Flash
[Timeout, invalid JSON or Zod rejection]alt[Valid JSON that passes Zod]Submit a pitchFirst 5,000 chars + responseSchema, 10s timeoutThree scored dimensions + suggestionsThe pitch publishes either wayCreate the pitch documentWeight the overall score in codePatch aiAnalysis onto the pitchLog it and return nullFounderVentureDenGemini 2.5 FlashFounderVentureDenGemini 2.5 Flash

04

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.

05

Trade-offs

ChoseOverWhyCost
Sanity with GROQA custom Prisma and Postgres layerEditor-managed content, a visual page builder in the Studio, live queries that publish without a redeployUpvotes, views and comments are document writes through the Sanity write client, not database rows
Gemini responseSchema plus ZodFree-form model textTyped fields render as panels and can be rejected when malformedTwo schemas describe the same shape and have to be kept in sync by hand
Running the analysis inside createPitchRunning it after the responseThe score already exists when the founder opens their pitchSubmitting waits on Gemini, up to the 10-second timeout
pnpm and Turborepo monorepoSeparate repos for site and StudioGenerated Sanity types flow straight into the frontendUpfront wiring of Turborepo and the five shared packages
Client-side Fuse.js searchA search endpointResults update on every keystroke with no requestIt only searches the startups already passed to the page
Next.js 16, React 19 and the React CompilerOlder, better-documented versionsLearning how they behave together was the pointSparse docs, edge cases solved by reading source and GitHub discussions, and auth on next-auth 5.0.0-beta.30

06

Results

  • VentureDen is live at venture-den.aayushbharti.in (opens in new tab), with the source on GitHub (opens in new tab).
  • 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.

07

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.

08

FAQ

Q.01What 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.

Q.02How 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.

Q.03What 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.

Q.04How 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.

Q.05Why 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.

Q.06How 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.