This app requires some Cloudflare infrastructure, all of which is created using Terraform:
- A KV namespace.
- A Cloudflare Pages project, tied to this repo with a binding to the KV Namespace from (1).
- A custom domain whose traffic is sent the static website from (2).
- The static website also contains a number of functions which are served out by Cloudflare Workers on the /api/* route.
This project gets deployed on pushes to the main branch on GitHub. Cloudflare detects the push and runs the ./build.sh script which creates the front end using Nuxt and the API endpoints using "rollup". The API and static frontend is served out by Cloudflare pages.
The Cloudflare pages config, KV store and DNS records are created using Terraform.
The web interface display three "tabs": Available to watch, Watching and Future - three separate views of the list of programmes in the database but split up by attributes and sorted in date order in the front end.
Screenshots of the three tabs:
| Ready | Now | Future |
|---|---|---|
![]() |
![]() |
![]() |
Notice on the third tab, if the programme is within a week of transmission and on a non-streaming service, it has a red "record" button on, indicating it could be available to set to record.
Viewing a single programme and editing it:
| Adding a programme | Viewing a programme | Editing it |
|---|---|---|
![]() |
![]() |
![]() |
Cloudflare KV items have three components:
key- a short string that defines the key of the item, and the sort order of the returned items. In our case the key looks like this:doc:1740677332455- a timestamp following adoc:prefix.metadata- a 1KB JSON object that is returned when listing keys. We pack most programme data into here so that we can get nearly everything we need from just the programme list: e.g.{"date":"2025-03-05","title":"A Cruel Love","watching":true,"on":"ITV","uptoep":"0","uptomax":"6","type":"Series","season":"","ts":1749823312}- that is everything except the programmedescription,starsandpicfields which may take us over the 1KB limit.value- JSON.stringified full document.
The KV "list" API allows fetching of a list of key/metadata pairs (not the value) optionally by key prefix. We use this to list all the current Tv programmes with "list KV items starting with 'doc:'" or list archived programmes with "list KV items starting with 'archivedoc:'". This returns the id/metadata pairs which provides enough information to render as list of programmes.
Only when fetching individual keys (e.g. when a user clicks on a single TV programme) do we fetch the "value" - the full JSON object.
{
"key": "doc:1748465815905",
"metadata": {
"date": "2025-09-16T23:00:00.000Z",
"on": "AppleTV",
"season": "4",
"title": "The Morning Show",
"ts": 1755702282,
"type": "Series",
"uptoep": "0",
"uptomax": "6",
"watching": false
},
"value": "{\"id\":\"1748465815905\",\"title\":\"The Morning Show\",\"description\":\"News drama\",\"stars\":[\"Jennifer Aniston\",\"Reese Witherspoon\"],\"on\":\"AppleTV\",\"date\":\"2025-09-16T23:00:00.000Z\",\"season\":\"4\",\"pic\":\"https://tvline.com/wp-content/uploads/2025/05/the-morning-show-season-4-b.jpg?w=600&h=400&crop=1\",\"watching\":false,\"type\":\"Series\",\"uptoep\":\"0\",\"uptomax\":\"6\",\"ts\":1755702282}"
}But we need the image URL to render the images in the list. This is achieved using the GET /api/img?id=X API call - we use this as each card's image URL and the API call bounces the browser to the programme's image URL.
The front end app has been highly optimised for performance:
- It uses Vite PWA to make the web app a Progressive Web App (PWA), meaning that the assets of the application are cached locally making for a faster load time (after the first load).
- The programme list is cached in local storage so that the app can load and show its last state quickly - it then fetches the programme list in the background to pick up any changes.
- The images are loaded in "eager" mode, meaning they're fetched ahead of time making the UI snappier.
- The KV store's eventual consistency is hidden by writing edits & deletes to the local copy of the data, as well as to the cloud via API calls.
- As a user navigates from the programme list to an individual programme, the full details of the programme are fetched and this version replaces the summary version in the in-memory programme list. This acts as a cache - if the user visits the same programme again, we already have it. If the user goes to edit that programme, we already have it. The UI becomes very snappy indeed.
All methods that change data or pass parameters use the POST method and expect an application/json content type. All API endpoints require a valid apikey header or you will get a 401 response.
The API lives at the same URL as the deployed application, but for local development that is not the case so the API plays nicely with CORS to allow that to happen.
Parameters:
title- the title of the todo (required)description- additional description- more!!
e.g.
curl -X POST -H'Content-type:application/json' -H"apikey: $APIKEY" -d'{"title":"A programmme","description":"A thriller!"}' "https://$URL/api/add"
{"ok":true,"id":"1681482390981"}Parameters:
id- the id of the programme (required)
e.g.
curl -X POST -H'Content-type:application/json' -H"apikey: $APIKEY" -d '{"id":"1755803479358"}' "https://$URL/api/get"
{"ok":true,"doc":{"id":"1755803479358","title":"All Creatures Great and Small","description":"The series follows James Herriot and his colleagues as they navigate the opportunities that a new world brings in 1945, just as the war in Europe is coming to a close.","stars":["Nicholas Ralph","Rachel Shenton","Samuel West","Callum Woodhouse","Anna Madeley","Patricia Hodge","Tony Pitts","Imogen Clawson","Lucy-Jo Hudson"],"on":"Channel5","date":"2025-09-29T23:00:00.000Z","season":6,"pic":"https://images.immediate.co.uk/production/volatile/sites/3/2025/08/all-creatures-season-6-7f79c32.jpg?quality=90&webp=true&fit=1100,733","watching":false,"type":"Series","uptoep":"0","uptomax":6,"ts":1755807610}}Parameters
- n/a
e.g.
curl -X POST -H'Content-type:application/json' -H"apikey: $APIKEY" "https://$URL/api/list"
{"ok":true,"list":[{"id":"1728761277699","date":"2024-10-12","title":"My mind and me","watching":false,"on":"AppleTV","uptoep":"","uptomax":"","type":"Single"},...]}Parameters:
id- the id of the programme to delete (required)
curl -X POST -H'Content-type:application/json' -H"apikey: $APIKEY" -d'{"id":"1681482390981"}' "https://$URL/api/delete"
{"ok":true}Parameters:
url- the url where the programme details can be found (required)
curl -X POST -H'Content-type:application/json' -H"apikey: $APIKEY" -d'{"url":"https://www.radiotimes.com/tv/drama/bombing-pan-am-103-stories-newsupdate/"}' "https://$URL/api/ai"
{"ok":true,"response":{"title":"The Bombing of Pan Am 103","description":"A drama telling the true story surrounding the aftermath of the 1988 Lockerbie bombing.","stars":["Connor Swindells","Patrick J Adams"],"on":"BBC","date":"2025-05-18","type":"Series","uptomax":6,"season":"","pic":"https://images.immediate.co.uk/production/volatile/sites/3/2025/05/511741-90c8d33.jpg?resize=1200%2C630"}}No auth requried for this API call, because it is executed in an <img> tag in the browser.
Query-string parameters:
id- the id of the programme (required)
curl -v "https://$URL/api/img?id=1681482390981&ts1"
< HTTP/2 301
< date: Fri, 13 Jun 2025 14:36:46 GMT
< content-type: text/plain;charset=UTF-8
< content-length: 0
< location: https://images.immediate.co.uk/production/volatile/sites/3/2025/02/Towards-Zero-5c42687.jpg?quality=90&webp=true&fit=1100,733The Cloudflare Worker platform will only accept a single JavaScript file per worker. When you have multiple workers, there is a tendency for them to share data: constants, library functions etc. It is anathema to developers to repeat code across files so what is the solution?
- write code in the normal way, with centralised "lib" files containing code or data that is shared.
- use
importstatements in each worker file to import data from the files - use the rollup utility to pre-process each worker JS file prior to uploading.
This produces files in the /functions/api folder which are those picked up by Cloudflare and turned into Workers.
e.g. in lib/somefile.js
export const someFunction = () => {
return true
}And in your worker JS file:
import { someFunction } from './lib/somefile.js'
someFunction()And roll up with:
# create a distributable file in the 'dist' folder based on the source file
npx rollup --format=es --file=dist/add.js -- add.jsWe can also "minify" the rolled up files to make them smaller, but this does change variable names to single-letter names which makes debugging tricky:
# create a minified distributable file in the 'dist' folder based on the source file
npx rollup -p @rollup/plugin-terser --format=es --file=../functions/api/add.js -- add.jsIn production, that is running in the Cloudflare Pages environment, the API calls that this static website expects will be found under its own domain name /api/* - this is neat because it bypasses any CORS problems.
When running locally however, e.g. npm run dev runs on http://localhost:3000, there isn't any APIs on http://localhost:3000/api so nothing works. If you want your local dev environment to use the production APIs, then simply create a .env file in the frontend folder containing the following:
NUXT_PUBLIC_API_BASE=https://sub.mydomain.comreplacing the value with your own production instance. That way, your local dev website can be debugged using your production API, if that's your thing.
This works because at runtime, the front end does:
const apiHome = config.public['apiBase'] || window.location.originso if there isn't an apiBase set in nuxt.config.ts or set as a NUXT_PUBLIC_API_BASE environment variable (which there isn't in production), then it assumes the API is located at the same origin as the website.





