Back to Insights/Engineering

How We Scaled Recuvix using Next.js 15, Prisma, and Tailwind CSS

NovaEdge Logo

Amit Kumar Raikwar

Founder & Lead Engineer•Portfolio ↗
October 7, 20266 min read
How We Scaled Recuvix using Next.js 15, Prisma, and Tailwind CSS

Recuvix's React SPA and Express backend hit PostgreSQL connection saturation and slow editor startup. Moving to Next.js 15 and Prisma cut initial JavaScript by 45% and brought global TTFB below 150ms.

Recuvix's scaling problem appeared in two places: PostgreSQL connections exhausted during AI generation traffic, and the content editor delayed the first useful screen. Adding application capacity would not address both. More instances could increase database pressure while users still downloaded the same large JavaScript bundle.

At NovaEdge Digital Labs, we migrated the React SPA and Express architecture to Next.js 15, Prisma, and Tailwind CSS. Initial JavaScript fell by 45%; global Time to First Byte dropped from 800ms to under 150ms. The useful lesson is how rendering, authorization, and connection ownership fit together.

The Core Engineering Challenge

AI generation requests combine short database operations with comparatively long waits on model providers. Under load, our previous backend saturated its available connections and produced database timeouts. The engineering question was not simply whether PostgreSQL could execute a query quickly. It was whether concurrent requests could acquire connections when they needed them.

Two independent bottlenecks

The frontend had a separate dependency chain. Loading a large editor client-side put substantial parsing, execution, and hydration work before useful content. Poor Largest Contentful Paint reflected that startup path. Fixing the database alone would leave the browser bottleneck intact; shrinking the bundle alone would leave generation requests timing out.

The migration therefore had two objectives: remove unnecessary browser work from initial rendering, and make database access bounded and predictable. Neither required pretending that the interactive editor could become entirely server-rendered.

System Architecture: Next.js 15 App Router

Moving workspace data fetching into React Server Components removed our browser-side request waterfall. The server could resolve the workspace context before sending the rendered shell, instead of making the browser hydrate components and then discover which API requests to issue.

Keep the client boundary narrow

Editor interactions still belong in Client Components. Navigation, workspace context, and other noninteractive output can remain server-rendered. That distinction matters: marking an entire layout with use client pulls its imported component tree into the client bundle. RSCs also do not automatically parallelize sequential awaits; independent reads still need deliberate scheduling.

Prisma supplied typed queries and connection pooling. Pooling addressed resource exhaustion; generated types made the schema and query refactor safer. Type safety itself does not repair timeouts. A reusable Prisma client and a connection budget across application instances are still necessary. Creating a fresh pool per request defeats that budget.

Long model-provider calls should sit outside database transactions. Otherwise, a slow external response extends connection occupancy without doing database work. Pool sizing must account for maximum application instances and reserved PostgreSQL capacity, not just the traffic seen by one process.

Tailwind and deployment boundaries

Tailwind CSS supports the server-rendered shell with build-generated styles and consistent layouts. It does not explain the database improvement, and it cannot remove editor JavaScript. Its contribution is keeping presentation independent of unnecessary client-side component logic.

Edge delivery and database execution are distinct. Our Edge deployment reduced document delivery latency; it did not distribute PostgreSQL globally. The direct Prisma example below runs in a Node.js server runtime. An Edge runtime requires a compatible adapter or database proxy. Authenticated workspace responses must not enter a shared public cache.

Code-Level Deep-Dive: One Prisma Authorization Query

Workspace authorization should establish membership, a permitted role, and billing entitlement together. The following representative example checks paid, managed generation. Its model names and tier labels are illustrative; adapt them to your schema. Free BYOK access needs its own entitlement policy.

Assume a unique (userId, workspaceId) membership, an optional one-to-one workspace subscription, and subscription state maintained from signature-verified Razorpay webhooks. userId comes from a validated server session, never a request body's claimed identity.

This example targets Prisma 6.x with PostgreSQL. Enable relationJoins and regenerate the client so relationLoadStrategy: "join" produces one SQL query rather than separate relation reads.

