Grant Application Service defines and manages farming grants and applications. It is the source of truth for all grant applications and their status.
- Running with other services
- User guide
- Developer guide
- Local development
- Service to service authentication
- HTTP client and API examples
- Docker
- Licence
If you want to run this service with other farming grants applications see fg-grants-core
As part of the Grant Application Service, you can configure and call actions on grants.
Given a grant definition is stored in GAS as per the following example:
- Note the $areaId and $segmentId in the URL, these are placeholders that will be replaced with actual values when the action is invoked.
{
"code": "my-grant-code",
"actions": [
{
"name": "land-area-calculation",
"method": "POST",
"url": "https://my-other-server.%ENVIRONMENT%.gov.uk/some-path/area-calc/$areaId/$segmentId"
}
]
}Note: The %ENVIRONMENT% placeholder will be replaced with the current CDP environment when running in a CDP environment.
This can be called via the following HTTP request:
- Note the areaId and segmentId query parameters, these will be used to replace the placeholders in the URL.
- The anotherParam query parameter is an example of additional parameters will be passed on in the URL query string.
POST http://gas-server/grants/my-grant-code/actions/land-area-calculation/invoke?areaId=123&segmentId=456&anotherParam=ABC123
Content-Type: application/json
{
"someKey": "someValue"
}This wil result in the following HTTP request being made to the configured URL:
POST https://my-other-server/some-path/area-calc/123/456?code=my-grant-code&anotherParam=ABC123
Content-Type: application/json
{
"someKey": "someValue"
}Please install Node Version Manager nvm
To use the correct version of Node.js for this application, via nvm:
cd fg-gas-backend
nvm useInstall application dependencies:
npm installCreate a .env file in the root of the project. You can use the .env.example file as a template.
cp .env.example .envVIEW_AGREEMENT_URI is the Agreements UI base used in accepted Agreement
lifecycle events. Do not include a trailing slash; the Agreement Number is
appended when the event is created.
GAS__SNS__REPORTING_EVENTS_TOPIC_ARN is the Grants Reporting SNS topic. GAS
writes Agreement-created and Agreement-status-changed events to its transactional
outbox so they are committed atomically with the corresponding Agreement version.
AGREEMENTS_JWT_SECRET (FGP-1307) is the shared HS256 secret GAS uses to verify
the caller token (the x-encrypted-auth header) forwarded by Agreements UI on
the agreement routes. It must match the secret the producer services sign with
(Caseworking frontend, PDF service and Grants UI). It is supplied per
environment from the platform secret store and must never be committed. It is
optional while verification runs in warn-only mode; when absent the caller token
is reported as unverified but requests are not rejected. The producer issuers
permitted to mint caller tokens (grants-ui, fg-cw-frontend, agreements-pdf)
are a fixed code constant rather than configuration — the same list applies in
every environment, so there is no env var to set and no cdp-app-config entry,
and the allowlist can never be misconfigured to an empty "accept any issuer" list.
SQS and SNS are emulated by floci on localhost:4566,
replacing LocalStack. The rest of the stack (fg-grants-core, fg-cw-backend)
uses the same emulator.
Queues, topics and subscriptions are created by init scripts mounted into the
floci container. They run in lexical order by filename, pooled across every
repo that mounts one in:
| Script | Mounted by |
|---|---|
10-setup-resources.sh |
this repo (compose/floci/start.d/) |
99-ready.sh |
this repo — writes the /tmp/READY marker |
The container's healthcheck waits on /tmp/READY rather than on the gateway
responding, because floci starts serving HTTP before the init scripts have
finished. Services that depends_on floci with condition: service_healthy
therefore never start before their queues exist. If you add a script, give it a
numeric prefix below 99.
When gas is pulled into another stack that already runs floci, only
10-setup-resources.sh is mounted and the host stack owns the marker — see
compose/ext/compose.gas-ext.example.yml.
State is in-memory: every compose up recreates the resources from scratch.
The compat image ships the AWS CLI, so the quickest route needs nothing installed on the host:
docker compose exec floci awslocal sqs list-queues
docker compose exec floci awslocal sns list-topics
docker compose exec floci awslocal sqs receive-message \
--queue-url $(docker compose exec -T floci awslocal sqs get-queue-url \
--queue-name gas__sqs__update_status_fifo.fifo --output text)Note that queues dead-letter after a single failed receive by default, so
peeking at a queue can consume the message. Set MAX_READS=2 in
compose/aws.env and recreate the stack if you need headroom.
To run the application in development mode run:
npm run devTo run the unit tests:
npm run test:unitTo run the integration tests:
npm run test:integrationThis project uses Pact to verify consumer-provider contracts.
- To run the contract tests against the Pact broker:
npm run test:contractNote: This is normally used in CI, but if running locally it requires .env.test to be populated with the broker details and you must be connected to the Azure VPN.
- To run the contract tests locally against local pact files (helpful when developing without Azure VPN access or iterating quickly), use the provided script which sets
PACT_USE_LOCAL=true:
npm run test:contract:localWhen PACT_USE_LOCAL=true, tests will read pact files from tmp/pacts (for example tmp/pacts/grants-ui-fg-gas-backend.json).
The GAS API uses simple bearer tokens for service access. Tokens are UUIDv4 values whose SHA‑256 hash is stored in MongoDB in the access_tokens collection.
Clients must send the raw token in the Authorization header as Bearer <token>.
Deployed environments give nobody direct database access, so tokens for other
services are not inserted by hand. GAS seeds one itself on boot from
SERVICE_ACCESS_TOKEN_HASH, supplied by the platform's secret store, which makes
issuing and rotating a credential a secret change plus a redeploy.
The value is a single client:sha256hex pair:
some-service:4bb35ade...
Only the hash reaches GAS. The raw token lives solely in the calling service's
own secrets, so GAS cannot mint or impersonate the credentials it accepts - the
same reason access_tokens stores hashes in the first place. Neither value
belongs in a repository: this repo is public, and callers' repos may be too.
This is an instruction to issue, not a record of who holds a token:
- It affects only the client it names. Other services' tokens are never touched,
and neither are tokens minted by hand with
scripts/mint-access-token.js(reconciliation is scoped to records the seeder created). - Unset it and nothing happens. Clearing the secret revokes nobody, so it can be emptied once a credential has been issued.
- Onboard services one at a time: point it at the next client and redeploy. The previously issued token stays valid.
Seeding never stops GAS starting. A malformed value or a database failure is logged and skipped, leaving that client without a working token until the next deploy, rather than taking the service down for everyone. Check the logs after a deploy - a credential that silently never appeared looks exactly like one that was never set.
Run once per environment - never reuse a token across environments, or a dev credential authenticates against prod:
npm run token:new -- <client-name>Then in the CDP portal for that environment:
fg-gas-backend-> Secrets ->SERVICE_ACCESS_TOKEN_HASH= the printedclient:hashpair- Give the raw token to the calling service as its own secret
- Redeploy
fg-gas-backend, then the caller
Setting a new hash for a client that already has one rotates it: a single seeded record per client is enforced by a unique index, so the new token replaces the previous one on the next boot. Expect 401s between the two redeploys, so deploy GAS first.
Locally, set SERVICE_ACCESS_TOKEN_HASH in .env instead of the portal.
There is no revoke-by-omission - clearing the secret deliberately does nothing.
To cut a client off, rotate it to a freshly generated hash and discard the raw
token: the old token stops working on the next boot and nobody holds the new one.
Removing the record outright needs database access. Issuing the new hash and
revoking the old one is a single atomic step, so confirm the deploy logged
Seeded access token for <client> - a failed seed leaves the old token live.
The client name is the identity. Reconciliation is scoped to the client named in
the secret, so renaming one issues a second credential rather than rotating the
first, and the original stays valid indefinitely. Cut the old name off the same
way - point the secret at <old-name>:<fresh hash>, redeploy, and discard that
token - before switching to the new name.
Check the GAS startup logs, which will show one of:
| Log line | Meaning |
|---|---|
Seeded access token for <client> |
Issued. A , replacing the previous one suffix means the hash changed; without it, the credential was already in place. |
SERVICE_ACCESS_TOKEN_HASH is not a client:sha256hex pair - nothing seeded |
The value is malformed. Nothing was issued or removed. |
Failed to seed access token for <client> |
The database write failed. GAS started anyway; retry by redeploying. |
| nothing | The secret is empty or unset, so there was nothing to do. |
Then confirm the raw token is accepted:
curl -s -o /dev/null -w '%{http_code}\n' \
-H "Authorization: Bearer <raw token>" \
https://fg-gas-backend.<env>.cdp-int.defra.cloud/grantsThere is a helper script to mint access tokens and optionally write them to MongoDB:
Use scripts/mint-access-token.js to mint a token and, if MONGO_URI and MONGO_DATABASE are set, write the hashed token to Mongo.
Usage:
# Mint and write hashed token to Mongo, print raw token to console
node --env-file=.env scripts/mint-access-token.js [clientName] [expiresISO]# Mint and print hashed token and raw token to console
node scripts/mint-access-token.js [clientName] [expiresISO]clientNamedefaults togrants-ui.expiresISOis optional (e.g.2099-01-01T00:00:00Z).
The script prints:
- A confirmation message and details.
- The hashed token if
MONGO_URIandMONGO_DATABASEare not set. - The raw bearer token on the last line – copy and store it securely. It will not be shown again.
There is also a convenience script to mint and write the raw token to http-client.private.env.json to allow for local testing with api.http:
npm run token:localThis runs: node --env-file=.env scripts/write-http-client-token.js local http-client 2099-01-01T00:00:00Z.
Which uses scripts/write-http-client-token.js to mint a token, store in the local MongoDB and write the raw value into http-client.private.env.json under the chosen environment key, so api.http can use it immediately.
MONGO_URI="mongodb://localhost:27017" MONGO_DATABASE=fg-gas-backend node scripts/mint-access-token.js
When using grants core with just the case working and gas dev mode the mongo db is available on localhost and does not use a direct connection.
If you're using grants core with external apps (grants-ui for example) then the db connection is through the docker network and direct connection is required. The env-file option won't work here. You have to manually add MONGO_URI and MONGO_DATABASE to the command - see "run with manual vars"
General script usage for any environment if needed:
node --env-file=.env scripts/write-http-client-token.js <envName> [clientName] [expiresISO]Examples:
# Local environment – also inserts the hash into Mongo (requires MONGO_URI/MONGO_DATABASE)
node --env-file=.env scripts/write-http-client-token.js local http-client 2099-01-01T00:00:00Z# Non-local environment – only updates http-client.private.env.json
node scripts/write-http-client-token.js dev grants-ui 2026-01-01T00:00:00ZAfter running, you should see output similar to:
Updated http-client.private.env.json -> [local].serviceToken
Notes and requirements:
- For local usage, ensure your
.envprovidesMONGO_URIandMONGO_DATABASEso the hashed token can be stored in MongoDB. http-client.private.env.jsonis git‑ignored by default. Keep tokens out of source control.
This repository includes api.http which you can run directly from JetBrains IDEs (HTTP Client) or VS Code (REST Client extension).
- Base URLs are defined in
http-client.env.json(checked in). - Private secrets (like the
serviceToken) are stored inhttp-client.private.env.json(git‑ignored). Do not commit this file.
api.http uses these variables:
{{base}}– selected environment base URL (for examplehttp://localhost:3000).{{serviceToken}}– a bearer token used by the API for service‑to‑service auth.
To run the requests:
- Ensure the API is running locally (see Development section), or pick a non‑local environment.
- Make sure
http-client.private.env.jsonhas aserviceTokenfor the environment you want to call. You can generate/populate this token using the scripts above. - Open
api.http, select the desired environment from the drop‑down (e.g.local) and send requests.
GAS exposes two QA-only endpoints for the agreement journey, accessibility and performance suites:
POST /api/test/agreements— create a GAS-managed Agreement, returns201withagreementData.agreementNumber.POST /api/test/agreements/{agreementNumber}/status— apply awithdrawn,cancelledorterminatedtransition, returns200with the updated Agreement.
Both are registered only when ENABLE_TEST_ENDPOINTS=true (default false), enabled in dev, test, ext-test and perf-test and never in production. They call the same agreement command handlers as normal processing and add no queues.
Full request and response schemas, status codes and the QA repositories that must migrate off the legacy Agreements API queue-message endpoint are documented in docs/TEST_ENDPOINTS.md.
Launch GAS and dependencies via Docker Compose:
docker compose up --watchRoutes can access use cases and schemas. Subscriptions can access use cases. Use cases can access repositories, http clients, domain classes and other use cases. Use cases should export a single function. Repositories can access db.
src/grants/services/ contains both stateless helpers and transactional application services. The application services coordinate repositories and domain objects for a complete operation, own their Mongo transaction, and pass its session to every participating repository call. entitlement.service.js and claims.service.js are the Grants entry points for the Grant Admin inbound adapter. Services may import domain models when coordinating those operations; domain models must not import services.
Cross-module data is obtained through a documented integration seam. In particular, Grants can use the reviewed Agreements reference-context query, which returns a plain context using the active Mongo session. It must not import an Agreements repository or domain model.
Routes and subscriptions should never respond with a domain object. Domain objects should never access use cases, repositories or subscriptions. Repositories should never accept or return db records.
This application uses Pino for structured logging, configured with ECS (Elastic Common Schema) formatting for better observability and log analysis.
Logging is configured in src/common/logger.js.
We use paired entry and exit logging patterns for better log correlation.
Entry logs indicate the start of an operation:
logger.info(
`Adding agreement ${agreementNumber} for application ${clientRef} with code ${code} on ${date}`,
);Exit logs indicate the completion of an operation:
logger.info(
`Finished: Adding agreement ${agreementNumber} for application ${clientRef} with code ${code}`,
);Note: We use consistent entry text to make it easier to correlate logs within OpenSearch.
For operations that have conditional logic between entry and exit logs, use logger.debug() or logger.info() based on relevance:
logger.debug(
`Application updated with agreement for ${clientRef} with code ${code}`,
);Example implementation: See src/grants/use-cases/add-agreement.use-case.js
Warning logs for recoverable issues:
Debug logs for detailed diagnostic information:
Note: Error logging (
logger.error) is typically not required in use cases as errors are thrown and will propagate up the call stack where they can be handled and logged by the error handling middleware.
- Include relevant identifiers (IDs, codes, references) in log messages when they must be searchable in OpenSearch
- CDP only indexes the supported ECS field subset, so unknown context-object properties are not available in OpenSearch
- Use context objects only for supported ECS fields such as
event.*anderror.*, or for diagnostics that do not need downstream search - Keep entry and exit log messages consistent for easier correlation
- Use appropriate log levels based on the importance of the information
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
The Open Government Licence (OGL) was developed by the Controller of Her Majesty's Stationery Office (HMSO) to enable information providers in the public sector to license the use and re-use of their information under a common open licence.
It is designed to encourage use and re-use of information freely and flexibly, with only a few conditions.