Repository navigation
Add the integration seam design specs - #1342
Open
jwrosewell wants to merge 179 commits into
Open
jwrosewell wants to merge 179 commits into
jwrosewell wants to merge 179 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.
… fetch's timeouts The design said an origin fetch has 15-second timeouts, and now says core asks for them and only the Fastly adapter applies them, the Axum client applying its own limits and the Cloudflare and Spin adapters none of their own.
The registry selects SelectedPrefixModule::default() and the trusted_server() constructor is gone, which ends the two code scanning alerts, so the first open question keeps only whether Prefixes::trusted_server should take the publisher domain alone.
…ines No debug line for a skipped EID carries the source the reader sent, and the line for a partner's EID names the partner's configured domain, with the test that shows it.
A handler is removed only where no quantifier, class or comment follows its prefix or bound, the region the Cloudflare lookup now reads is reported to check, main's jurisdiction check is described as it was, one document run on two adapters is covered, and the review log records round 2.
The specs copy had stopped at the round 2 review of 15 October. It now carries the x-geo response headers written by the headers.geo serve middleware, the Axum and Spin refusal of the platform lookup, and every adapter calling detect and writing the request filters' response headers, as the branches do, with its review log last.
Every named route in the request cycle is shown to run the filters on Axum, Cloudflare and Spin, the design names what a device module is shown and what an unreadable Cookie value loses, gives a true reason no shipped filter is tested through an adapter, names the two DataDome designs whose scope this lifts, and its review log records round 2.
The wait on Axum when the service cannot be reached and the targets its tests skip are corrected, and the review log records the review of the change that has every adapter run the filter.
The specs copy now carries the 16 October fix, in which the rewriter moves each address once in one pass, with the tests that show it.
The specs copy now carries the 16 October code fixes for the rollout and privacy finding of review round 1, in which a withdrawal on an addressed request also expires a pull-sync marker under the root path and tombstones the row of each Edge Cookie the request carried, and the two open questions they raise.
The specs copy now carries section 2.6, in which core names no hosting platform or platform domain in any line, with the counts, the _vercel prefix, the guard and the tests that moved to the Fastly adapter.
The specs copy now carries the 15 October code fixes and the 16 October section in which cache headers follow the stored item's policy, which closes the open question on the cache headers.
A graph configured later replaces only a server-side module's cookie, and a client-side module's cookie keeps its identifier with no row, at the cost the design states. The change discloses more in three cases, the client-side module on a host with no graph being the third, each adapter's resolve route is shown to set a client-side module's cookie, and the review log records round 2.
…exist The design said both caches are built with detachable: false, a field no code has. It now says a load runs in the request that started it, as every load of the loading cache does.
The Cloudflare and Spin adapters install a logger of their own, so the warning for a route that enables the Image Optimizer and the error for Spin's refusal of a host_header naming another host reach each host's log, and the design says where. Spin's refusal is marked breaking for the publisher path too, the warning is weighed against the auction refusal, and the review log records round 2.
…stions The configuration guide says core applies no lifetime of a declared table and when to push one, the integration guide lists .with_cache_features, and the two cache declaration checks are pub(crate), so items 5 to 8 leave the open questions. Section 2.7 and section 3 also say the deploy check refuses a module that offers no loading cache, as it has since the loading cache change.
jwrosewell
removed this pull request from stack #1408
October 18, 2026 20:15
A load that fails asking the origin is the answer to every request that ran or waited on it, so the origin is asked once, and the design's failure handling, the cost of a slow origin and the tests say so.
jwrosewell
added this pull request to stack #1418
October 19, 2026 12:18
The origin redirect design describes the header rewrite merged in main (#1238) with the origin folder that #1378 gives it, in place of the Location rule #1378 no longer has, and records the five deliberate faults its tests catch. The object storage design's sentence on redirects names the mapping it now follows. The same text is on the branch of #1378.
jwrosewell
removed this pull request from stack #1418
October 19, 2026 16:30
jwrosewell
added this pull request to stack #1421
October 19, 2026 17:38
jwrosewell
removed this pull request from stack #1421
October 19, 2026 18:29
jwrosewell
added this pull request to stack #1422
October 19, 2026 19:20
jwrosewell
marked this pull request as ready for review
October 19, 2026 20:22
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, and each design a pull request of the stack links as its spec ends in its review log.
Test plan
CI on this pull request at a854526 passed all 23 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!)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.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, and the last document has no pull request 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-12-attestation-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-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.