Digitari SolutionsDigitari°

Blog / Case Study

Case StudyFebruary 10, 20268 min read

Yesso Case Study: Building a Production-Ready SaaS in 6 Weeks on Next.js 15

How we built Yesso, an email-based approval workflow platform, from zero to production in 6 weeks with sub-100ms load times and 90+ Lighthouse scores.

Yesso is an email-based approval workflow tool that lets teams approve requests — PTO, expenses, invoices — without anyone needing to log into another platform. Approvers get an email and click approve or deny. That is it.

The concept was simple. Building it to production standards in six weeks was the actual challenge. Here is what we built, what broke along the way, and what we learned.

The stack

Next.js 15 with the App Router, TypeScript, Tailwind CSS, Prisma ORM, Supabase PostgreSQL, Clerk for authentication, Resend for transactional email, and Vercel for deployment.

Each was chosen for a specific reason, and several of them saved us from significant production failures.

Performance targets and what we hit

The goal was a waitlist site that loads in under 100ms and scores 90+ on Lighthouse before we wrote a single line of product code. SEO and performance are exponentially harder to retrofit than to build in from the start.

Static site generation on all public pages (export const dynamic = 'force-static') deployed to Vercel's edge network delivered:

  • Time to First Byte: consistently under 100ms
  • First Contentful Paint: under 1 second
  • Largest Contentful Paint: under 1.5 seconds
  • Lighthouse score: 90+
  • SEO score: 95+

Pre-rendered HTML means Google sees full page content immediately without executing JavaScript. This is the same architecture we use for every client site.

TypeScript caught four production failures before they happened

Next.js 15 introduced a breaking change to route handler parameters. Params changed from synchronous objects to Promises, which means the old destructuring pattern silently fails.

// Old pattern — fails silently in Next.js 15
export async function POST({ params: { id } }) { ... }

// Correct pattern — TypeScript caught this immediately
export async function POST({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
}

TypeScript caught all four affected route handlers at compile time. Without it, these would have been silent failures in production that would have been hard to trace.

The lazy initialization pattern

On Vercel, environment variables are not available during the build phase, only at runtime. Any client initialized at module level using an environment variable will cause your build to fail.

The naive approach breaks:

// This fails on Vercel — env vars not available at build time
const resend = new Resend(process.env.RESEND_API_KEY);

The correct pattern defers initialization to request time:

function getResendClient() {
  const apiKey = process.env.RESEND_API_KEY;
  if (!apiKey) throw new Error('Missing RESEND_API_KEY');
  return new Resend(apiKey);
}

This applies to Prisma, Resend, Supabase, and any other client that reads environment variables on initialization. It is a serverless-specific pattern that is not obvious until you hit the error. The full technical breakdown covers how this architecture carries over to client sites.

Multi-tenant from day one

Every database table includes an organizationId foreign key with a proper index. This was the first migration, before any feature tables were added.

The benefit is that data isolation between organizations is architecturally enforced rather than application-enforced. It is impossible to accidentally leak data between organizations because query patterns require filtering by organizationId at the ORM level. It also means the codebase scales to any number of organizations without structural changes.

Building multi-tenant in after the fact is one of the most painful refactors a SaaS can go through. Doing it first costs almost nothing.

Magic links as a product differentiator

Clerk handles authentication, but the meaningful innovation was using magic link authentication for approvers. Managers who need to approve requests do not create accounts. They get an email, click approve or deny, and it is done in under 30 seconds with zero login friction.

This became Yesso's core differentiator. Approval workflow tools often fail in practice because the friction of logging in creates a bottleneck. Remove the login requirement and approvals actually happen.

The Prisma connection pooling mistake

Supabase PostgreSQL has two connection strings: a direct connection on port 5432 and a pooled connection via pgbouncer on port 6543. On serverless platforms, you must use the pooled connection.

Each serverless function invocation opens a database connection. Without connection pooling, a burst of 50 concurrent requests opens 50 simultaneous database connections and immediately exhausts the limit. Using pgbouncer means those 50 requests share a managed pool.

This mistake is in the Supabase documentation but easy to miss. We caught it before production under load testing.

What Yesso taught us about building fast

Six weeks from zero to production is achievable when the architecture decisions are right. Static generation for public pages, lazy initialization for all clients, TypeScript everywhere, multi-tenant data model from the first migration, and Vercel for deployment.

The framework does the hard work when you build with it rather than against it. The patterns are learnable in hours. The production stability they provide is worth many days of debugging.

Yesso is at getyesso.com. The same technical foundation powers every Digitari client site.

#casestudy#next.js#saas#technical

Want us to look at your site?

Send us the URL and we will tell you what is holding it back, free.

Get a Free AuditGet an Estimate