From Null to Tenant: Dynamic SSR Fetching with Orval, Next.js, and ASP.NET APIs

David Palacios
3 min read

I used to think tenant-aware SSR was going to be the easy part of a multi-tenant app.

In my head, the flow was simple: detect tenant from the host, call the API, render page, done. In reality, it turned into a pile of tiny problems: inconsistent headers, client and server fetching different payloads, and API contracts drifting quietly over time.

This post is the version I wish I had read before I started.

Where things usually break#

The first breakage was not dramatic. The app still "worked". It just felt unreliable:

  • One tenant rendered correct data server-side, another tenant hydrated into a different state.
  • A backend DTO changed and TypeScript only failed much later in unrelated UI code.
  • Team members copied fetch logic in multiple places, each one setting tenant headers slightly differently.

None of that is a single catastrophic bug. It is worse: gradual entropy.

The shift that fixed most of it#

The fix was architectural, not cosmetic:

  1. Generate API clients from OpenAPI with Orval.
  2. Keep tenant resolution in one clear server path.
  3. Use SSR as the "source of truth" for first render.
  4. Let client queries hydrate from that SSR data rather than racing it.

Once we did that, most weirdness disappeared.

The mental model#

Think of the page request as a pipeline:

  1. Request arrives (tenant-a.myapp.com).
  2. Server resolves tenant identity.
  3. Server client calls API with explicit tenant context.
  4. Page renders with real tenant data.
  5. Client can revalidate later, but starts from that stable snapshot.

That gives you consistency first, then interactivity.

Why Orval helped more than expected#

I originally adopted Orval for convenience. It ended up being a guardrail.

When backend contracts changed, it forced those changes into frontend compile-time feedback. That changed code reviews, too: people discussed domain models instead of debating handwritten fetch wrappers.

A tiny example:

No guessing endpoint strings. No "did we remember that optional field?" roulette.

Tenant context: boring on purpose#

The key improvement was making tenant extraction boring and centralized.

Whether the tenant comes from a subdomain, custom domain map, or trusted header from your edge layer, do that resolution once and pass that context down. Do not scatter tenant parsing across pages and hooks.

If one thing in this post is worth stealing, it is this:

Treat tenant resolution as infrastructure, not UI logic.

SSR + client revalidation without fights#

A pattern that worked well:

  • Server component fetches initial data.
  • Client component receives it as initial state.
  • React Query (or your equivalent) can revalidate after mount.

This gives you fast first paint, avoids loading flashes, and keeps your UX honest.

What changed in day-to-day dev#

After the refactor:

  • Fewer "it only fails on tenant X" bugs.
  • Fewer hydration mismatch surprises.
  • Faster onboarding for new contributors (fetching path was obvious).
  • Better confidence when shipping backend changes.

The app did not become magically simple. It became legible.

Trade-offs to keep in mind#

This approach still requires discipline:

  • Keep OpenAPI specs accurate.
  • Keep mutators thin and predictable.
  • Avoid hidden fallback tenant behavior in production.
  • Log tenant resolution failures aggressively.

If you skip these, type-safety alone will not save you.

If you are implementing this now#

Start with one page and one endpoint. Prove the end-to-end flow first. Do not migrate every endpoint in one sprint.

The stack here (Next.js App Router + Orval + ASP.NET Core) scales well, but only when each layer has one job:

  • Next.js: request-time composition and rendering.
  • ASP.NET Core: domain and API contracts.
  • Orval: contract synchronization.

When those boundaries are respected, multi-tenant SSR stops feeling fragile.

Closing thought#

I called this "From Null to Tenant" because that is exactly how it felt. At first, tenant context was null in all the places where it mattered. After the refactor, tenant context became explicit, typed, and hard to accidentally bypass.

That is when the app started feeling production-ready.