Skip to content

Architecture

stackvitals/
src/
collectors/
providers/ # Pure adapter logic per provider
liveClients/ # Real API calls, isolated from adapters
stores/ # DB write layer (Supabase recorder)
services/ # Frontend data-fetching and aggregation
lib/ # Supabase client setup
tests/ # Mirrors source tree
supabase/
migrations/ # Applied in numeric order

Supabase is the backend database and auth layer. The frontend is a static React/Vite app deployed on any static host. There are no always-on servers — collection runs on a schedule plus manual refresh.

Everything flows through the ProviderAdapter contract in src/collectors/types.ts. Each provider implements collect(context) → CollectorAdapterResult, returning a uniform shape: resources, metrics, costs, healthChecks, errors, plus a status.

  • runCollectors.ts runs adapters sequentially, catches errors (converting them to a failed result rather than aborting), optionally hands each result to a CollectorRunRecorder, and derives an overall run status.
  • runConfiguredCollectors.ts is the entry point. It reads env vars and projects.config.json, then conditionally assembles the adapter list — an adapter is only added if its credentials are present.
  • stores/supabaseCollectorRunRecorder.ts maps results into the DB tables, resolving provider/project slugs to UUIDs with an in-memory cache.

Dependency injection is the testing seam: adapters receive a client interface, real implementations live in liveClients/, and tests inject fakes.

src/services/dashboardData.ts is the heart of the read side. fetchDashboardData(client) issues parallel Supabase selects, then does client-side aggregation:

  • Dedup-to-latest per key
  • Month-to-date vs. last-month cost bounds
  • OpenAI usage roll-ups
  • GitHub Actions summaries
  • Per-project provider status
  • Collector-error scoping (errors are suppressed once a newer successful run exists)

The raw DB snake_case row shapes are defined in dashboardData.ts; the app-facing camelCase types live in src/types.ts. App.tsx renders the returned DashboardData across tabs.

Supabase Postgres, schema in supabase/migrations/*.sql. Core tables:

TablePurpose
projectsOne row per tracked app, keyed by a free-form slug
providersProvider registry (aws, amplify, supabase, openai, etc.)
resourcesDeployments, domains, databases, API accounts, and other provider resources
metric_snapshotsStatus, counts, usage, latency, deploy state over time
cost_snapshotsDaily or monthly cost by provider/service; account-level by default
health_checksUptime, HTTP status, response time, last successful check
collector_runsAudit trail for each collection run, including errors

Snapshots are append-only — the read layer picks the latest per logical key rather than updating in place.

  • Only aggregate operational data is stored (status, counts, durations, costs).
  • Never copy raw user data, request payloads, message bodies, or table dumps from watched apps.
  • For watched-app databases, use count-only RPCs or aggregate views only.
  • Keep the hub’s Supabase credentials separate from every watched app’s.
  • Prefer read-only provider credentials for collectors.
  • Collector/service secrets never reach the frontend (only VITE_* env is exposed to the browser).
  • Access is gated twice: frontend email allowlist + Supabase RLS.
  • Track apps where they already run — the dashboard adapts, it doesn’t require migration.
  • Use daily collection frequency before adding deeper monitoring.
  • Avoid always-on infrastructure or paid monitoring services.
  • Keep provider costs account-level rather than guessing per-project splits.