Stage 7 · Master
The Gateway Service
Internal Calls and Context Propagation
How one external request remains one coherent internal story: tenant context, deadlines, trace correlation, and actor identity must survive the gateway hop, with libs/platform/tenancy providing the first shared contract that already exists in meridian today.
Why Context Is a Correctness Problem
Context propagation is often taught as an observability concern. In a multi-tenant platform it is also a correctness concern. If the gateway authenticates a caller and resolves a tenant, but the downstream service forgets that tenant boundary and invents a fresh background context, the system has lost the most important fact about the request before any database query even starts. That is not merely untidy. It is a direct path to wrong-tenant reads and impossible-to-debug incidents.
A request that enters the platform should retain at least four pieces of continuity as it travels inward: a trusted tenant identifier, a caller or actor identity, a trace or request identifier, and a deadline budget. These values do not all serve the same purpose. Tenant ID constrains data access. Actor identity constrains authorization. Trace ID links logs and errors. Deadline preserves the caller's remaining patience so downstream work does not outlive the request that needed it.
Workflow
Auth: Gateway authentication establishes who the caller is before any private hop is attempted.
Step 1 / 5 — Auth
The Shared Tenant Contract
Meridian already ships one important shared propagation contract: libs/platform/tenancy defines X-Tenant-Id and context helpers. docs/architecture/services.md makes the policy explicit: the browser never tells internal services which tenant to use directly. The gateway resolves the human-readable slug to the tenant UUID and forwards that UUID downstream. No internal service should invent an alternative resolution rule.
This is a strong design choice because it centralizes trust. The gateway is the only service allowed to translate slug into tenant UUID. Downstream services should consume that resolved tenant ID rather than repeating resolution logic with their own caches, route params, or query parameters. Shared vocabulary plus single-point resolution is how a distributed system avoids becoming a distributed argument.
Propagating Deadlines and Identity
Tenant context is necessary and not sufficient. The gateway should also forward actor identity or trusted auth claims after validation, and it should preserve the request's context.Context so cancellation and deadlines propagate into internal HTTP calls. The rule is simple: internal calls should use http.NewRequestWithContext, never context.Background(). If the client is gone or the deadline is spent, downstream work should stop too.
| Propagation choice | Why it looks harmless | Real consequence |
|---|---|---|
| context.Background() for internal calls | Avoids nil-context mistakes | Drops cancellation, deadlines, and request lineage |
| Forward client-supplied tenant header directly | Looks like less work | Lets the public caller attempt to smuggle untrusted scope into private services |
| Gateway-derived context and headers | Requires deliberate middleware | Chosen because internal services now trust one source of tenant and request lineage |
What the Gateway Must Overwrite
A common mistake is to treat forwarding as copying. Real propagation is selective rewriting. The gateway must distrust any public request header that claims to be internal authority. If the browser sends X-Tenant-Id, X-User-Id, or any future internal correlation header, the gateway should discard those values, authenticate and resolve the request itself, and then write fresh internal headers for the private hop. Trust is created at the gateway boundary, not inherited from the internet.
The same discipline applies to deadlines. The gateway should forward the remaining request budget, not invent extra time for downstream services. If almost no time remains, failing fast at the gateway is often better than starting work the downstream service cannot possibly finish meaningfully. Go's context propagation gives this behavior almost for free if the original context is preserved.
Current Repository Status
libs/platform/tenancy is implemented, but no middleware currently reads X-Tenant-Id from an incoming internal request or writes it into context. apps/gateway does not yet proxy to downstream services. This lesson specifies the propagation design that should be built on top of the existing shared contract.
apps/gateway/internal/router/router.gomodifyResponsibility: Will forward trusted tenant context and request context on reverse-proxied calls.
Why now: The gateway is the only service allowed to translate slug scope into internal tenant identity.
apps/gateway/internal/middleware/middleware.gomodifyResponsibility: Will eventually authenticate requests, resolve tenant scope, and sanitize untrusted public headers.
Why now: Trust should be established at the gateway boundary before the request enters the private network.
apps/identity-service/internal/http/middleware.gocreateResponsibility: Would read X-Tenant-Id and populate request context via libs/platform/tenancy.WithTenantID.
Why now: Downstream domain and repository code should consume trusted tenant context rather than parse transport headers directly.
- Treat context propagation as both an observability concern and a tenant-correctness concern.
- Use the existing X-Tenant-Id and context helpers from libs/platform/tenancy as the single tenant vocabulary.
- Forward internal requests with http.NewRequestWithContext so cancellation and deadlines survive the hop.
- Overwrite any public attempt to supply internal authority headers; the gateway must mint trusted context itself.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.