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

David Palacios
4 min read

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

In theory, the flow is simple: detect tenant from the request, call the API, render the page.

In practice, it turned into a series of small inconsistencies that compounded over time:

  • tenant context handled slightly differently across requests
  • server and client rendering diverging under certain conditions
  • API contracts changing without immediate visibility
  • fetch logic duplicated across the codebase

Nothing failed catastrophically.

But the system became harder to trust.


Where things usually break#

The first issues were subtle.

The app still worked — but it stopped being predictable:

  • One tenant rendered correctly server-side, but hydrated into a different state on the client
  • A backend DTO changed, and TypeScript only surfaced issues far away from the source
  • Fetch logic drifted across files, each implementation slightly different in how tenant context was passed

This is not a single bug.

It is loss of determinism.

And once that starts, the system slowly becomes harder to reason about.


The shift that fixed most of it#

The solution was not a patch — it was a boundary decision.

  1. Generate API clients from OpenAPI using Orval
  2. Resolve tenant context once, at the request boundary
  3. Treat SSR as the authoritative source for initial state
  4. Let the client revalidate from that state, not compete with it

This reduced ambiguity across layers.

More importantly, it made data flow explicit.


The mental model#

Instead of thinking in terms of “fetching data,” it helps to think in terms of a request pipeline:

  1. Request arrives (tenant-a.myapp.com)
  2. Tenant identity is resolved
  3. Server calls backend APIs with explicit tenant context
  4. Page renders from that data
  5. Client enhances or revalidates, but does not redefine initial state

This creates a clear rule:

The first render should already be correct.

Everything after that is enhancement, not correction.


Why Orval mattered more than expected#

Orval started as a convenience tool. It became a structural constraint.

By generating clients from OpenAPI:

  • backend changes surfaced immediately at compile time
  • endpoint usage became consistent
  • implicit assumptions about payloads were reduced

No manual endpoint strings.
No guessing response shapes.

Example:

This shifted discussions from “how are we calling this API?” to
“is this the correct domain model?”

Which is where they should be.


Tenant context: infrastructure, not UI logic#

The most important change was conceptual:

Tenant resolution is not a UI concern — it is part of the system’s infrastructure layer.

Whether tenant identity comes from:

  • subdomains
  • custom domains
  • trusted upstream headers

It should be resolved once, early, and passed explicitly through the system.

Scattering tenant parsing across components and hooks introduces inconsistency.

Centralizing it makes behavior predictable.


SSR + client revalidation without conflict#

A pattern that held up well:

  • Server components fetch initial data
  • Client components receive it as initial state
  • Client-side queries (e.g. React Query) revalidate after mount

This avoids:

  • loading flashes
  • duplicate requests racing each other
  • inconsistent UI state during hydration

It also reinforces a simple rule:

The client should not correct the server — it should build on it.


What changed in day-to-day development#

After this shift:

  • Fewer tenant-specific bugs that were hard to reproduce
  • Fewer hydration mismatches
  • A clearer, shared understanding of the data flow
  • Faster onboarding (the path from request → data → UI was explicit)

The system did not become simpler.

It became legible.


Trade-offs to keep in mind#

This approach still depends on discipline:

  • OpenAPI specifications must stay accurate
  • API mutators should remain predictable and thin
  • Tenant resolution must not silently fall back in production
  • Failures in tenant resolution should be logged and visible

Type-safety helps, but it does not replace system design.


If you are implementing this now#

Start small.

Pick one route and one endpoint. Validate the full flow end-to-end.

Do not migrate everything at once.

The stack works well when responsibilities are clear:

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

Respecting those boundaries is what makes the system scale.


Closing thought#

I called this “From Null to Tenant” because that was the real issue.

Tenant context existed — but not in the places where it mattered.

After the refactor, tenant context became:

  • explicit
  • consistent
  • and difficult to bypass accidentally

That is when the system started feeling stable.

Not because it was more complex — but because it was more intentional.