One GraphQL schema serves your own app and external partners — how do you scope what each audience sees?
answer
- Two questions wearing one word
- Only one of them is a boundary
- Pick by lifecycle, not by secrecy
- Showing a field is promising it
- Generate variants, never hand-edit
basics
~20 sSeparate visibility from authorization. What executes is decided by checks at execution for every audience; what a published schema shows is a contract decision, and a field a partner can see is one you must deprecate rather than delete.
solid answer
~50 sTwo different questions get conflated here. **Authorization** decides what runs and must be enforced during execution for every caller, internal or not. **Visibility** decides what an audience is handed as documentation — and a field that is only safe because a partner cannot see it is not safe. Then pick a visibility mechanism by lifecycle, not by secrecy: one schema with everything visible is cheapest and makes every field a de-facto public commitment; a variant filtered from the same source at build time keeps one source of truth at the cost of drift; separate endpoints per audience isolate hardest and duplicate most; per-request filtering by viewer is the most expensive and the hardest to support, since two callers now get different errors for the same document. Default to one executable schema plus authorization, and add a filtered variant only when an audience has its own change cadence.
code
graphql · 7 linestype Case @key(fields: "id") {
id: ID!
caption: String!
filings(first: Int = 20): [Filing!]!
billingRateCents: Int! @tag(name: "internal")
conflictCheck: ConflictCheck @tag(name: "internal")
}go deeper
Take away the core rule: whether a caller sees a field and whether a caller may use it are different questions, and only the second is decided by a security check.
Be ready to explain why a field left visible to an external audience is hard to remove later, and what a deprecation window buys you compared with deleting the field outright.
Argue the mechanisms concretely — one schema, a build-time variant, a separate endpoint, per-request filtering — and name the operational cost of each, especially drift between what is published and what executes.
Own the policy: what differs between audiences, who may add a field to a published variant, how variants are generated and diffed in CI, and the standing rule that no field's safety may rest on an audience not seeing it.
## The distinction the whole answer rests on A schema is two things at once: an enforcement artefact and a published contract. Confusing them produces both of the classic failures — a team that thinks hiding a field protects it, and a team that publishes everything and then cannot change anything. * **Authorization** is what executes. It is enforced when a field resolves, it applies identically to your own app and to a partner, and it is the only thing on this page that is a security boundary. * **Visibility** is what an audience is told exists. It is documentation and commitment. It affects support load, integration speed and your freedom to evolve, and it affects an attacker's discovery cost by roughly one afternoon. The rule that follows: *no field may be safe only because an audience cannot see it.* Every design below assumes authorization already holds, and then chooses a visibility arrangement for contract reasons. ## The four arrangements, and what each actually costs **One schema, fully visible, authorized at execution.** The cheapest thing that works, and the right default. Its real cost is not security, it is *commitment*: everything in the type system becomes a documented promise to whoever reads it. On a legal case-file graph that meant `billingRateCents` — added for an internal dashboard — appeared in the schema partners introspected, a partner client began selecting it, and fourteen months later removing it was a breaking change that needed a deprecation window rather than a delete. Nobody decided to expose it; nobody had to. **One source, several published variants.** Keep one executable schema and derive per-audience schemas from it at build time. In a federated setup the Apollo Federation composition directives give you the labelling primitive — `@tag` attaches metadata to a field or type, and the tooling that assembles a published variant is what actually drops the tagged elements. One source of truth, no duplicated types, and the filtering is reviewable in the same pull request as the field. The costs are a build step in the critical path, a second artefact to version, and drift: what a partner reads and what the server executes are now two objects that can disagree, so the variant must be generated by the pipeline and never edited. **Separate endpoints, separate schemas.** Strongest isolation and the easiest to reason about in an audit, because the partner-facing service simply does not define the internal fields. It is also the most expensive: duplicated types drift, a change lands twice, and a shared domain concept ends up with two subtly different representations. Justified when the audiences differ in more than visibility — different rate limits, different availability commitments, different deployment cadence. **Per-request filtering by viewer.** Compute the visible schema from the caller's identity. It sounds like the general solution and is the one to argue against. Building a filtered schema is not free — recomputing one per request measured at 11 ms against a 340 ms p99 budget for the case-file API, so it must be cached per audience, at which point you have variants again with extra machinery. Worse, it makes behaviour caller-dependent: the same document is valid for one caller and a validation error for another, so support cannot reproduce a partner's failure from an internal session, and the differing error text is itself an oracle. ## Choosing, as a lead Ask what actually differs between the audiences. If the answer is only "partners should not be distracted by internal fields", that is a documentation problem and a single schema with good deprecations and a curated partner guide solves it. If the answer is "partners are on a different change cadence and we cannot break them", that is a lifecycle difference and it justifies a published variant. If it is "partners have different availability, throttling and support commitments", that is a product difference and it justifies a separate endpoint. Then write down the consequences you are accepting: * **Visibility is a promise.** Publishing a field to an audience starts a deprecation obligation for that audience. Decide who may add a field to a published variant, and treat that as a contract review. * **The executable schema stays fully authorized.** A filtered variant is never a control; the router or server still resolves whatever a caller names. * **Drift is the failure mode to engineer against.** Generate variants in CI, publish them to a registry, and diff them per release so a removal is caught before a partner's pinned client discovers it. * **Descriptions are audience-facing text.** If two audiences read the same field, its description is written for the outer one. ## The short answer "Authorize everything at execution regardless of audience, then treat the schema each audience sees as a contract decision. Start with one schema; introduce a build-time filtered variant when an audience has its own change cadence; go to separate endpoints only when it has its own operational commitments. And remember that showing a field to a partner is the same as promising it."
- Why is per-request schema filtering by viewer usually the wrong answer?It makes validity depend on who is asking. The same document is accepted for one caller and rejected for another, so support cannot reproduce a partner's failure from an internal session, and the differing error text leaks the very structure the filter was meant to hide. It also costs real latency unless the filtered schema is cached per audience — at which point you have built variants the hard way.
- What does the @tag directive from the federation composition specification do on its own?It attaches a label and nothing more. Tagging a field neither hides it from clients nor denies it at execution; the tool that assembles a published schema variant is what reads the labels and omits the tagged elements. The executable schema still resolves the field for anyone who names it, which is why authorization must be independent of the tag.
- A partner is pinned to a field you want to delete. How does the visibility decision change the options?It removes the fast options. Once a field was published to that audience, removal is a breaking change on their timeline, not yours: deprecate it with a reason, watch usage until it reaches zero for that audience, then remove. If the field had never been in their variant, the same deletion would have been an internal change reviewed in a single pull request.
saying these in an interview costs you the question
- Treats an audience-filtered schema as an access control
- Adds fields to a partner variant without contract review
- Hand-edits the published schema instead of generating it
- Assumes @tag itself hides or denies the field
- Filters per request without measuring the latency
- Thinks a hidden field cannot be selected and resolved