New here? PROTOTYPE.md is written for designers — running it, what a set is, adding one, and how syncing with the real service works.
The frontend for the high-risk plants import notification journey. It runs on
the same obligation and journey platform as the live-animals frontend: a
journey-agnostic engine under src/server/app/, with all journey content in
the high-risk-plants set beneath it. The set provides the notification
journey from commodity selection through declaration and confirmation.
Run the set's unit suite from the repo root: npm run test:high-risk-plants.
The prototype maintainer reviews and merges designers' pull requests and the weekly sync pull requests.
- Runtime. The prototype runs on its own stubs everywhere, sign-in
included: locally with
npm run dev, and in CDP dev from the production image its ownDockerfilebuilds.STUB_MODE's bypass is honoured in production too, by design (Sam's decision) — the deployed prototype signs in exactly the way a localnpm run devdoes, so it needs no Defra ID stub and noDEFRA_ID_*variables in CDP dev. SettingSTUB_MODE=falserestores plants-frontend's own Defra ID sign-in. The pull-request boot check in.github/workflows/check-pull-request.ymlproves the deployed shape every time: it boots the built image in production mode and drives a stub sign-in through to the chooser, then boots it again with a password set and signs in through the password page. - The prototype password. CDP puts nothing in front of a service, so
the prototype has its own shared password, the way the GOV.UK Prototype
Kit protects a deployed prototype. Add a CDP secret called
PROTOTYPE_PASSWORDto the prototype's dev environment in the CDP portal and redeploy: every page but/healththen asks for it first (the password page is at/prototype-password), and remembers it in a signed cookie for 30 days. Unset, the prototype is open to anyone who reaches it, and says so in its start-up log. Changing the secret signs everyone out./prototype-password/sign-outforgets it in one browser. It is one shared password for now; the code lives insrc/server/prototype-password/. Try it locally withPROTOTYPE_PASSWORD=<anything> npm run dev. - Env vars.
src/config/config.jsis the one list, with each variable'sdocsaying what it is for and what it defaults to. The redirect URLs already default to this prototype's own port (3103), not plants-frontend's 3003. The header's "Address book" link is deliberately dead — no env var sets it — because the address book belongs to the Import Notification Service, which this prototype does not include. - One instance. Run a single instance in CDP dev. Data lives in
memory (
SESSION_CACHE_ENGINE=memory, the stub stores), so a second instance would show a different set of examples to different visitors depending on which one served them. - Data resets. Every merge to
mainredeploys the prototype, which restarts the process and empties every set back to its seeded examples — the same thing "Reset this prototype's data" does for one set on request. Nothing else clears it. - Freeze merges during research. A merge mid-session would reset every participant's data. Ask the team not to merge anything from "before a demo or a research session" (PROTOTYPE.md, "Deploying and merging") until the session ends.
- The demo page and the technical report. Every pull request, and every
push to
main, builds a stakeholder demo page — the most important journeys first, each with a video, paced so it is watchable — plus the full Playwright report (FIT tests plus every walkthrough, with traces) attests/underneath it. Both publish to thegh-pagesbranch together:reports/pr-<n>/for a pull request, the site root (https://defra.github.io/trade-imports-plants-prototype/) formain, so the lasting link never depends on a pull request having existed (reports/main/stays as a redirect to the root, for links made before the move). The pull request gets a comment leading with the demo page link. A nightly job (.github/workflows/prune-reports.yml) removes closed pull requests' reports, sogh-pagesstays small; the root demo page and report are never pruned. Each run is also uploaded as theprototype-playwright-reportActions artifact (the demo page plustests/), so the checks are useful even before Pages is turned on.
Three things are still pending:
.claude/settings.jsonis still plants-frontend's copy, so it wires up three Sonar hook scriptsoverrides.jsondeletes rather than the designer edit guard (scripts/designer/hooks/guard-edit.js). Replacing it, and adding it tooverrides.jsonoursin the same commit on achore/<slug>branch, so the weekly sync stops restoring the Sonar hooks, is Sam's call: raise it with him before making the change.- Once the prototype is deployed, put its address in
deployedUrlinscripts/designer/prototype.json(the research sheet reads it from there) and in PROTOTYPE.md, "Deploying and merging". - Turn on GitHub Pages for this repository: Settings, Pages, "Deploy
from a branch",
gh-pages,/ (root). The branch appears after the first pull request or merge publishes a report. Until Pages is on, the report links above resolve to nothing and the pull request comment says so, but nothing fails: the Actions artifact is still there.
The high-risk-plants journey is implemented, with a notification dashboard, a task-list hub, collecting pages and a review and submission flow. The entry guard sends an unstarted notification to the commodity-type question. See The served surface today for the registered routes and tasks, and Recipe exemplars for real features to follow when extending the journey.
Deployed end-to-end tests for this service live in the shared tests repository
trade-imports-ins-tests, as its plants Playwright project alongside
animals, animals-admin and ins. See
Cross-repo test ownership.
Node 24 or later, and npm 11.6.2 or later — the floor in engines.
To use the correct version of Node.js for this application, via nvm:
cd trade-imports-plants-prototype
nvm useWe use Catbox for server-side caching. By default the service uses CatboxRedis
when deployed and CatboxMemory for local development. Override with
SESSION_CACHE_ENGINE, set to either redis or memory.
CatboxMemory (memory) is not suitable for production use: the cache is not
shared between instances of the service and does not survive a restart.
Redis is an in-memory key-value store. Every instance of a service has access to
the same Redis key-value store, similar to how services might have a database.
All frontend services are given a namespaced prefix that matches the service
name, so my-service has access to everything in Redis prefixed with
my-service.
We use forward-proxy, which is set up by default. To make use of it,
import { fetch } from 'undici': because of the
setGlobalDispatcher(new ProxyAgent(proxyUrl)) call, requests use the
ProxyAgent dispatcher.
If you are not using Wreck, Axios, Undici or a similar HTTP client that uses
Request, provide the proxy dispatcher yourself:
import { ProxyAgent } from 'undici'
return await fetch(url, {
dispatcher: new ProxyAgent({
uri: proxyUrl,
keepAliveTimeout: 10,
keepAliveMaxTimeout: 10
})
})Install application dependencies:
npm installnpm install installs the pre-commit hook — postinstall runs
npm run setup:husky. The hook runs npm run git:pre-commit-hook: format
check, lint and the unit suite.
To run the application in development mode:
npm run devIt serves on port 3103.
To mimic the application running in production mode locally:
npm startAll available npm scripts are in package.json. To list them:
npm runThe ones you will use most:
npm run test:high-risk-plants # the set's own Vitest suite, no coverage
npm test # the full Vitest suite with coverage
PORT=3053 npm run test:fit:features
npm run test:fit:journeys
npm run lint # JS, stylesheet and dependency-cruiser
npm run formatnpm run fit:start — the one the Playwright web server uses — stays
stub-backed and needs no stack.
npm run lint:arch runs Dependency Cruiser over src/server/app and enforces
the L1–L4 layer rules in .dependency-cruiser.cjs. Production code cannot use
test exemptions.
This prototype is never part of the workspace docker stack (see "Local
stack" below): it runs against the Defra ID stub standalone, or with
STUB_MODE=true bypassing the OIDC round trip entirely (see below).
To run against a real stub on localhost:3007, create an env file:
DEFRA_ID_OIDC_CONFIGURATION_URL=http://localhost:3007/idphub/b2c/b2c_1a_cui_cpdev_signupsigninsfi/.well-known/openid-configuration
DEFRA_ID_CLIENT_ID=8c5e0bd-8223-4908-a5aa-c9c1d7cddaac
DEFRA_ID_CLIENT_SECRET=test_value
DEFRA_ID_SERVICE_ID=aeaa0a80-15f3-48b2-8bd7-0e02874b3d32
DEFRA_ID_POLICY=b2c_1a_cui_cpdev_signupsigninsfi
Alternatively set STUB_MODE=true, which serves stub data and signs its own
session instead of doing the Defra ID OIDC exchange. Auth is still enforced —
only the external round-trip is bypassed. Unlike plants-frontend, this
prototype honours the switch in production too, by design (Sam's decision):
prototype-defaults.js turns it on unless it is already set, so the deployed
prototype signs in exactly the way npm run dev does. Set STUB_MODE=false
to restore plants-frontend's own Defra ID sign-in. The Playwright suite sets
STUB_MODE=true for its own web server, so npm run test:fit needs no other
service running.
Stub sign-in lets anyone in, so the prototype adds its own shared password in
front of every page when PROTOTYPE_PASSWORD is set (a CDP secret on the
deployed prototype; see "For maintainers" above). It sits before sign-in, so a
visitor gives the password first and then signs in as usual.
Tip
For Apple Silicon users, you may need to add --platform linux/amd64 to the
docker run command to ensure compatibility, for example
docker build --platform=linux/arm64 --no-cache --tag trade-imports-plants-prototype
Build:
docker build --target development --no-cache --tag trade-imports-plants-prototype:development .Run:
docker run -p 3103:3103 trade-imports-plants-prototype:developmentBuild:
docker build --no-cache --tag trade-imports-plants-prototype .Run:
docker run -p 3103:3103 trade-imports-plants-prototypeThis repository carries no compose file of its own, and the workspace
stack does not run this prototype: ./scripts/stack/run-stack.sh in
DEFRA/trade-imports-workspace
stands up MongoDB, Floci, Redis, the stubs and the real trade-imports
services, but never this one. This prototype always runs on its own
stubs, with npm run dev, whether or not the stack is running.
The header's "Address book" link is deliberately dead, stack running or not: the address book belongs to the Import Notification Service, which this prototype does not include, and there is no setting that points the link at a real one, local or deployed. It is never needed to see a change made here.
THIS INFORMATION IS LICENSED UNDER THE CONDITIONS OF THE OPEN GOVERNMENT LICENCE found at:
http://www.nationalarchives.gov.uk/doc/open-government-licence/version/3
The following attribution statement MUST be cited in your products and applications when using this information.
Contains public sector information licensed under the Open Government license v3