ideas.
June 29, 2026 4 min read webteamdev-tools

Self-hosted internal dev portal for engineering teams

A YAML-configured single-page app that lists all your team's internal services with live status badges — a shared browser start page with no database and no account required.

The idea

A static-ish single-page app you self-host that renders a grid of cards — one per internal service. Each card shows the service name, a description, a link, and an optional live status dot fetched from your uptime monitor. Config is a YAML file checked into your repo. No database, no user accounts, no login screen.

The pitch to your team: bookmark this URL. It is the one place that links to every staging environment, every runbook, every dashboard, and every on-call tool your team uses. Most teams build this by hand as a Confluence page or a pinned Notion doc; this is a proper self-hosted alternative that stays in sync because the config lives in Git.

Why build this

Engineering teams accumulate internal services fast: staging environments, Grafana dashboards, Datadog, the Jira board, the incident runbook, the deploy pipeline, the status page, the internal admin panel. The links to these things end up scattered across Slack channels, old Notion pages, and individual bookmarks. New engineers waste hours during onboarding hunting for them.

Every medium-sized team eventually builds some version of an internal linktree and then lets it rot. Keeping it accurate requires updating a Confluence page that nobody remembers the URL to. Checking it into Git means changes go through PR review and the history is auditable — the same habits that keep the rest of your infrastructure sane apply to the homepage too.

The status dot is the feature that makes it more than a fancy bookmark list. A quick visual scan of the homepage tells you at a glance whether staging is up, whether the webhook endpoint is responding, whether the internal API is healthy. That replaces refreshing six browser tabs after a deploy.

Stack sketch

  • Framework: Astro deployed as a Docker image; pages render server-side on each request so status dots are live, not cached from a previous build
  • Config: A single services.yaml at the repo root — list of entries with name, url, description, category, and an optional health_check_url; optionally a team block with the PagerDuty or OpsGenie on-call API endpoint
  • Status checks: At request time, the server issues concurrent HEAD requests to each configured health_check_url with a 2-second timeout; 200 = green, timeout or non-2xx = red, no URL = grey
  • UI: Vanilla CSS grid, no JavaScript framework; entries grouped by category (e.g., "Environments", "Observability", "Docs", "Admin"); dark mode via prefers-color-scheme
  • Deployment: Single docker run or a Compose service behind Traefik; the YAML is bind-mounted so adding an entry requires only editing the file, not rebuilding the image
  • Auth: Optional HTTP Basic Auth via a Traefik middleware — the portal is internal only, so Basic Auth is a lightweight gate for teams that do not run a full SSO proxy

Scope for v1

  • YAML schema with name, URL, description, category, and optional health check URL per service
  • Docker image that reads the config on each request — no restart needed after adding an entry
  • Concurrent HEAD-based health checks with a 2-second timeout; results passed as server-side props to the template
  • Category grouping in the rendered grid, alphabetically sorted within each group
  • A five-minute setup guide in the README: clone the repo, fill in services.yaml, docker run, done
  • Deliberately out of scope: user-specific pinned bookmarks, a web editor for the YAML, per-service permissions, on-call rotation display, response-time sparklines, historical uptime graphs

Where it could go

The most-requested second feature will be search. Once a team has 30 or 40 entries in the config, a filter box that hides non-matching cards as you type is the difference between a useful tool and a slow directory. The implementation is a small Alpine.js snippet on the page; no server changes are required.

The second natural expansion is live on-call attribution. A sidebar card showing the current on-call engineer's name, fetched from the PagerDuty or OpsGenie schedules API with a 60-second server-side cache, answers the question every engineer asks on a Friday afternoon: who do I ping if this breaks? That API call is one authenticated GET per schedule. Combined with the service grid, it gives new or on-call engineers a complete picture of the team's running infrastructure from a single tab.

Watch out for

Issuing HEAD requests to every service on each page load adds latency proportional to the number of entries and the slowness of the laggiest one. Even with parallel fetches and a 2-second cap, a portal with 40 services where two are degraded will block the render noticeably. A better approach: a background goroutine or Astro server action that polls all health endpoints on a 30-second interval and stores results in memory; the page request reads the cached state and returns immediately. Status dots are then 30 seconds stale at worst, and the page loads in under 100 ms regardless of service health.