Skip to content

GavT/fueller-backend

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

21 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Fueller backend

A Ktor server that serves UK petrol-station and fuel-price data to the Fueller Android app.

Deployment modes

The backend can populate its in-memory cache in one of two ways. The /api/search contract served to mobile clients is identical in both.

Pull mode (default)

The backend periodically calls the UK government Fuel Finder API directly, using OAuth2 client credentials. Used in development and once the deployed backend's egress IP is whitelisted by the Fuel Finder team.

Required configuration:

  • FUEL_FINDER_MODE=pull (or unset — pull is the default)
  • FUEL_FINDER_CLIENT_ID
  • FUEL_FINDER_CLIENT_SECRET

Optional:

  • fuelfinder.baseUrl (default: https://stg.fuel-finder.ics.gov.uk)
  • fuelfinder.priceRefreshMinutes (default: 30)
  • fuelfinder.stationRefreshHours (default: 24)
  • fuelfinder.staleAfterMinutes (default: 90)

Push mode

A separate ingester (typically a script on a whitelisted laptop or a CI job) fetches data from Fuel Finder and POSTs it to the backend via POST /admin/ingest. The backend stores no Fuel Finder credentials in this mode.

Required configuration:

  • FUEL_FINDER_MODE=push
  • INGEST_TOKEN (32+ bytes random; shared with the ingester)

Optional:

  • ingest.maxBodyBytes (default: 33,554,432 — 32 MiB)
  • fuelfinder.staleAfterMinutes (default: 90)

The first /api/search request after startup returns 503 until the first ingest lands. If no ingest arrives for longer than the stale threshold, /api/search returns 503 again until the cache is refreshed.

Endpoints

GET /api/search?postcode=SW1A1AA&radius=5

Public endpoint consumed by the Android app. Identical contract in both modes.

Returns 503 if the cache is empty (dataLoaded = false) or stale (isStale = true).

GET /api/health

{
  "status": "ok",
  "mode": "push",
  "dataLoaded": true,
  "isStale": false,
  "lastPriceRefresh": "2026-04-29T20:00:00Z",
  "nextPriceRefresh": "2026-04-29T20:30:00Z",
  "stationCount": 8421,
  "priceCount": 8237,
  "ingestSource": "laptop-push",
  "ingesterVersion": "1.0.0"
}

External monitoring should poll this endpoint and alert when isStale: true or non-200.

POST /admin/ingest (push mode only)

POST /admin/ingest
Content-Type: application/json
X-Ingest-Token: <secret>

{
  "fetched_at": "2026-04-29T20:00:00Z",
  "ingester_version": "1.0.0",
  "stations": [ <Fuel Finder station objects, passthrough> ],
  "prices":   [ <Fuel Finder price-record objects, passthrough> ]
}
Status Meaning
200 Accepted, cache swapped atomically
400 Malformed JSON, missing fields, empty arrays, or unparseable fetched_at
401 Missing or wrong X-Ingest-Token
409 fetched_at is older than the currently cached snapshot (out-of-order)
413 Payload exceeds ingest.maxBodyBytes
503 Server is in pull mode

Building

cd backend
./gradlew build               # compile + tests
./gradlew shadowJar           # produces backend/build/libs/fueller-backend.jar
./gradlew ebBundle            # produces backend/build/eb/fueller-backend-eb.zip (for EB deploys)

Deploying to Elastic Beanstalk

The backend runs on a single-instance Elastic Beanstalk environment in eu-west-1 (fueller-backend-env). TLS for api.fueller.app is terminated by nginx on the EC2 instance using a Let's Encrypt cert.

Deploy with the EB bundle, not a bare JAR. Uploading a bare JAR works for the Java runtime, but it bypasses the .platform/nginx/conf.d/ config in this repo — which means EB regenerates /etc/nginx/ from defaults on each deploy and silently strips the HTTPS server block. Always use the ZIP produced by ebBundle:

cd backend
./gradlew ebBundle
# Upload backend/build/eb/fueller-backend-eb.zip via:
#   - AWS Console: EB → environment → Upload and deploy
#   - or: eb deploy (if EB CLI is configured for this project)

The bundle contains:

File Purpose
fueller-backend.jar The Ktor server (shadowJar output)
Procfile Tells EB how to run the JAR (web: java -jar fueller-backend.jar)
.platform/nginx/conf.d/00-https.conf nginx HTTPS server block + HTTP→HTTPS redirect for api.fueller.app
.platform/hooks/predeploy/01-tls-precheck.sh Gates the deploy on the TLS cert existing at /etc/letsencrypt/live/api.fueller.app/; fails the deploy with a clear recovery message if the cert is missing (e.g. fresh EC2 instance)

The cert itself lives in /etc/letsencrypt/ on the instance, which EB does not touch — so the cert survives deploys, and only the nginx wiring to use it needs to be regenerated, which is exactly what .platform/nginx/conf.d/ does.

Cert auto-renewal runs on certbot's systemd timer independently of EB.

Fresh-instance bootstrap

If EB ever replaces the underlying EC2 instance (platform updates, manual rebuild env, autoscaling event), /etc/letsencrypt/ will be empty on the new host and the predeploy precheck will fail with the recovery procedure. Briefly:

  1. Add an SSH (port 22) inbound rule to the env's security group, source: your laptop's IP
  2. EC2 Instance Connect into the new instance (AWS Console: EC2 → Instances → Connect)
  3. sudo dnf install -y certbot python3-certbot-nginx
  4. sudo certbot --nginx -d api.fueller.app --non-interactive --agree-tos --email gav_h_thomas@yahoo.co.uk --redirect
  5. Lock port 22 back down
  6. Re-trigger the deploy — the .platform/ config now has its cert and the precheck passes

Running locally

Pull mode (development):

export FUEL_FINDER_MODE=pull
export FUEL_FINDER_CLIENT_ID=...
export FUEL_FINDER_CLIENT_SECRET=...
java -jar backend/build/libs/fueller-backend.jar

Push mode:

export FUEL_FINDER_MODE=push
export INGEST_TOKEN=$(openssl rand -base64 32 | tr -d '=' | tr '/+' '_-')
java -jar backend/build/libs/fueller-backend.jar

See Fueller_LaptopIngest_Design.md in the workspace folder for the full design rationale and the laptop-side ingester spec.

About

Hosted backend for Fueller app

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors