How I plan and ship web apps for startups: discovery to launch
By Nayan Kyada · · 5 min read
Part of Next.js Performance
Building web apps for startups is less about raw technical skill and more about preventing scope from quietly expanding until the budget is gone. Most projects fail in the planning phase, not the build phase. Here is the exact process I follow, from first call to handoff, with the artifacts I produce at each stage.
What discovery actually produces
I run a paid discovery session before any code is written — usually two to four hours across two calls. The output is a two-page brief and a milestone plan. Not a 40-page requirements document. Not a Figma prototype. Two pages.
The brief covers four things:
- The one-sentence product — what it does and who it is for, agreed in writing so we have something to return to when scope creeps.
- The primary user action — the single thing the user must be able to do on day one. For a booking app it is "complete a booking". For a dashboard it is "see the metric that prompted the build".
- Explicit out-of-scope list — features that came up in conversation but are deferred. This list is as important as the in-scope list.
- Constraints — budget ceiling, deploy date, auth provider already chosen, existing API contracts, team members who will own content after handoff.
A real brief excerpt looks like this:
// docs/brief.ts — not runtime code, just a typed record I keep in the repo
export const projectBrief = {
product: "Subscription analytics dashboard for SaaS founders",
primaryAction: "View MRR, churn, and LTV in a single authenticated view",
outOfScope: [
"CSV export",
"Slack notifications",
"Multi-workspace support",
],
constraints: {
budgetCeiling: 18000,
launchDate: "2026-10-15",
authProvider: "Clerk",
existingAPIs: ["Stripe Billing API"],
},
} as const;Keeping the brief as a committed file means every PR exists in a repo that also contains the product definition. New contributors read the README, not Notion.
Thin-slice milestone planning
I split the project into three to five milestones, each ending in something you can open in a browser and click. Not "backend complete" — that is not a milestone, that is a phase. A milestone is a deployed, reviewable slice of the real product.
For a typical startup web app I use this structure:
| Milestone | What gets deployed | Typical duration |
|---|---|---|
| M1: Skeleton | Auth, navigation shell, one real data read | 1 week |
| M2: Core loop | Primary user action works end to end | 2 weeks |
| M3: Content & polish | CMS wired, metadata, error states, empty states | 1 week |
| M4: Performance & SEO | Core Web Vitals pass, JSON-LD, sitemap, image optimisation | 1 week |
| M5: Launch | Final QA, staging sign-off, production deploy | 3 days |
Total: five to six weeks for a focused MVP. Projects that skip M1 and M2 and jump straight to M3 polish are the ones that slip by three months.
The rule I hold to: every milestone ends with a staging URL sent to the client and a written sign-off before the next milestone begins. No sign-off, no next milestone. This sounds bureaucratic. It has saved me from scope arguments on every single project.
Staging from day one
I push to a staging environment on day one of the build, not the week before launch. For Next.js projects this means a Vercel preview deployment tied to the staging branch.
The environment variable pattern I use:
// lib/env.ts
import { z } from "zod";
const envSchema = z.object({
NEXT_PUBLIC_APP_ENV: z.enum(["development", "staging", "production"]),
DATABASE_URL: z.string().url(),
SANITY_PROJECT_ID: z.string(),
SANITY_API_VERSION: z.string().default("2026-01-01"),
});
export const env = envSchema.parse(process.env);Sanity dataset is staging in staging and production in production — they are separate datasets from day one. I have cleaned up too many projects where staging editors polluted production content because the datasets were shared. Separate datasets cost nothing in Sanity and save real pain.
The staging URL goes in the brief document. Clients bookmark it. They test on it. By launch day they have already clicked through the app fifty times and there are no surprises.
The launch checklist I actually use
I keep this as a GitHub issue template so it auto-populates on every project:
-
NEXT_PUBLIC_APP_ENV=productionconfirmed in Vercel dashboard - Sanity dataset switched to
productionand seed content checked -
next/imagedomains andremotePatternslocked to production origins only -
robots.txtallows crawl (staging hasDisallow: /) -
sitemap.xmlgenerated and returning 200 with correct URLs - JSON-LD structured data validated in Google Rich Results Test
- Core Web Vitals: LCP under 2.5 s, CLS under 0.1, INP under 200 ms on mobile 4G sim
- Error boundaries on every async data boundary
- Auth redirect loop tested on incognito — protected routes redirect, public routes do not
- Vercel Analytics enabled and reporting
- Custom domain with HTTPS confirmed — no mixed content warnings
- Client handed Sanity Studio URL, editor login, and a two-minute Loom walkthrough
The Loom walkthrough is not optional. Written docs for Sanity Studio do not get read. A two-minute screen recording of "here is how you add a blog post, here is how you update the homepage hero" gets watched and re-watched.
Why thin slices beat sprints for startup web apps
Sprint planning assumes a stable backlog. Startups do not have stable backlogs — founders change their minds, investors give feedback, a competitor ships something. Thin-slice milestones give you natural renegotiation points without blowing up the contract. At the end of M2 you have something working. If the client wants to pivot, you pivot from a working base, not from a half-finished sprint.
The brief, the milestone plan, and the staging environment are not overhead. They are the actual product of the first week. Everything else is implementation.