Skip to content

Technical documentationInternal

For the team and anyone taking the product over. Not linked from the site and not indexed.

Technical documentation pages

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's dataPolicy.wasm is true. 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 a data: entry in connect-src are added only when an entry's dataPolicy.cspAllowances names 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 a data: address. The education tools inherit the Cell policy, so they get both allowances too.
  • In development (phase-development-server, or NODE_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) in document-policy.ts classifies a path:
    • /o, /o/…, /study, /study/…, /literature and /literature/app belong to the boundary objects.
    • /workspace and /workspace/… belong to the distinct account boundary. Entering or leaving it reloads the document so its tighter CSP applies.
    • /<segment>/app… belongs to app:<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 in src/app/layout.tsx. It adds a capture-phase click listener on window. 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;
    • download links;
    • links with a target other than _self;
    • links marked data-policy-navigation="managed".
  • navigateWithDocumentPolicy (policy-navigation.ts) calls flushPendingWorkspaceWork() from src/lib/workspace/pending-work.ts so that registered drafts are saved first. If the flush succeeds and the page has not changed meanwhile, it calls location.assign (or location.replace). "The page" is its origin, path, query and any section hash such as #studies, but not a #s= state fragment (samePage in document-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) wraps router.push and router.replace with 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. 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-host and consent-required are not. A Retry-After header is honoured when exposed, capped at 60 s.
  • Limiter and breaker.
    • Limits are keyed by service, not host, because www.ebi.ac.uk serves 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.
  • Consent. A service configured with consent: "always" needs a ConsentToken for the target host, or the request throws consent-required. Tokens come only from a press in the consent dialog. Inbound links never grant consent. Without a mounted ConsentHost, every request resolves to null and 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. fetchWithTimeout does 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) uses fetchWithTimeout with a consent token from requestConsent (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's trackRequest.
  • Pipeline's src/components/tools/drug-pipeline-explorer/fetchers.ts is shared with the snapshot pipeline and uses plain fetch with its own timeout handling.
  • Molecule's PubChem name lookup in src/components/tools/molecule-workbench/use-workbench.tsx uses plain fetch with a 15 s timeout and an in-memory cache. Molecule's registry endpoint for PubChem declares consent: "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. publicResponse in src/lib/literature/sources.ts refuses any host other than api.openalex.org and www.ebi.ac.uk over HTTPS, and fetches with redirect: "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 Origin matching the request origin (SEC-05), including discover and fulltext through requestBody. Bounded unhosted loopback normalisation works for both development and next start production previews (REL-701): the request URL and canonical Host must both be allowed loopbacks at the same port, using the URL's scheme; VERCEL or WORKSPACE_SITE_URL disables 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.

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).

  • encodeFragmentState writes #s= followed by encodeURIComponent of a key=value&… string. Any other fragment is treated as a plain anchor.
  • The query string is inbound only. acceptInbound validates 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) lists mine and walk. A mine reference 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-src is '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.