Why We Switched from WordPress to Next.js for Every Client Site
After years of building WordPress sites, we moved every client build to Next.js. Here is why, what changed in performance and SEO, and what it means for trades businesses.
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.
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.
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:
Pre-rendered HTML means Google sees full page content immediately without executing JavaScript. This is the same architecture we use for every client site.
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.
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.
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.
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.
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.
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.
From Digitari Solutions
Want us to look at your site?
Send us the URL and we will tell you what is holding it back, free.
More from the blog
After years of building WordPress sites, we moved every client build to Next.js. Here is why, what changed in performance and SEO, and what it means for trades businesses.
Most roofing websites in Vancouver look the same and rank for nothing. Here is what separates a site that generates calls from one that just exists.