prismaschema.prisma — generator configuration
generator client {
  provider        = "prisma-client-js"
  previewFeatures = ["relationJoins"]
}
typescriptlib/require-generation-access.ts
import { prisma } from "@/lib/prisma";

// userId must come from the validated server session.
export async function requireGenerationAccess(
  userId: string,
  workspaceId: string,
) {
  const access = await prisma.workspaceMembership.findUnique({
    relationLoadStrategy: "join",
    where: {
      userId_workspaceId: { userId, workspaceId },
      revokedAt: null,
      role: { in: ["OWNER", "ADMIN", "EDITOR"] },
      workspace: {
        is: {
          archivedAt: null,
          subscription: {
            is: {
              provider: "RAZORPAY",
              status: "ACTIVE",
              tier: { in: ["PRO", "TEAM", "ENTERPRISE"] },
              currentPeriodEnd: { gt: new Date() },
            },
          },
        },
      },
    },
    select: {
      role: true,
      workspace: {
        select: {
          id: true,
          subscription: { select: { tier: true } },
        },
      },
    },
  });

  if (!access?.workspace.subscription) {
    throw new Error("Generation access denied");
  }

  return {
    workspaceId: access.workspace.id,
    role: access.role,
    tier: access.workspace.subscription.tier,
  };
}

The compound unique index bounds the membership lookup. Relation filters reject archived workspaces, revoked membership, insufficient roles, and inactive or expired paid plans. Nested select returns only the workspace ID, role, and tier, avoiding document bodies or unrelated billing fields. These constraints keep authorization queries small enough to reason about when traffic and tenant counts grow.

One Prisma method call is not automatically one database round trip: relation load strategy determines that behavior. The explicit join makes the authorization read one statement. It does not reserve generation credits or revalidate a payment against Razorpay's live API. Credit consumption requires a separate atomic operation.

Performance Results: Payload, TTFB, and PostgreSQL Timeouts

MetricBeforeAfter
Initial JavaScript payloadPrevious SPA payload45% smaller
Global TTFB800msUnder 150ms
PostgreSQL connection timeoutsOccurred under generation loadEntirely resolved through pooling

These outcomes address different parts of the request path. Less initial JavaScript reduces browser download and startup work. Lower TTFB gets the document to the browser sooner. Resolved connection timeouts make database access reliable under the generation workload. None of these measurements establishes how quickly an external model finishes writing an article.

Likewise, the payload reduction is not an LCP percentage improvement. LCP depends on the actual largest element, rendering work, fonts, images, and network conditions. A useful regression check separates cold starts from warm requests, records pool wait time, and compares equivalent routes and devices. Otherwise, an attractive aggregate can hide a slower editor or a saturated database.

Conclusion: Why NovaEdge Uses This Stack for B2B SaaS

We prefer this stack for enterprise B2B SaaS because it makes boundaries explicit. Next.js separates server-rendered context from browser interaction. Prisma expresses relational authorization with generated types and controlled database access. Tailwind keeps the interface consistent without requiring presentation to live inside a large client bundle.

The framework choices matter only when those boundaries survive implementation. Multi-tenant products still need permission checks at mutation boundaries, deliberate cache scope, and a connection budget that holds as instances scale. A fast initial document cannot compensate for an expensive authorization path or unreliable generation requests.

Recuvix reinforced the approach behind NovaEdge's SaaS engineering: remove avoidable work, keep scarce resources occupied briefly, and measure each stage independently. That is the engineering lesson we carry into the other products in our portfolio.

AKR
Built & Curated by Amit Kumar Raikwar

Full-stack engineer & product designer shipping production software worldwide.

Visit amitkumarraikwar.dev

Frequently Asked Questions

Moving workspace data fetching to React Server Components removed the browser-side request waterfall and reduced initial JavaScript. PostgreSQL connection pooling addressed the database timeouts separately; the interactive editor still requires Client Components.
#Next.js#Prisma#PostgreSQL#React Server Components#Tailwind CSS#Engineering#Case Study
AKR
Founder & Lead Engineer

Amit Kumar Raikwar

Full-stack engineer and product designer in Indore. I take software from an empty Figma file to a live product that takes payments, and I do the interface, the API, the database and the deploy myself.

Visit Personal Portfolio

Keep Reading

Related Insights