Security and network
This page explains how the suite's HTTP security headers are generated from the app registry and which hosts each page may contact. It also covers the client and server code that makes outbound requests, and the rules that keep user state out of server logs and out of third-party hands.
Where the headers come from
next.config.ts is a thin wrapper. Its headers() returns suiteHeaders(phase) from content/apps/next-config.ts. That function builds the header rules from the registry (content/apps.ts) through content/csp.ts, content/redirects.ts and content/education-tools.ts. No app edits the config itself. See Registry and content for the registry.
suiteHeaders returns its rules in this order:
| Order | Source pattern | Headers | Function |
|---|---|---|---|
| 1 | /:path* |
X-Robots-Tag: noindex, unless SUITE_INDEXABLE=1 |
noindexRules |
| 2 | /vendor/:path* |
Cache-Control: public, max-age=31536000, immutable |
inline |
| 3 | /data/:path* |
Cache-Control: public, max-age=600, stale-while-revalidate=86400 |
SHORT_LIVED |
| 4 | /<education-id>/app/:path* |
The Cell (inside-a-cell) security headers |
headersFor("inside-a-cell") per EDUCATION_NAV entry |
| 5 | Hashed search catalogue files and each app's cacheRules |
Immutable caching | appCacheRules in content/redirects.ts |
| 6 | /<segment>/app/:path* for each app |
The app's security headers | appHeaderRules |
| 7 | /<segment>/:task(id1|id2…) for each app with tasks |
The app's security headers | taskHeaderRules |
| 8 | /o/:path*, /study/:path*, /literature |
The union policy of the apps with object tabs | objectPageHeaderRules |
| 9 | /workspace/:path* |
Same-origin account policy and standard security headers | accountPageHeaderRules |
Rule sources are built by helpers in content/apps/routes.ts: workspaceRouteSource, taskRouteSource and OBJECT_ROUTE_SOURCES. SUITE_BASE is the empty string, so the suite serves from the root of its origin.
Routes not listed above get no Content Security Policy and none of the other security headers from these rules. The exception is /technical, where src/proxy.ts sets its own headers (see the last section). That covers the landing page, product pages, docs, explainers, /security, /regulatory, /changelog and the /api/* route handlers. They still receive the noindex header until launch. content/csp.test.ts asserts that the landing, product, docs and policy pages get no CSP.
The header comment in content/csp.ts follows the registry's root workspace rules (/<segment>/app, with Calc retaining /calculators/app), the union object policy and the separate account policy.
Which policy a route gets
| Route | Policy | Built by |
|---|---|---|
App workspace, /<segment>/app and every page under it (print pages, the Cell tour's step pages) |
That app's policy | headersFor(slug) via appHeaderRules |
Task pages, /<segment>/<task> |
The same headers as the app's workspace, because task pages take user input | taskHeaderRules; an app with no tasks gets no rule |
Education tools, /<id>/app/:path* for each id in EDUCATION_NAV |
The Cell app's policy: no external hosts, plus the Mol* widenings | suiteHeaders |
Bench, object and study pages (/o, /o/…, /study/…) and /literature |
The union of the apps whose registry entry lists objects.tabs |
objectPageCsp, objectPageHeaderRules |
Account page and callback (/workspace, /workspace/…) |
Same-origin connections, no workers, no production eval; frame-ancestors 'none' |
accountPageHeaderRules |
A study page shows no app view, but it uses the object pages' policy so that the two cannot drift apart. That reason is given in the comment on objectPageHeaderRules.
On object pages, an endpoint marked workspaceOnly: true in the registry is left out of the union policy. Its app's object tab never contacts that host. Plasmid's NCBI E-utilities endpoint is the current case; the comment in content/apps.ts cites the CTO's ruling of 6 October 2026, memo 2.
The Content Security Policy
directivesFor in content/csp.ts emits these directives, in this order:
| Directive | Value |
|---|---|
default-src |
'self' |
script-src |
'self' 'unsafe-inline', plus 'wasm-unsafe-eval' and/or 'unsafe-eval' where allowed (see below) |
worker-src |
'self' blob: |
connect-src |
'self', https://<host> for each declared endpoint host, blob:, plus data: where allowed |
img-src |
'self' data: blob:, plus https://<host> for endpoints with cspAlso: ["img-src"] when a rendered image needs that host; the unused AlphaFold allowance was removed (SEC-09) |
style-src |
'self' 'unsafe-inline' |
font-src |
'self' |
object-src |
'none' |
base-uri |
'self' |
form-action |
'self' |
frame-ancestors |
'none' |
An app's connect-src is exactly the hosts its registry entry declares in dataPolicy.endpoints, via hostsFor(slug). Each endpoint is an AppEndpoint with a bare host, a limiter service, the operator, purpose, what the host receives, a consent level and the provider's terms. To call a new API, an app has to declare it in the registry first. Declaring it puts the host on the product page, in Settings and in the privacy statement before the browser will allow the request. content/csp.test.ts checks that each app's connect-src is 'self', its declared hosts over HTTPS and blob:.
Why inline scripts are allowed
The pages are statically generated and carry Next's own inline start-up scripts. A hash-based or nonce-based script policy would block those scripts unless every app page were rendered on request. So script-src includes 'unsafe-inline'. The reason is recorded in the header comment of content/csp.ts.
Eval and WebAssembly allowances
'wasm-unsafe-eval'is added only when an app'sdataPolicy.wasmistrue. That is currently Molecule (content/apps/molecule-workbench.ts) and Assay (content/apps/assay-analyser.ts), both of which use the shared RDKit WebAssembly engine.'unsafe-eval'and adata:entry inconnect-srcare added only when an entry'sdataPolicy.cspAllowancesnames them, each with a reason string. Protein (content/apps/protein-explorer.ts) and Cell (content/apps/inside-a-cell.ts) declare both for the Mol* viewer. Mol* starts a WebAssembly module bundled inside it, and it reads that module from adata:address. The education tools inherit the Cell policy, so they get both allowances too.- In development (
phase-development-server, orNODE_ENV=development),'unsafe-eval'is added to every policy because React's debugging tools need it.
The registry type comment says each allowance was found by loading the app under its policy and recording every violation, and that content/csp.ts adds nothing that is not in the registry.
Other security headers
headersFor(slug), objectPageHeaderRules and accountPageHeaderRules send these headers alongside the CSP:
| Header | Value |
|---|---|
Referrer-Policy |
no-referrer |
Permissions-Policy |
camera=(), microphone=(), geolocation=(), payment=(), usb=() (PERMISSIONS_POLICY) |
X-Content-Type-Options |
nosniff |
Cross-Origin-Opener-Policy |
same-origin |
The design-level description is in design system §11.5 (docs/design/suite-design-system.md) and contracts §8.6 (docs/design/shared-contracts.md).
Account and literature route handlers set their own response headers; see Server, accounts and billing. src/app/api/share-image/route.ts adds X-Content-Type-Options: nosniff to the images it generates.
Search indexing
The CEO's 16 October 2026 request enables public indexing by default.
SUITE_INDEXABLE=0 explicitly restores the global preview noindex header.
Robots permits crawling and names the static sitemap. The sitemap includes
all reference workflows, public app hubs, /o and /literature, as well as
existing public tasks, explainers, documentation and methods pages. Ordinary
app workspaces, personal object/work/study pages and technical documentation
retain their own noindex metadata. No saved browser data is server-rendered
into the public /o or Literature scaffold.
Each reference path gets exactly the policy of its workspaceApp; the Calc
Gibson route therefore gets Plasmid's policy. /calc participates in document
policy boundaries. See Public reference workflows.
Crossing a policy boundary loads a new document
A CSP belongs to the document that was loaded, not to a client-router response. If Next's client router moved from a page under one policy to a page under another, the new page would run under the old document's policy. src/components/suite/navigation/ prevents this.
documentPolicyBoundary(pathname)indocument-policy.tsclassifies a path:/o,/o/…,/study,/study/…,/literatureand/literature/appbelong to the boundaryobjects./workspaceand/workspace/…belong to the distinctaccountboundary. Entering or leaving it reloads the document so its tighter CSP applies./<segment>/app…belongs toapp:<segment>. This includes the education tools.- Any other path whose first segment is an app segment belongs to
route:<path>. Each product or task page is a separate boundary, even within one app. - Everything else is
public.
needsDocumentNavigation(from, to)is true when the origin or the boundary differs.PolicyNavigationBoundary(policy-navigation-boundary.tsx) is mounted once insrc/app/layout.tsx. It adds a capture-phaseclicklistener onwindow. Same-origin links that cross a boundary are intercepted before Next can turn them into client-side navigation. The listener ignores:- modified clicks and non-primary clicks;
downloadlinks;- links with a target other than
_self; - links marked
data-policy-navigation="managed".
navigateWithDocumentPolicy(policy-navigation.ts) callsflushPendingWorkspaceWork()fromsrc/lib/workspace/pending-work.tsso that registered drafts are saved first. If the flush succeeds and the page has not changed meanwhile, it callslocation.assign(orlocation.replace). "The page" is its origin, path, query and any section hash such as#studies, but not a#s=state fragment (samePageindocument-policy.ts): the flush itself may rewrite the page's own#s=fragment (the URL-state writer is one of the registered barriers), and before 17 October 2026 that fragment rewrite silently cancelled the leave (REL-240).usePolicyRouter(use-policy-router.ts) wrapsrouter.pushandrouter.replacewith the same check. It is used for imperative navigation such as the command palette, handoffs and object actions.
The header rules give /literature the object-page policy, but documentPolicyBoundary classifies it as public. A move between /literature and a public page is therefore treated as same-boundary, even though the two have different header policies. See the boundary tests in src/components/suite/navigation/document-policy.test.ts before changing either side.
The client network layer
src/components/suite/net/ implements the networking contract in contracts §8 (docs/design/shared-contracts.md). The CSP is the browser-enforced limit. This layer adds a host check, consent, rate limiting and diagnostics in code.
| Module | Role |
|---|---|
fetch.ts |
fetchWithTimeout, fetchJson, checkHost, configureNet, the request log, FetchError kinds |
backoff.ts |
Retry with exponential backoff (contracts §8.2) |
limiter.ts |
Per-service concurrency lanes and circuit breaker (contracts §8.3) |
budget.ts |
Per-job request counting and in-flight activity for the status bar (contracts §8.4) |
consent-dialog.tsx, consent-view.tsx |
The consent gate's dialog, requestConsent, ConsentHost (contracts §8.5) |
offline.ts |
The online/offline signal (contracts §8.7) |
Host declaration and enforcement
AppShell (src/components/suite/shell/app-shell.tsx) calls configureNet({ hosts: hostsOf(app) }). That is the same list that feeds the app's connect-src. checkHost resolves the URL against the page origin. A same-origin URL is allowed. A cross-origin URL is allowed only over https: with a declared hostname. Anything else throws FetchError("blocked-host") before any network activity. Until configureNet runs, only the page's own origin is allowed.
Timeouts, retries, limits and consent
- Timeouts. The default is 15 s (
DEFAULT_TIMEOUT_MS). Download lanes get 60 s (DOWNLOAD_TIMEOUT_MS). A service may set its own timeout. The timer stays active until the body has been read, so a body that stalls still times out. - Retries. Three attempts by default, waiting 1 s and then 2 s with ±25 % jitter. Network errors and HTTP 429, 502, 503 and 504 are retried. Other 4xx responses, parse errors, timeouts, aborts,
cooling-down,blocked-hostandconsent-requiredare not. ARetry-Afterheader is honoured when exposed, capped at 60 s. - Limiter and breaker.
- Limits are keyed by service, not host, because
www.ebi.ac.ukserves several services. - Each service has lanes with a concurrency limit and an optional minimum interval between request starts (
DEFAULT_SERVICES). - The breaker opens for 60 s after three consecutive failures, after three network failures within 10 s, or at once on a 429. After 60 s it lets one probe request through.
- Lane slots use Web Locks, and breaker state is broadcast on
BroadcastChannel("mbio-suite:limiter"), so several tabs act as one client.
- Limits are keyed by service, not host, because
- Consent. A service configured with
consent: "always"needs aConsentTokenfor the target host, or the request throwsconsent-required. Tokens come only from a press in the consent dialog. Inbound links never grant consent. Without a mountedConsentHost, every request resolves tonulland nothing is sent. - Request log. The log keeps the last 500 requests in memory for About › Diagnostics. Each entry records the service, host, status, time, bytes and label. It never records a URL, query, header or key, and it is never transmitted.
- No response cache.
fetchWithTimeoutdoes not cache responses. When the connection returns, nothing is refetched automatically.
Which code uses it
Not every app goes through fetchWithTimeout today. The CSP still bounds them all. The helper's header describes that implemented scope; the remaining provider migrations and contracts §8.1 conformance stay open under MNT-01.
- Plasmid's NCBI client (
src/components/tools/plasmid-map/eutils-client.ts) usesfetchWithTimeoutwith a consent token fromrequestConsent(src/apps/plasmid-map/views/plasmid-workspace.tsx). - Protein has its own
src/components/tools/protein-explorer/fetch.ts, which adds timeouts and retries on dropped connections. It reports in-flight requests through the suite'strackRequest. - Pipeline's
src/components/tools/drug-pipeline-explorer/fetchers.tsis shared with the snapshot pipeline and uses plainfetchwith its own timeout handling. - Molecule's PubChem name lookup in
src/components/tools/molecule-workbench/use-workbench.tsxuses plainfetchwith a 15 s timeout and an in-memory cache. Molecule's registry endpoint for PubChem declaresconsent: "none".
Same-origin calls to the suite's own /api/* routes use plain fetch. Examples are the UniProt suggestion hook in src/lib/search/, the literature client in src/components/suite/literature/client.ts and the workspace account code. The CSP allows these through 'self'.
Server-side proxies
Some features call external services from route handlers on the server, so that page policies do not need to widen. The comment in src/app/api/search/compounds/route.ts says this lets "every search surface use PubChem without loosening app CSPs". Server, accounts and billing documents each handler. In summary:
- Fixed upstream hosts. Each handler builds its upstream URL from a fixed host: PubChem, ChEMBL at
www.ebi.ac.uk, UniProt REST, Europe PMC, OpenAlex or the Anthropic API. The client never supplies a URL.publicResponseinsrc/lib/literature/sources.tsrefuses any host other thanapi.openalex.organdwww.ebi.ac.ukover HTTPS, and fetches withredirect: "error". - Bounded input. Inputs are bounded and pattern-checked:
- search text is limited to 120 characters, with no control characters;
- identifiers such as
CHEMBL…, PubChem CIDs, DOIs, PMIDs and PMCIDs must match fixed patterns; - JSON bodies are size-limited (
requestBody,limitedText,boundedRequestText).
- Bounded upstream calls. Every upstream call has an
AbortSignal.timeout, between 8 s and 80 s depending on the route. - Origin checks. Every literature POST entry path requires a present exact
Originmatching the request origin (SEC-05), including discover and fulltext throughrequestBody. Bounded unhosted loopback normalisation works for both development andnext startproduction previews (REL-701): the request URL and canonicalHostmust both be allowed loopbacks at the same port, using the URL's scheme;VERCELorWORKSPACE_SITE_URLdisables this exception. Forwarded headers never supply the recovered host. The AI routes and all account mutation routes also require an exact origin match, with their existing independent access checks. This is a browser-origin defence, not authentication: a nonbrowser caller can forge the header, and per-client/provider abuse controls remain open.
Compound-name suggestions use the bundled catalogues. Name lookups contact PubChem through this site. SEC-02 disables the automatic remote supplement in usePubchemSuggestions across the palette, literature search, Molecule and Calc without changing their local suggestions or deliberate lookup actions. The retained autocomplete endpoint uses upstream cache: "no-store" and Cache-Control: private, no-store on every response.
The client consent gate in fetch.ts still does not apply to these same-origin calls. Direct PubChem name lookups therefore remain a consent-alignment follow-up: the limiter says "always", while direct-call registry entries say "none". The automatic-typing disclosure is repaired; complete consent alignment and the legal privacy wording remain open.
Link state lives in the fragment
Shareable state goes in the URL fragment, #s=<encoded>, never in the query string. The fragment is never sent to a server, so it does not reach server logs or the Referer header. The grammar is in src/components/suite/store/url-state.ts (design system §6.27; contracts §3 in docs/design/shared-contracts.md).
encodeFragmentStatewrites#s=followed byencodeURIComponentof akey=value&…string. Any other fragment is treated as a plain anchor.- The query string is inbound only.
acceptInboundvalidates the frozen legacy parameters, merges them (the fragment wins) and rewrites the address to the fragment form before any request is made. FRAGMENT_ONLY_KEYS(src/lib/handoff/fragment-only.ts) listsmineandwalk. Aminereference identifies a person's own work, so these keys are dropped from inbound queries and never written into a query or a Copy link.- Links longer than
LINK_MAX_LENGTH(2,000 characters) are refused, and the user is offered a file instead. - History writes follow contracts §3.3. A subject change pushes a new entry. Other changes replace the current entry, at most once every 2 s.
Server-side search routes are the exception. They take their input in the query string of a same-origin GET request to /api/search/*. That is a request, not shareable link state.
No analytics in the workspaces
The engineering rules in CLAUDE.md forbid analytics in the workspaces. The public copy in content/apps/security.ts and content/apps/shared-texts.ts states there is no analytics and no error reporting in the apps.
The mechanism is structural:
- An app may connect only to
'self'and its declared endpoints. font-srcis'self'.- Scripts load only from
'self', apart from Next's inline start-up scripts.
An analytics or monitoring host would therefore have to be added to an app's registry endpoints. That would put it in the privacy host table. Do not add one.
Technical documentation
The technical documentation served at /technical asks for no password (D-0081; the HTTP Basic authentication D-0052 required was removed on 13 October 2026). src/proxy.ts sets that area's own response headers: no shared caching, noindex and a policy with no third-party hosts. In Next 16, the proxy file convention replaces middleware. The overview describes the area.
Tests that guard this
| Test | What it checks |
|---|---|
content/csp.test.ts |
Directives, connect-src equals declared hosts, the WebAssembly and eval allowances, education, task and object rules, the pages that get no CSP, rule order |
content/apps/next-config.test.ts |
Noindex until SUITE_INDEXABLE=1, build identity and origin variables |
src/components/suite/navigation/document-policy.test.ts |
Policy boundary classification |
src/components/suite/net/*.test.ts |
Host guard, retry, limiter and breaker behaviour |
Build, test and CI explains how these run. Deployment and environment covers SUITE_INDEXABLE and the origin variables.