Repository navigation
Add the integration seam design specs - #1342
Open
jwrosewell wants to merge 203 commits into
Open
jwrosewell wants to merge 203 commits into
jwrosewell wants to merge 203 commits into
Conversation
The five-PR series (#1043 to #1047) opens the identity, device and geo seams. The nine vendor integrations already in core sit behind the integration registry instead, which is a private table, so none of them can move out until that table is opened. This spec defines the one core change that opens it: public registration builders with a second input on IntegrationRegistry, browser JavaScript carried on the registration, startup validation as a hook, the same treatment for auction providers and the bid renderer contract, and neutral replacements for the two places where a vendor reaches into core. It then sets out the migration of all nine existing integrations, one PR each. The change is complete in itself: after it, no vendor move needs a core change. Written against the series' tree with the file and line references for every claim about the current code. Documentation only.
… registration capabilities Adopt the registration shape asked for in the review of PR 1043 and apply its rule consistently, so a module declares its identity, geo and device providers alongside its JavaScript and hooks (new section 3.6). Record the relationship to the pluggable-providers spec in PR 986, reorder the series so this spec and its core implementation precede PR 1043, and add the capabilities round trip to the acceptance list.
A probe integration built outside core and registered through an adapter exercised every seam end to end. Four things surfaced that reading the code did not: the operator CLI reaches deploy validation through a type that supplies no builders, so a vendor's own rules are skipped on the path an operator uses; a carried browser module's hash literal is hand-maintained and breaks on a checkout that rewrites line endings; a provider is resolved more than once per request, which the shared-backend principle needs a per-request context to fix; and one core reader still reads the APS renderer payload directly. Also records the construction-time verification of a carried module's declared hash.
The review of #1043 asked that spec changes land before the code that implements them, so a divergence is a decision taken in review rather than a ratification of something already merged. PRs #1043 to #1047 each carried the design document for their own step, and #1043 carried a 607-line spec describing device providers, geo providers, the permission model and the browser resolve endpoint, none of which is in that PR. Move all six series documents here, so this PR carries the complete normative set and no code: - 2026-07-30-pluggable-providers-design.md (from #1043) - provider-code-registry.md (from #1043) - 2026-07-30-permission-model-design.md (from #1045) - 2026-07-30-client-cycle-ec-resolve-design.md (from #1046, later revised by #1047) - 2026-07-30-integration-response-header-hook-design.md (from #1047) - 2026-07-30-provider-migration-rollout-design.md (from #1047) Each file is taken verbatim at the tip of the stack, so the later revisions are preserved: the provider-switching continuity section, the geo requires-signal floor, and the code-envelope paragraph #1047 added to the client-cycle spec. The revision-record tables are unchanged. No document's substance was edited. The only edits are to this spec's own status line, which said the PR adds one document and that the series specs land with #1047, and a revision-record row recording the move.
The rule that every trait method needs a production caller in the change that introduces it is right for a behavioural trait, where a method nothing calls is dead weight. It is wrong for an evidence interface, and we applied it there and should not have. An evidence interface describes what a request carries, not what today's code happens to read. Held to the caller rule it grows a method each time a vendor arrives, so a vendor cannot write against it and it cannot stay stable across a release. It also puts the boundary in the wrong place. What a provider may see is not the control, because what a provider may do with what it sees is the control, and that is the permission model's job. So RequestInfo carries what the request carries. The rule stands everywhere else. Addresses: docs/superpowers/specs/2026-07-30-pluggable-providers-design.md section 4, a rule applied to an interface it does not suit
"Mint" is jargon for something ordinary, which is making an identifier. It was defined in the glossary, so it was at least explained, but a reader should not need a glossary for the word that describes the main operation the specs are about. Fifty-one uses across five files, including the glossary row itself, now read create, creates, created or creating. Nothing else changed, so a reader comparing against the previous revision sees one word substituted and no argument altered. Addresses: plain language in documents others read
…style fixes Delete the reserved 51dd provider-code registry row so the unpublished vendor provider is not named in the public spec. Correct section 8's count from four to seven, replace several with many, and restructure clause-joining colons into separate sentences across the provider specs. The 51Degrees contribution attribution lines are kept deliberately.
The four series specs (client-cycle EC resolve, permission model, pluggable providers, migration and rollout) each carried a Status line saying they were implemented. The code they describe is only in PRs #1043 to #1047 and none of those is merged, so the line read as shipped behavior. Each now says Proposed, names the PR that carries the implementation and states that it is not yet on main, keeping the existing revision dates and notes. The integration provider seam spec carried counts and line references that do not hold on main at d516a9e. Corrected against that commit: - Section 4 said migration_guards.rs embeds "the thirteen vendor files". The directory holds 23 .rs files (2 infrastructure, 6 in nextjs/, 2 in datadome/, 13 top-level integration modules), the guard embeds 20 of them and 9 of those 20 belong to the nine vendors, with osano.rs and the two datadome/ files absent. builders() registers 13 integrations, which is a different 13 from the file count. - Section 3.5 gave no counts for the prepare and finalize calls. There are nine production prepare_request call sites across the four adapters and a tenth in core, and the single production finalize_response call site is in core rather than in any adapter. - Section 8 item 3 described a proxy resolving geo twice, which does not happen on main. The real double resolution is the adapter EC context build against handle_auction on POST /auction. - Section 8 item 5 understated the Spin gap and misdescribed Cloudflare. Cloudflare covers every route it registers and has no health route, while Spin skips its first-party bindings as well as its inline admin stubs. - Line references: settings.rs:166 to :215, auction/mod.rs:49 to the list at :51 to :53, publisher.rs:4361 to :4369. Section 6 now requires the round trip to be proven on the Fastly adapter, the primary deployment target, rather than on any adapter, because Fastly has no library target and the round trip otherwise only runs on the Axum dev server.
Rewrite the resolve endpoint's origin check as an exact serialized-origin
comparison, defaulting to the single origin https://{publisher.domain}
with an optional operator-configured list, and delete the sibling
subdomain justification that did not hold. Record that the origin check is
defense in depth under the provider's envelope verification, require the
sibling subdomain, wrong scheme and non-default port rejection cases, and
close open question 7.5.
Require core to expire the ts-ecr marker on any request carrying a ts-ec
the selected provider does not own, in both the client-cycle and pluggable
provider specs, so a provider switch restarts the client cycle instead of
leaving a visitor with a marker and no identity. Record the constraints on
carrying resolved permissions to the browser without designing the
mechanism, which belongs with the first vendor module.
Require a nonempty DeviceProvider::required_permissions from a
module-supplied provider to be refused at registry resolution rather than
described as a documented no-op.
Replace the DataDome singleton-header rule that skipped the vendor call on
a repeated field, which would have created an attacker-controlled bot
detection bypass and described no shipped behavior. Core now passes
repeated field lines through as received and leaves interpretation to the
vendor integration, keeping only the protocol-level content-length rule.
Correct the APS migration row and add a seam spec finding for the
pre-existing browser-side APS coupling in core TypeScript.
Sign-off row 6 said no vendor move needs a core change after this design. Section 8 item 8 now records that TSJS core still imports the APS renderer directly, so an APS move needs a browser renderer contract that this design does not provide. Row 6 now claims only the Rust side, which is what the design actually delivers.
…wording claims Prettier realigns table columns in six specs that the 1 September review edits widened, which the format-docs workflow checks. The client-cycle spec no longer says the origin check normalizes to an effective port, because origins_match compares the port as the browser wrote it and adds or removes nothing. The response-header-hook spec no longer claims Trusted Server rejects a malformed or body-inconsistent content-length, because no core code does that. It is HTTP framing handled by the host HTTP layer, and the point that matters is that no integration reads the value.
The client-cycle spec described the origin check as a string comparison of the serialized origin. The implementation on the client-resolve branch now applies the RFC 6454 same-origin test, comparing the scheme, host and port triple with a missing port meaning the scheme's default, so the spec names that rule and the RFC rather than describing a comparison of its own.
…s and the page The pluggable-providers spec said a geo declaration is refused like a device one, but nothing consults PlatformGeo::required_permissions and its own body says so, so the summary now names device only. The permission-model spec records that a module device provider may not declare a permission, because the registry refuses one at startup. The client-cycle spec says the demo page script now declares and checks its required Data Use through tsjs.whenPermissions(), that the head script needs a head to inject into, that a bot-classified visitor receives the state resolved for the request rather than an empty one, and what the promise resolves with when nothing arrives.
The rules section of permissions.yaml becomes one tree, and [geo] default_country retires. Every node carries a group with optional children, country codes sit at the first level and region codes beneath them, a node written as a single string is shorthand for that group with no children, and matching runs region, then country, then the top node. The top node is the only one that also requires a jurisdiction, whose values come from the Jurisdiction type in crates/trusted-server-core/src/consent/jurisdiction.rs, and it states the baseline and the consent handling for a request whose country cannot be resolved. The permission model spec carries the change in its format, validation, resolution and failure-mode sections, with the canonical example and a new section 12 recording each before and after row and the date. The providers and rollout specs follow, where the configuration example, the startup-error rows and the migrated operator configuration drop default_country and point at the top node instead. Historical ledger rows keep what earlier pull requests implemented and gain a note naming the restructure, so the record still reads true. The audience for permissions.yaml is a policy owner rather than a developer, so the nesting shows precedence on the page instead of hiding it in slash-joined keys. Trusted Server encodes no jurisdiction's law, as the deployer states the policy and the software only carries it out.
Jurisdiction::from_policy_name in crates/trusted-server-core/src/consent/jurisdiction.rs reads a US state as us-state/<CODE>, for example us-state/CA, rather than the shorter form the format section first listed. The section now names the four accepted values as the parser spells them and records that any other value is a configuration error. Trusted Server encodes no jurisdiction's law, as the deployer states the policy and the software only carries it out.
The rules tree carries jurisdiction on any node, not only at the top. A node without one inherits the nearest ancestor that has one, the top node must carry both group and jurisdiction so the inheritance always terminates, and group and jurisdiction fall back independently, so the two answers for a visitor need not come from the same node. The us-state value drops its state code, because the region node it sits on already names the state, which also makes the value invalid at the top and on a country node. Section 3.4, which recorded one source of jurisdiction truth as deferred, is now adopted. consent.gdpr.applies_in and consent.us_states.privacy_states retire into the tree, where the shipped file holds the same 31 GDPR countries inheriting gdpr from the top node and the same 20 states with a comprehensive privacy law as region children of US carrying jurisdiction: us-state, so the drift the draft named has nothing left to drift against. The consumer side stays open, as auction dispatch still reads the consent subsystem's gate. The format, validation, resolution and failure-mode sections carry the change, section 12 gains a row for each of the three extensions, and the providers and rollout specs pick it up where they describe the top node and the migrated operator configuration. Trusted Server encodes no jurisdiction's law, as the deployer states the policy and the software only carries it out.
A geo provider that reports an outright lookup failure resolves every permission to the requires-signal floor and its jurisdiction to unknown, rather than reaching the tree at all. Section 5.2 and the failure-mode matrix now say so, matching what permissions.yaml states for the same case. Trusted Server encodes no jurisdiction's law, as the deployer states the policy and the software only carries it out.
…named owners The 3.6 list loosely named the host geo lookup among the platform services, which contradicted the design's own rule that geo is a provider a deployer selects, with a host lookup only one data source an implementation may consume. The 4 migration rule now also requires each migrated vendor crate to carry a visible maintainers declaration, the way Prebid.js requires of every adapter, and per-crate code ownership, so a crate boundary carries a named owner from its first day. The deployer selects providers, and the software encodes no vendor's or jurisdiction's choice.
Section 3.6 keeps its rule and now records that the client IP and the TLS, JA4 and HTTP/2 signals are candidates to migrate behind an EdgeZero evidence contract when one exists, which the review discussion on this PR proposed. Also replace a clause-joining semicolon in the same paragraph with a full stop.
Both specs still placed the permissions file at the repository root after the move to config/permissions/vanilla.yaml on the permissions branch.
The permission signal providers in #1045 replace the fixed rule, where an opt-out always beat a consenting TCF record, with the order a deployment configures in [permission_signal] sources. The spec still described the retired rule, so it now states what the implementation does. Section 4 describes the resolution steps, how the order is configured and its default, and the decision matrix under that default. The overview, policy format, validation, withdrawal, normalization, US field mapping, failure-mode matrix, testing strategy and the divergence and revision records follow it, and a new section 13 records each change. The sample policy's opt-out revoke list is also described as it is, a list of specific Data Uses rather than all.
Section 11 said the US opt-out revoke set ships as all, and the example policy in section 3.2 showed revokes: all. The shipped config/permissions/vanilla.yaml lists device storage, sale, sharing, third-party sale, first- and third-party targeted advertising, and profiling, as section 4.5 already says. The example and the section 11 row now match the file.
The implementation renames `config/permissions/vanilla.yaml` to `config/permissions/sample.yaml` and says in the file, the guides and on the constant that the sample is for testing and evaluation only, is not a production policy and is not legal advice. Both specs that name the file follow it, and the two places that introduce it now carry the same qualification, so a reader of the spec is told what the file is before deciding to run it. The permission model spec also drops the phrase "sample policy" for plain "sample", because calling it a policy is the reading the rename exists to prevent. Tests. Prettier accepts both files under the docs configuration, checked with the same `prettier --check` the `format-docs` job runs.
PR #1016 replaced the auction provider table that section 3.4 opened with a compiled auction plan, and the seam implementation follows that plan rather than adding an auction provider builder. Only the generalized bid renderer survives from section 3.4. - Section 3.4 says the change does not open the auction, keeps the renderer contract, and records what the plan keeps closed, being the fixed profile registry, the profile special cases in the plan compiler, and the single mediator the plan accepts. - Section 1 item 4, section 3.1, section 3.3, the APS row in section 4, section 8 item 4 and section 9 row 6 no longer claim an auction provider seam, and section 3.1 records that the Prebid and APS ids are reserved by the duplicate id check.
Every pluggable thing is a provider, each provider type is one top-level table, and a `provider` key inside that table selects what runs. The series specs still described the shapes that convention replaces, so a reader building against them would have written configuration the stack rejects. The providers spec now states the convention once, in a new section 2.1, and the other specs follow it. The seven types are `ec`, `geo`, `device`, `permission_signal`, `demand`, `adserver` and `integration`, all singular and snake_case. `provider` takes a string where one runs and a list where several run. A `[<type>.<name>]` table exists only where that provider has something to set, the word `providers` appears nowhere in configuration, and the name is the implementation unless the table carries `implementation`, which is how two Prebid Servers run side by side under different names. A table its type's `provider` does not select refuses startup, an implementation the build does not carry refuses startup with the list of what it does carry, a demand or ad server endpoint must be HTTPS or HTTP to a loopback host, every provider rejects settings it does not know, and a secret setting names a key in `trusted_server_secrets` rather than holding a value. What changed, by shape: - `[ec.providers.<key>]` becomes `[ec.<name>]`, and the hyphenated selection names become `host_signals`, `client_fixed`, `gpp_sale_opt_out` and `us_privacy`. The `client-fixed-demo` cargo feature keeps its name, because a cargo feature is not a provider name. - `[permission_signal] sources` becomes `[permission_signal] provider`, the list form of the same key every other type uses. - `[integrations.<id>]` becomes `[integration.<id>]`, and which vendors run is `[integration] provider`. - `[auction.providers]` and the word mediator are gone. Demand and the ad server are provider types in their own right, so the seam spec section 3.4 now shows `[demand]`, `[demand.<name>]`, `[adserver]` and the `[auction.bidders.<code>] provider` mapping that stays. APS stopped being an integration, so it is configured under `[demand.<name>]`, carries its `rendering_mode` there, and can never be named in `[integration] provider`. - `[auction]` is not a provider type and keeps `enabled`, `timeout_ms`, the creative settings and `allowed_context_keys`. All three specs that describe the seam now say the same thing about it. Core names no vendor, an implementation is registered by an integration builder, and a builder that supplies an implementation does not have to be a page integration. That is how the HMAC identity provider, the User-Agent-only device classifier and the four permission signal schemes reach configuration without any of them shipping browser JavaScript or being named in `[integration] provider`. Left as they are. The permission model's record of the draft's unadopted `regime` class still names `us-privacy`, because that is a class name from a proposal that was rejected rather than a configuration key. The `signals.us_opt_out.sources` list is policy inside the compiled `permissions.yaml`, not a provider selector, so it keeps its name. `provider-code-registry.md` needed no change, because it allocates four-character identifier codes and describes no configuration. Verified. `prettier --check` under the docs configuration, the same check the `format-docs` job runs, passes on all seven files. Every markdown link, document reference and section cross-reference in the changed files resolves, apart from one that was already broken, being `docs/guide/permission-model.md` in the permission model spec, which exists further down the stack but not on this branch, because this branch targets main directly.
Two rows of the decisions table joined their clauses with a colon and a semicolon. They now read as one sentence each, and the table is realigned to the widened cell.
The loading cache, asset cache, integrations crate and module cache declarations designs end with a review log entry for each round they took, as CONTRIBUTING.md asks.
The module cache declarations, asset routes, no modules crate and client hints designs no longer say that a stock build links a loading cache module or that core holds the publisher's assets, following the loading cache and asset cache designs. The ts command's list carries no loading cache module, the Axum probe test links core's MemoryLoadingCache beside the probe's module and a fourth case links none, the asset routes offer a GET to the module [asset-cache] selects and keep no copy in core, and the 51Degrees cloud answer is held only where a deployment links a loading cache module.
The loading cache design describes the change to rows 24, 32 and 43 as built, with line numbers still those of the code before it, and the asset cache design's header says that row 34 now holds the module, where both said they were written ahead of the code.
43 of the other design documents name a path under crates/<type>/<vendor>/ at this revision, not 41, because the developer experience and module endpoints designs name such paths too. The design gives the search that produces the count.
The developer experience design gives the sample policy as 24,271 bytes, its size at row 4. The loading cache design says that the section 9 search prints six lines at the head of row 55 and, at the published commit it takes line numbers from, those six and the ten stock-list lines the change removes, that the change rewrote the two sibling designs on this branch, and that row 32's rewritten probe test has four cases. The round 4 and round 5 entries of their review logs end with none blocking and no minor point.
The round 1 entry no longer names the first commit of fixpass11/55-scripts-in-crates, which was rewritten before it was pushed and is on no branch.
Forty-one Status and header lines in 38 of our designs called a pull request a draft when it is ready for review. Each now says pull request. Lines naming a pull request that is still a draft keep the word, being the fork's jwrosewell#36 and #43, row 55 (#1423), and Tech Lab's #1194 and #894. Tech Lab's own documents are not touched.
…deployed No build compiles a permission policy in. A domain's settings name the publisher's policy file in `[permissions] policy` and record its SHA-256, `ts config push --permissions <file>` supplies the file, and a domain whose file is missing, unnamed or a different file from the one pushed answers 500 and logs the reason. - The permission model design says how the file travels on each adapter, in the blob from #1345 and as one shared entry per file on Fastly from #1352, the 60 seconds a live push waits between a new entry and the blob from #1354, what a release build carries, and what reading the file costs. - The config by host design adds the shared policy entries, one for each file under its SHA-256, each written before the blobs that name it. - The inspection endpoints design shows the policy's name and digest and never its text. - The config migrate design leaves naming the policy file to do, on every adapter, and a document written for main may name it before or after it is migrated. - The provider migration rollout and response header hook designs follow the same model.
The permission model design's list of scripts carried whole now names only those of the crates a release links, and says that at row 55 the seam probe carries its js/probe.js whole too, in a crate each adapter lists as a dev-dependency alone, so the script never reaches a release. The Cloudflare item names the adapter's own test of its manifests, where the sentence had it read as the README's. The template build digest names the build-support file the build script includes, as CRATE_INPUTS lists it. The rollout design names the commit of main at which it cites the line that carries deny_unknown_fields on Settings.
The permission model, response header hook, inspection endpoints, config by host and config migrate designs end with an entry for each of review rounds 3, 4 and 5, and the provider migration rollout design gains a review log with rounds 1, 2 and 3, as CONTRIBUTING.md asks. Each entry says what the round found and what changed after it. The last round approved the permission model and rollout designs with none blocking, and their entries name the wording points fixed after it. It found no issues in the other four.
The round 5 entries of the permission model and config migrate designs say how the text was brought into line with the code before the round, so their round 3 entries read as records of that round. The response header hook's round 5 entry names the commit its section 1a states.
Step 7 of the upgrade steps said the publisher's stored assets are also fetched again because their keys hold a code worked out from the settings. Core holds no copy of a file once the asset cache is a module, and the asset cache module's keys hold no such code, so the step names the stored pages alone.
The client hints design says it describes the stack from row 52, where every adapter runs request filters, because below that row only the Fastly adapter runs them. The no modules crate design names the order test the ts list has. The Fastly names design names the commit its count of 173 lines is taken at. The module scripts design's first review names the row's branch rather than a local one.
Row 31 moves fiftyone-caching from 4.5.8 to 4.5.13. In the loading cache design, the paragraph on the pin, the table of how rows 24 and 31 differ and the source link say so, and the published 4.5.8-49 and 4.5.13 are compared file by file. The integrations crate design's license sentence names 4.5.13.
The three designs added in this pass are named for 2026-10-27, and every status line, last updated line and revision row that said 2026-10-26 says 2026-10-27, with the links to the three renamed files.
A load asks for the rules held for a pinned digest before it asks a source for the text, so an instance that already holds a host's digest reads and hashes nothing for it. Section 3.7 and item 6 of section 3.1 of the permission model design say so, and the line numbers cited in permissions.rs and config_payload.rs follow the fix in #1345. The config by host design says that such an instance reads no policy entry for the host, in its account of each policy file and of the entry that makes a host fail to load.
A lifetime set in [cache.51degrees-assets-vary] is refused when the settings load and by the deploy check, a fix found in review in #1375. The caches the module declares, failure handling, the rollout of a document written for row 34, the module's tests and what no test covers say so.
A review of this update's changes on 27 October found nothing blocking. Its round is logged in the permission model, config by host, asset cache, loading cache and integrations crate designs. The config by host design says that an instance already holding a SHA-256's rules reads no policy entry for it, where it said such an instance reads nothing. A round on 28 October read again the four designs whose last logged round had found a blocking issue, being the consent decoders, device signals, publisher object storage origin and asset routes designs, against the code of their rows, and two lighter rounds then read the fixes, the last finding nothing blocking. The issues they found in these designs are fixed here as fixes found in review, with revision rows and the three rounds' log entries in each design. The consent decoders design's gate paragraph no longer says what an EU signal that only GPP carries does. The device signals design says its line numbers are those of the commit James Rogers approved, and names row 52 among the later rows. The publisher object storage origin design says what its row does with a URL in a response header and what the next row adds. The asset routes design says a route keeps copies only where a deployment links and selects the 51Degrees asset cache module and the 51Degrees loading cache module, and names EdgeZero v0.0.10.
This was referenced Oct 28, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Changes
docs/superpowerscrates/trusted-server-cli/srcCloses
Part of #777
Spec
Spec: this pull request carries every design of the stack. Each design that a pull request of the stack links as its spec ends in its review log.
Test plan
CI on this pull request at c075081 passed all 24 checks.
cargo test-fastly && cargo test-axumcargo clippy-fastly && cargo clippy-axumcargo fmt --all -- --checkcd crates/trusted-server-js/lib && npx vitest runcd crates/trusted-server-js/lib && npm run formatcd docs && npm run formatcargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1fastly compute serveChecklist
unwrap()in production code (clippy deniesunwrap_usedin CI)logmacros as AGENTS.md asks (notprintln!)This pull request carries the design documents of the stack and the one code change above.
The seven documents in the first table are the specs for the module series
and for the integration seam, reviewed here before the code that implements
them in #1343 to #1348. It targets
maindirectly.At the 51Degrees PR review on 23 October, IAB Tech Lab asked to review the permission designs first and the developer experience after them. The two sections below list those designs in that order, and the tables after them list every design of the stack.
Read first: permissions
2026-07-30-permission-model-design.md, the permission model, built by row 4 (Add the permission model with the Privacy Taxonomy vocabulary #1345). A module declares the permissions its use of data needs, and the publisher's policy file decides which are held. The file is selected and supplied when a service is deployed, so no adapter build carries a policy, and a domain without a valid one answers 500 to every request.2026-10-14-consent-decoders-design.md, built by row 42 (Move consent decoders into their standards' crates #1383). The TCF, GPP and US Privacy decoders leave core for the crates named for their standards, and core keeps only the interface every permission signal module fills.2026-10-12-inspection-endpoints-design.md, built by row 9 (Add inspection endpoints that show what a deployment runs and decides #1350)./_ts/configshows the settings a deployment runs, with secrets masked and the policy's name and SHA-256, and/_ts/permissionsshows the permissions resolved for a request and names the policy that resolved them.2026-10-12-config-by-host-design.md, built by row 11 (Add one app-config blob for each host #1352). Each host has a settings blob of its own, which names its policy file, and the service stores each policy file once however many hosts name it.2026-10-12-module-context-design.md, built by row 8 (Add one module context for every kind of module #1349). Core hands every module one context, and a value whose use needs a permission, being the location, the device signals or the Edge Cookie identifier, reaches only a module that declares the permission and is granted it.2026-10-12-inmobi-choice-module-design.md, built by row 18 (Add the InMobi Choice consent prompt as a module #1359). A consent prompt as a module, what it places on a page, and how the reader's answer reaches the permission model only through the permission signal modules.Developer experience
2026-10-27-developer-experience-design.mdfollows three people through every step, being a module author adding a module, an operator running a deployment from the one signed release WASM that serves every publisher, and the release manager building and signing that WASM. It adds no code. Two new designs sit beside it, one for a module outside core serving an endpoint of its own (row 12, #1353), and one for folding the module crates into one crate that keeps each<type>/<vendor>folder (for a new row 56).What changed after the 51Degrees PR review
Agreed at the review on 23 October, point 1. No configuration or permission policy is compiled into the WASM, and a WASM without a policy does not start.
ts config push --permissions <file>supplies the file, and that a domain whose file is missing, unnamed or not the one pushed answers 500 and logs why.Agreed at the review on 23 October, point 2. The 51Degrees loading cache is pluggable through modules, and the publisher asset feature becomes a module.
crates/asset-cache/51degreesbehind one interface in core, and the module cache declarations design says that a build that links one loading cache module holds a declared cache's answers. Their review logs record each round.Agreed at the review on 23 October, point 3. The
<type>/<vendor>folders stay, in one crate.crates/trusted-server-integrations, with a folder for each type and vendor, and its review log records each round. Its code is a later pull request, row 56, on top of this stack.Agreed at the review on 23 October, point 4. Attestation leaves the stack, with no attestation interface in core.
Agreed at the review on 23 October, point 5. Tech Lab reviews the permission designs first, then the developer experience.
Fixes found in review
crates/<type>/<vendor>/, 43 of them, and gives the search that finds them.tslist, the Fastly names design names the commit its count is taken at, and the module scripts design's first review names the branch of Move module browser scripts and Tinybird into their crates #1423.[cache.51degrees-assets-vary]refuses the lifetimes it never reads, as Hold the publisher's assets in the loading cache #1375 now does.Dependency update
fiftyone-caching4.5.13, which Move the loading cache module to the released caching crate #1372 moves to, and compares the published 4.5.8-49 and 4.5.13 file by file. The integrations crate design's license sentence names 4.5.13.Kept current
mainat 7aa5c99.One more commit dates the three designs this update adds 2026-10-27, the day they are proposed, and gives the same date to the status, last updated and revision lines this update writes.
The documents
2026-08-27-integration-provider-seam-design.md2026-07-30-pluggable-providers-design.md[ec] module,[device] moduleand[geo] module, and the configuration convention every type follows2026-07-30-permission-model-design.mdrules:tree in the permissions file, and permission signal modules run in a configured order2026-07-30-client-cycle-ec-resolve-design.md2026-07-30-provider-migration-rollout-design.md2026-07-30-integration-response-header-hook-design.mdprovider-code-registry.mdThe stack's design documents, one for each pull request
This pull request carries every design document of the stack in one place, the seven above and those in the table below. The table gives the pull request that builds each document and the issue it answers. The developer experience design adds no code, and no pull request builds the settings and consumers design yet. The move to the released caching crate (#1372) completes the loading cache and is recorded in the loading cache's document.
2026-10-12-module-context-design.md2026-10-12-inspection-endpoints-design.md2026-10-12-page-middleware-design.md2026-10-12-config-by-host-design.md2026-10-15-fastly-per-host-selector-design.md2026-10-27-module-endpoints-design.md2026-10-12-administration-off-the-publisher-domain-design.md2026-10-12-robots-txt-from-modules-design.md2026-10-12-diagnostic-headers-design.md2026-10-12-analytics-module-design.md2026-10-12-cookie-constant-design.md2026-10-12-inmobi-choice-module-design.md2026-10-12-middleware-media-types-design.md2026-10-12-ads-txt-sellers-design.md2026-10-12-consent-prompt-phases-design.md2026-10-12-middleware-creates-a-missing-file-design.md2026-10-12-middleware-prepare-design.md2026-10-12-loading-cache-design.md2026-10-12-robots-txt-middleware-design.md2026-10-12-shared-vendor-tables-design.md2026-10-12-prefix-module-design.md2026-10-12-device-signals-design.md2026-10-12-viewport-sizes-design.md2026-10-12-google-tag-module-design.md2026-10-15-module-cache-declarations-design.md2026-10-15-geo-module-request-design.md2026-10-15-asset-cache-design.md2026-10-13-request-addressing-design.md2026-10-13-publisher-object-storage-origin-design.md2026-10-13-origin-redirect-design.md2026-10-13-middleware-settings-design.md2026-10-14-source-modules-design.md2026-10-14-rsc-flight-middleware-design.md2026-10-14-identifier-modules-design.md2026-10-14-consent-decoders-design.md2026-10-14-no-modules-crate-design.md2026-10-14-51degrees-client-hints-design.md2026-10-14-auction-protocol-design.md2026-10-14-last-names-in-core-design.md2026-10-14-backend-naming-design.md2026-10-14-origin-signing-design.md2026-10-15-fastly-names-in-core-design.md2026-10-15-cookies-by-path-design.md2026-10-17-config-migrate-design.mdts config migraterewrites a settings document written formain2026-10-17-request-filters-on-every-adapter-design.md2026-10-17-edge-cookie-on-every-adapter-design.md2026-10-17-asset-routes-on-every-adapter-design.md2026-10-19-module-scripts-in-crates-design.md2026-10-27-integrations-crate-design.md2026-10-27-developer-experience-design.md2026-10-16-settings-and-consumers-design.mdThe one configuration convention
Every type is a top-level table with a selector key inside it, and
[<type>.<name>]holds that selection's settings only when it has any:The key is
modulewhere a type runs one andmoduleswhere it runsseveral, for every type.
[ec],[geo],[device]and[ad-server]takemodule,[permission-signal]and[demand]takemodules, and a pageintegration's section takes whichever fits. A table's name is the
implementation it configures, unless the table carries an
implementationline, which names the implementation by its module path. A demand table
always carries one, because
demandis not the type its implementations arenamed under, and that is how two instances of one implementation run side by
side under names a deployment chooses. A name is parts joined by
., each oflower case letters, digits,
_or-. A module from a crate is named by itsfolder below
crates/, and a[demand]or[ad-server]label of theoperator's own is snake_case.
The specs also state what this buys, which is the part a reviewer should
weigh. A table the selector does not name stops startup rather than sitting
unused, an implementation this build does not have stops startup with the ones
it does have listed, a setting a module does not know is refused rather than
dropped, and demand and ad server endpoints must be HTTPS, or HTTP to a
loopback host. The operator-facing version of the same rules lands with the
code as
docs/guide/configuration-rules.mdin #1348.
The auction in the seam spec
Section 3.4 opens the three things the compiled auction plan keeps in core on
main, being a fixed list of three OpenRTB profiles, special cases for theprebid-serverandapsprofiles by name, andadserver_mockas the only adserver the plan accepts. The ad server, plain OpenRTB, Prebid Server and APS
become implementations that an integration registers on its builder, named
by module path as
ad-server.mock,auction-protocol.openrtb,auction.prebid-serverandauction.aps, and a deployment selects them with[demand] modulesand[ad-server] module.The compiled plan stays, and what it compiles changes from a fixed profile
list to whatever implementations the registered builders offer, so core names
no vendor. An auction-side vendor needs its own crate and a registration, on
the same terms as an identity, location or device module.
The bid renderer contract is unchanged. It is an open descriptor, a type tag
plus the payload the demand provider supplies, with the same serialized form,
so the response a page receives does not change.
The migration and rollout spec records the cost. The auction keys change with
no deprecation period, which is a deliberate choice for a major release, and a
configuration still carrying
[auction.providers]or[auction] mediatorisrefused with a message naming where the setting moved to.
Page changes as middleware
Section 3.7 of the seam spec. Today a page change is one of four hook traits
an integration implements, run in the integration's one position in the
pipeline, for every document. The spec replaces the four traits and their four
context types with one middleware contract, run only where an ordered
[[fetch]]or[[serve]]entry names it. An entry covers one media type,optionally only the requests under a path prefix, and lists the middleware in
order, so a publisher can run a change on part of a site, or place two
changes of one integration apart, without touching the auction's priority.
Entries carry no settings and activate nothing, which stays where it is
decided today.
The two phases make the shared-template rule structural rather than
declared. A fetch middleware is handed nothing about the reader, so what it
leaves is what the shared template stores for every reader. A serve
middleware runs per reader on that reader's copy and nothing it writes is
stored. Each integration converts in a change of its own, checked against the
differential harness, and the traits are deleted when the last one converts.
Governance question this raises
The seam spec's section 2 states a principle for the task force rather than for
the compiler. Tech Lab engineering owns core and reviews vendor crates, and
vendors ship and maintain their own integrations. That is the change that
removes the per-vendor core work the project pays for today, most recently the
LiveRamp module (#1054). Opening the auction side widens it, because a demand
source or an ad server is now a vendor crate too.
Declared interest
51Degrees is a vendor and would use this seam for its own integration (#1072).
The reasoning here applies to every vendor on the same terms, ourselves
included, and our own module follows the same route.