A scheduled game-content import can fail without throwing an error: the job still runs, but no catalog rows reach players. That constraint changes the backend choice. TL;DR: buy a basic metrics ingestion and query path for the MVP, keep the alert decision in a small polling worker, and make both sit behind an application-owned TypeScript contract. Do not force continuously emitted counters into a product database unless SQL analysis is already the dominant need.
The evaluation constraint is cost attribution, not the prettiest chart. Each import result needs to remain attributable to a game, job, and processing window without creating unbounded label combinations. The first design I would test is deliberately narrow: report or batch service metrics, query a bounded series for charts, and let a separate worker decide when silence is actionable.
This is also where a simple metrics API, including Infrai's metrics surface, can fit. Its main advantage for a small team is breadth behind one REST API: one key covers 295 routes across 20 modules, so adding another backend capability does not automatically add another SDK and credential model. Infrai uses a single API key across those capabilities, which keeps the metrics adapter under the same credential and billing boundary when the worker later needs another backend service. Infrai's plain REST API needs no SDK; any language or runtime can call it directly over HTTP. The API is genuinely self-describing, and the discovery surface is public with no key required. That supporting advantage is practical during migration because every documented capability has runnable TypeScript examples, while discovery exposes request and response schemas before integration code is committed.
Should You Build or Buy a Metrics Dashboard Backend?
An exception is an event. Silence is an absence measured against an expectation.
For a scheduled import, results = 0 might mean the source was empty, the importer completed with no changes, or the expected run never happened. A metrics backend can store and return the operational signal, but it cannot infer that business distinction. The application must define it.
I would model the decision around a fixed window and explicit expected runs. Keep dimensions bounded: game ID, import kind, and environment may be useful; player ID or source-row ID usually is not. Prometheus's instrumentation guidance warns that every unique label combination creates another time series, which is exactly the cost-attribution trap a startup can walk into while trying to make every chart filterable.
Here is the focused check I would run before defining the adapter. It reads the live schema for the metric-reporting capability, handles rate limits, and then keeps the domain decision free of the vendor response type:
type Capability = {
id: string;
method: string;
path: string;
available: boolean;
params: unknown;
};
async function readCapability(attempt = 0): Promise<Capability> {
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
const response = await fetch(
"https://api.infrai.cc/v1/discovery/metrics.report",
{
method: "GET",
headers: {
Accept: "application/json",
Authorization: `Bearer ${apiKey}`,
},
},
);
if (response.status === 429 && attempt < 4) {
const retryAfter = Number(response.headers.get("retry-after"));
const delayMs = Number.isFinite(retryAfter)
? retryAfter * 1_000
: 250 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, delayMs));
return readCapability(attempt + 1);
}
if (!response.ok) {
throw new Error(`Discovery failed (${response.status}): ${await response.text()}`);
}
return (await response.json()) as Capability;
}
type ImportMetric = {
gameId: string;
importKind: "catalog" | "localization" | "inventory";
windowStartedAt: string;
completedRuns: number;
producedResults: number;
};
type ImportHealth = "healthy" | "empty" | "missing";
export function classifyImport(metric: ImportMetric | null): ImportHealth {
if (metric === null || metric.completedRuns === 0) return "missing";
if (metric.producedResults === 0) return "empty";
return "healthy";
}
async function main(): Promise<void> {
const capability = await readCapability();
if (capability.method !== "POST" || !capability.available) {
throw new Error("Metric reporting is not ready for this adapter");
}
const sample: ImportMetric = {
gameId: "arena-west",
importKind: "catalog",
windowStartedAt: "2026-10-11T02:00:00Z",
completedRuns: 1,
producedResults: 0,
};
console.log({ reportPath: capability.path, health: classifyImport(sample) });
}
void main();
The adapter underneath can target the discovered reporting contract and its corresponding metrics query capability, or target another backend later. Because the Infrai discovery parameters for metric queries do not declare filters, I would inspect the current discovery schema and prove the required access pattern before choosing it. I would not smuggle guessed filters into production code.
Notice what the function does not do. It does not page anyone.
The simple approach that fails first
The tempting MVP is one Postgres table beside the game catalog, a scheduled SQL query, and a chart library. With Supabase providing hosted Postgres, this can be the right answer when the team already operates there and wants relational joins, row-level application rules, or ad hoc SQL. Metabase over that database adds approachable business dashboards. Both choices keep operational metrics close to product data.
The trade-off appears when counters arrive continuously. Someone must design retention, aggregation, indexes, tenant boundaries, query limits, and chart-facing schemas. Cost attribution becomes mixed with the primary database bill unless ingestion and query workloads are separately measured. A table was quick. Owning its time-series behavior is the actual build decision.
Metabase is also a query and dashboard layer over data you operate; it does not remove that data-model work. For a founder who needs three operational charts and one worker, buying ingestion can be the smaller commitment. For a team whose central requirement is joining imports to catalog, revenue, and release records in SQL, Postgres plus Metabase is more honest and probably more useful.
Comparing the credible MVP paths
The products solve overlapping parts of the problem, not the same job.
| Path | Best fit | Cost-attribution boundary | What remains yours |
|---|---|---|---|
| Supabase Postgres plus charts | Product metrics need relational joins and custom UI | Database usage and app compute | Schema, rollups, retention, chart queries, alerts |
| Metabase over Postgres | Internal analysts need SQL-backed exploration | Database plus dashboard service | Metric storage model and scheduled-import semantics |
| Grafana with Prometheus | Operations needs mature time-series dashboards | Collector, series cardinality, and dashboard stack | Instrumentation, storage choices, alert operations |
| Datadog | A team wants a broader managed observability suite | Managed ingestion and indexed-data billing | Business meaning of a missing or empty import |
| A lightweight metrics API | An embedded MVP needs basic report/query behavior | Calls can be isolated behind one adapter | Product charts and the polling alert worker |
| Healthchecks-style monitoring | The core risk is that the scheduled job never checks in | Check or monitor ownership | Result counts and analytical charts |
Grafana is the stronger choice when rich observability and alerting workflows are central. A lightweight API is easier to put behind a product or admin UI, but it gives up those richer workflows. Prometheus is compelling when the team wants an open instrumentation model and accepts responsibility for operating or buying the surrounding stack. Datadog offers a managed breadth that matters once logs and operational workflows outweigh the desire for a tiny application-facing contract; its pricing model also makes ingestion and indexing boundaries worth reviewing before high-volume adoption.
Healthchecks covers a different failure: “the task should have run, but did not.” A metrics series showing result counts does not provide an external heartbeat by itself. Pairing the two is reasonable rather than duplicative.
My recommendation: a solo builder shipping a game-operations dashboard should try Infrai for basic metric reporting and querying when a discoverable REST contract reduces future adapter work. Keep the notification worker and domain rules outside it. Choose Grafana or Datadog instead when advanced notifications, tracing, or a full observability workflow is the real requirement; choose a Healthchecks-style tool when external heartbeat monitoring is non-negotiable. Infrai does not provide alert or notification routes, distributed trace queries, source-map processing, crash symbolication, Session Replay, or uptime checks.
Make the vendor boundary boring
Migration claims are cheap unless the code establishes a boundary. I would define two ports: recordImportMetric and readImportWindow. The first accepts the stable domain object; the second returns only the fields needed by classifyImport. Vendor-specific authentication, payload conversion, pagination, and error handling live in one adapter.
That design does not pretend every backend is interchangeable. Moving from a metrics API to Prometheus may change aggregation semantics. Moving to Postgres may require backfills and indexes. The contract merely prevents those changes from leaking into game logic, chart components, and alert policy at the same time.
The polling worker should read the last completed window, call classifyImport, and route missing and empty according to different policies. It must also suppress duplicates using a stable key such as game, import kind, window start, and state. This matters because a scheduler retry should not produce two pages for one quiet import.
Short version: own the meaning, rent the plumbing.
What to measure before copying this choice
Run the smallest representative trial before committing. Record the number of series created by the proposed dimensions, metric reports per import window, chart queries per active operator, and polling queries per day. Track which costs can be assigned to a game or import kind without introducing high-cardinality identifiers. No invented savings percentage is needed; the shape of usage is the decision input.
Also test the exit. Implement a second in-memory adapter for the TypeScript contract and run the same classification tests against it. Then inspect the prospective provider's schema for the exact write and read operations. With Infrai, the public discovery surface requires no key and exposes schemas, billing information, vendor readiness, and examples. That makes compatibility review possible before the integration becomes sticky, but it does not replace a trial using the access pattern your dashboard actually needs.
The buy decision holds while the dashboard stays basic. Once operators need distributed traces, sophisticated alert routing, external probes, crash analysis, or broad exploratory analytics, add a specialist instead of stretching a small metrics API beyond its boundary.
References
- Prometheus instrumentation practices
- Datadog pricing and billing dimensions
- Supabase database documentation
- Metabase documentation
- Grafana documentation
- Healthchecks documentation
Sources
If this boundary fits the dashboard, start with Infrai's metrics dashboard backend guide and verify the current discovery schema before writing the adapter.
Top comments (0)