From Null to Tenant: Dynamic SSR Fetching with Orval, Next.js, and ASP.NET APIs
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.
- Generate API clients from OpenAPI using Orval
- Resolve tenant context once, at the request boundary
- Treat SSR as the authoritative source for initial state
- 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:
- Request arrives (
tenant-a.myapp.com) - Tenant identity is resolved
- Server calls backend APIs with explicit tenant context
- Page renders from that data
- 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.