Orange Book
End-to-end projects

Project ten, an app dashboard (Heimdall-style)

Build a minimal, controllable app dashboard with a Worker rendering tiles, KV holding the manifest, and parallel subrequest health checks.

Edited and verified by Orange Book Editorial Team ·

PROJECT 10INTERMEDIATEabout 60 minutesOutcome: a Worker-rendered minimal app dashboard

Completion criteria

Opening / shows the tile grid; GET /api/health probes every application in parallel and aggregates up/down; results are cached in KV for 60 seconds so repeated page loads do not hammer your backends; only requests holding ADMIN_KEY can add or remove applications.

Prerequisites and companion example

You need Node.js 22, pnpm, and the repository dependencies. The example lives in examples/app-dashboard and uses local KV. Copy the local secret, then start the Worker:

cp examples/app-dashboard/.dev.vars.example examples/app-dashboard/.dev.vars
pnpm wrangler dev --config examples/app-dashboard/wrangler.jsonc

In another terminal, run node --test examples/app-dashboard/test/index.test.mjs; six tests should pass. Add two applications with the README curl commands and expect status 201; opening http://127.0.0.1:8787/ shows the tile grid; /api/health returns up or down per application. A management request without an Authorization header must return 401; a non-http(s) url must return 400 invalid_url.

Example authentication boundary

ADMIN_KEY only demonstrates server-side authorization, and the management API targets server-side scripts. Never ship the key to browser code in a public deployment; a team-facing dashboard should use Cloudflare Access or a real session instead.

Clarify first: Heimdall cannot run on Cloudflare

The Heimdall that user communities refer to is linuxserver/Heimdall (9000+ stars). It is a PHP/Laravel application distributed for Docker/VPS only, and it cannot run on Cloudflare Workers or Pages; the Cloudflare ecosystem currently has no mature Heimdall replacement either. If you want a no-code option, build gethomepage/homepage (32000+ stars) into a fully static site and host it on Cloudflare Pages. This project demonstrates the third path: write a minimal, fully controllable dashboard yourself with Workers + KV, in exchange for full control over authentication, health-check logic, and boundaries.

RequirementChoice
No-code, statically buildable start pageBuild gethomepage/homepage and host it on Pages
Full Heimdall features (app awareness, enhanced tiles)Keep it on Docker/VPS; do not force a migration
Controlled auth, custom health checks, edge deploymentThis project: Worker rendering plus a KV manifest

Design: Worker-rendered tiles plus a KV manifest

A dashboard does not need a framework. One Worker reads the apps key from KV on GET / (a JSON array with id, name, description, url, healthUrl per entry) and renders the tile grid on the server: each tile shows the name, the description, and the first letter of the name as the icon, and the whole tile links to the application. Every user-controlled field is HTML-escaped before rendering, and url is restricted to http/https on write, so pseudo-protocols such as javascript: never reach an href.

const apps = await env.DASHBOARD_KV.get('apps', 'json');
const tiles = (apps ?? []).map((app) => `
  <a class='tile' href='${escapeHtml(app.url)}' rel='noopener noreferrer'>
    <span class="icon">${escapeHtml(app.name[0].toUpperCase())}</span>
    <span class="name">${escapeHtml(app.name)}</span>
  </a>`);

KV fits this manifest: read-heavy, small values, and second-level eventual consistency is acceptable. Cap the application count (50 in the example) so the list cannot grow without bound and slow rendering.

Health checks: parallel subrequests with a 60 second cache

GET /api/health issues a Worker subrequest against each application's healthUrl. Promise.allSettled runs them in parallel so a single failure cannot drag down the whole check; AbortSignal.timeout(3000) bounds every probe at three seconds, and timeouts and connection refusals both map to down. Results are written to KV keyed by health URL with expirationTtl: 60; cache hits return immediately without any subrequest:

const settled = await Promise.allSettled(
  misses.map((app) => fetchImpl(app.healthUrl, { signal: AbortSignal.timeout(3000) })),
);
// Cached apps are skipped entirely; probe results for misses are written back to KV with a 60 second TTL

Subrequests originate from the network edge

Worker subrequests come from Cloudflare edge IPs, not from your office or home network. Public services you monitor must allow these sources in their firewall or WAF; applications reachable only on a private network need a Tunnel. See protect private service access with a Tunnel.

Management API and input validation

POST /api/apps and DELETE /api/apps/<id> require Authorization: Bearer <ADMIN_KEY>; both sides are SHA-256 hashed and compared byte-by-byte so timing differences do not leak the secret. New applications are validated: name is 1 to 60 characters, description is at most 160 characters, url must be a valid http/https URL, and healthUrl is optional and falls back to url. A body over 4KB returns 413, a non-JSON body returns 415, and a full manifest returns 409 app_limit_reached.

Verification and troubleshooting

SymptomCheck firstRecovery
401 unauthorized.dev.vars and the Authorization header matchRecopy the local template; never commit the value
400 invalid_urlurl starts with http:// or https://Fix the full URL and retry
Every health check is downWhether targets allow Cloudflare edge sourcesAllow the sources, or put internal apps behind a Tunnel
Status does not refresh after reloadWhether you are inside the 60 second cache windowWait for the TTL, or delete the health: prefixed KV keys

Remote boundary and rollback

Creating a remote KV namespace, writing a production secret, or deploying the Worker changes Cloudflare state; this tutorial does not run those actions. On rollback, remember that reverting code does not remove the application manifest or the health cache already written to KV. The health: keys expire on their own after the 60 second TTL, but the apps manifest must be removed explicitly through the management API or wrangler kv key delete. If a new health-check revision misjudges targets and triggers alert noise, roll back to the stable code first, then audit any stale cache keys left in KV.

Next: choose the right storage service by data shape.

Primary sources

Did this page help you complete your goal?

Beta feedback is generated in this browser and is never uploaded automatically.

On this page