Blog post

Request Context Is Where SaaS Trust Becomes Executable

Why request context is not backend plumbing, but the place where identity, tenant scope, and permissions become enforceable.

Request Context Is Where SaaS Trust Becomes Executable

I used to think of request context as backend plumbing.

Useful, necessary, but not especially interesting. A place to put the current user, maybe a request ID, maybe some metadata for logging.

Then I started thinking more seriously about multi-tenant SaaS architecture, and the object changed meaning.

Request context is not just a convenience. It is where the backend turns claims into trust.

The problem it solves

Every meaningful operation needs to answer three questions:

  • Who is acting?
  • Which workspace or tenant are they acting within?
  • What are they allowed to do?

If those answers are rediscovered in every route, action, repository, or component boundary, the system becomes fragile.

One endpoint reads a tenant from the URL.

Another trusts a value from the request body.

A third checks permissions in a helper.

A fourth assumes the UI would never send an invalid workspace.

Individually, each decision can look harmless. Together, they create a system where trust is renegotiated everywhere.

That is not a safe architecture.

Claims are not context

A frontend can express intent.

It can say: the user selected this workspace. The user is opening this record. The user clicked this action.

But those are claims.

They are not yet trusted context.

The backend has to validate them against the authenticated identity, membership, permissions, tenant access, and current policy. Only then can the request operate inside a trusted scope.

That distinction matters.

A selected workspace is not the same thing as an authorized tenant scope.

A role stored somewhere is not the same thing as permission to perform this action now.

A user ID in a session is not enough unless the system also knows what that user can access in this request.

Request context is the object that should carry the validated answer.

What belongs in it

A simple request context might contain:

type RequestContext = {
  actorId: string;
  tenantScope: TenantScope;
  permissions: PermissionSet;
  requestId: string;
};

The exact shape does not matter as much as the principle.

The context should be established once, server-side, at the boundary of the request.

It should be passed through the application deliberately.

It should not be casually rebuilt, widened, or overridden by lower layers.

Most importantly, dangerous operations should require it.

If a use case can mutate tenant-owned data without trusted context, the type system is allowing an unsafe state.

Why this matters more with AI-generated code

AI coding agents are very good at continuing local patterns.

If one route passes tenantId manually, another route may do the same.

If one repository accepts raw tenant strings, another repository may copy that shape.

If authorization is optional in one path, generated code may treat it as optional elsewhere.

This is why request context is not only a runtime concept. It is also a design signal.

It tells both humans and agents: identity and tenant scope are not casual parameters. They are trusted inputs created by a controlled boundary.

The clearer that contract is, the less each new piece of code has to invent.

The anti-pattern

The dangerous version looks like this:

await updateRecord({
  tenantId: body.tenantId,
  userId: session.user.id,
  recordId: body.recordId,
  data: body.data,
});

It works, but the trust story is unclear.

Where did the tenant come from?

Was it validated against the session?

Can this user act in that tenant?

Is this permission checked here, upstream, or nowhere?

The safer version moves that uncertainty out of the operation:

await updateRecord({
  context,
  recordId,
  data,
});

Now the use case can require a trusted context instead of raw claims.

That does not solve everything, but it changes the default. The operation starts from validated authority, not from values supplied by the request.

The principle

Request context is the backbone because it prevents every part of the backend from inventing its own idea of trust.

It does not make security automatic. Nothing does.

But it gives the system a place where identity, tenant scope, permissions, and traceability become explicit and enforceable.

Without trusted context, every endpoint becomes a place where trust can be accidentally renegotiated.

With it, trust has a path through the system.

Continue exploring

Follow the same line of thought through themes, tags, or a broader local search across the archive.

Keep following the thread.