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 ·
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.jsoncIn 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.
| Requirement | Choice |
|---|---|
| No-code, statically buildable start page | Build 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 deployment | This 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 TTLSubrequests 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
| Symptom | Check first | Recovery |
|---|---|---|
401 unauthorized | .dev.vars and the Authorization header match | Recopy the local template; never commit the value |
400 invalid_url | url starts with http:// or https:// | Fix the full URL and retry |
Every health check is down | Whether targets allow Cloudflare edge sources | Allow the sources, or put internal apps behind a Tunnel |
| Status does not refresh after reload | Whether you are inside the 60 second cache window | Wait 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.