[TERMINAL · SKILLS]
> mounting /skills...
> indexing skill manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Use Cases/Build a Tenant Usage Dashboard

Build a Tenant Usage Dashboard

Build a multi-tenant usage dashboard with per-tenant metrics, resource consumption tracking, quota management, cost allocation, and self-service analytics for SaaS platforms.

Development#typescript#tsconfig#type-checking#type-safety#javascript-migration
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 5 skills

Avg quality 96/100·All SAFE
>

typescript

v1.0.0

Sets up and repairs TypeScript projects: picks tsconfig.json settings for a Node.js service, an npm library or a bundled web app, runs type-checking in CI, and replaces `any` and unsafe casts with types the compiler can verify. Use when someone asks to "set up TypeScript", "fix this tsconfig", "fix a TypeScript error", "upgrade to TypeScript 6 or 7", "migrate JavaScript to TypeScript", "publish types for a package", "run .ts files in Node" or "make this type-safe". Covers the changed defaults in TypeScript 6.0 and 7.0, module and moduleResolution, strict mode, project references, declaration output, narrowing, satisfies and common compiler errors.

93/100 quality
7.30× impact
SAFE· stale score
View skill
>

redis

v1.1.0

Redis is an in-memory data store used as a cache, session store, message broker and counter service. Use when adding a cache layer, storing sessions, rate limiting an API, running pub/sub or Streams with consumer groups, taking a distributed lock, building a leaderboard, or configuring Redis for production (persistence, maxmemory, eviction).

93/100 quality
1.79× impact
SAFE· stale score
View skill
>

postgresql

v1.1.0

Assists with designing schemas, writing performant queries, managing indexes, and operating PostgreSQL databases. Use when working with JSONB, full-text search, window functions, CTEs, row-level security, replication, or performance tuning. Trigger words: postgresql, postgres, sql, database, jsonb, rls, window functions, cte.

100/100 quality
5.15× impact
SAFE· stale score
View skill
>

hono

v1.1.0

Hono is a small web framework built on Web Standards (Request/Response) that runs unchanged on Cloudflare Workers, Deno, Bun, Node.js, AWS Lambda and Vercel. Use it when the user asks to build a Hono API, add Zod validation, JWT auth, CORS or middleware, use the type-safe RPC client (hc), render JSX, stream responses, or deploy one codebase to several runtimes.

100/100 quality
6.20× impact
SAFE· stale score
View skill
>

zod

v1.1.0

Zod is a TypeScript-first schema library that validates data at runtime and infers static types from the same schema. Use when validating API request bodies, form input, environment variables or config files, parsing untrusted JSON, or migrating from Zod 3 to Zod 4. Covers z.object, safeParse, z.infer, coercion, discriminated unions, refinements, JSON Schema output and the v4 API changes.

93/100 quality
3.87× impact
SAFE· stale score
View skill
$

The Problem

Olga leads ops at a 25-person multi-tenant SaaS. Each tenant asks "how much am I using?" and the answer requires an engineer running SQL queries. There's no visibility into which tenants are approaching storage limits. Cost allocation per tenant is guesswork — the biggest customer might be subsidized by smaller ones. When a tenant hits a limit, they get a cryptic 500 error instead of a helpful message. They need a tenant usage dashboard: self-service usage metrics, quota tracking with alerts, cost breakdown per tenant, and admin overview of all tenants.

Step 1: Build the Usage Dashboard

typescript
// src/tenants/usage.ts — Multi-tenant usage tracking with quotas and cost allocation
import { pool } from "../db";
import { Redis } from "ioredis";

const redis = new Redis(process.env.REDIS_URL!);

interface TenantUsage {
  tenantId: string;
  metrics: {
    apiCalls: { used: number; limit: number; percentage: number };
    storage: { usedMb: number; limitMb: number; percentage: number };
    users: { active: number; limit: number; percentage: number };
    bandwidth: { usedMb: number; limitMb: number; percentage: number };
  };
  cost: { estimated: number; breakdown: Record<string, number> };
  period: string;
}

interface TenantQuota {
  tenantId: string;
  plan: string;
  limits: Record<string, number>;
  overageAllowed: boolean;
  overageRate: Record<string, number>;
}

// Track usage event
export async function trackUsage(tenantId: string, metric: string, amount: number = 1): Promise<{ allowed: boolean; remaining: number }> {
  const period = getCurrentPeriod();
  const key = `usage:${tenantId}:${metric}:${period}`;

  const current = await redis.incrbyfloat(key, amount);
  await redis.expire(key, 86400 * 35);

  const quota = await getTenantQuota(tenantId);
  const limit = quota.limits[metric] || Infinity;
  const remaining = Math.max(0, limit - current);

  if (current > limit && !quota.overageAllowed) {
    await redis.incrbyfloat(key, -amount);
    return { allowed: false, remaining: 0 };
  }

  // Alert at thresholds
  const percentage = (current / limit) * 100;
  for (const threshold of [80, 90, 95, 100]) {
    if (percentage >= threshold) {
      const alertKey = `usage:alert:${tenantId}:${metric}:${threshold}:${period}`;
      const sent = await redis.set(alertKey, "1", "EX", 86400, "NX");
      if (sent) {
        await redis.rpush("notification:queue", JSON.stringify({
          type: "usage_threshold", tenantId, metric, percentage: threshold, current, limit,
        }));
      }
    }
  }

  return { allowed: true, remaining };
}

// Get tenant usage dashboard
export async function getTenantDashboard(tenantId: string): Promise<TenantUsage> {
  const period = getCurrentPeriod();
  const quota = await getTenantQuota(tenantId);

  const metrics: Record<string, { used: number; limit: number; percentage: number }> = {};
  const costBreakdown: Record<string, number> = {};

  for (const [metric, limit] of Object.entries(quota.limits)) {
    const key = `usage:${tenantId}:${metric}:${period}`;
    const used = parseFloat(await redis.get(key) || "0");
    const percentage = limit > 0 ? Math.round((used / limit) * 100) : 0;
    metrics[metric] = { used, limit, percentage } as any;

    // Cost calculation
    const includedCost = 0;
    const overageCost = used > limit && quota.overageAllowed
      ? (used - limit) * (quota.overageRate[metric] || 0)
      : 0;
    costBreakdown[metric] = Math.round(overageCost * 100) / 100;
  }

  const estimatedCost = Object.values(costBreakdown).reduce((s, c) => s + c, 0);

  return {
    tenantId,
    metrics: metrics as any,
    cost: { estimated: estimatedCost, breakdown: costBreakdown },
    period,
  };
}

// Admin: overview of all tenants
export async function getAllTenantsOverview(): Promise<Array<{
  tenantId: string; plan: string; topMetric: string; topUsagePercentage: number; estimatedCost: number;
}>> {
  const { rows: tenants } = await pool.query("SELECT id, plan FROM tenants WHERE status = 'active'");
  const overviews = [];

  for (const tenant of tenants) {
    const dashboard = await getTenantDashboard(tenant.id);
    const topMetric = Object.entries(dashboard.metrics)
      .sort((a, b) => (b[1] as any).percentage - (a[1] as any).percentage)[0];

    overviews.push({
      tenantId: tenant.id,
      plan: tenant.plan,
      topMetric: topMetric?.[0] || "none",
      topUsagePercentage: (topMetric?.[1] as any)?.percentage || 0,
      estimatedCost: dashboard.cost.estimated,
    });
  }

  return overviews.sort((a, b) => b.topUsagePercentage - a.topUsagePercentage);
}

// Middleware: check quota before processing request
export function quotaMiddleware(metric: string, amount: number = 1) {
  return async (c: any, next: any) => {
    const tenantId = c.get("tenantId") || c.req.header("X-Tenant-ID");
    if (!tenantId) return c.json({ error: "Tenant ID required" }, 400);

    const { allowed, remaining } = await trackUsage(tenantId, metric, amount);
    c.header("X-Usage-Remaining", String(remaining));

    if (!allowed) {
      return c.json({
        error: "Quota exceeded",
        metric,
        message: `You've reached your ${metric} limit. Upgrade your plan for more.`,
      }, 429);
    }

    await next();
  };
}

async function getTenantQuota(tenantId: string): Promise<TenantQuota> {
  const cached = await redis.get(`quota:${tenantId}`);
  if (cached) return JSON.parse(cached);

  const { rows: [tenant] } = await pool.query("SELECT id, plan FROM tenants WHERE id = $1", [tenantId]);
  const planLimits: Record<string, Record<string, number>> = {
    free: { apiCalls: 1000, storage: 100, users: 3, bandwidth: 500 },
    starter: { apiCalls: 50000, storage: 5000, users: 10, bandwidth: 10000 },
    pro: { apiCalls: 500000, storage: 50000, users: 50, bandwidth: 100000 },
    enterprise: { apiCalls: 5000000, storage: 500000, users: 500, bandwidth: 1000000 },
  };

  const quota: TenantQuota = {
    tenantId,
    plan: tenant?.plan || "free",
    limits: planLimits[tenant?.plan || "free"] || planLimits.free,
    overageAllowed: ["pro", "enterprise"].includes(tenant?.plan),
    overageRate: { apiCalls: 0.001, storage: 0.05, bandwidth: 0.02 },
  };

  await redis.setex(`quota:${tenantId}`, 300, JSON.stringify(quota));
  return quota;
}

function getCurrentPeriod(): string {
  return new Date().toISOString().slice(0, 7);
}

Results

  • Self-service usage visibility — tenants see their API calls, storage, users, bandwidth in real-time dashboard; no more engineering support requests for "how much am I using?"
  • Quota enforcement with friendly errors — free tier hits 1000 API calls → clear message "upgrade for more" with remaining count in header; no cryptic 500 errors
  • Cost allocation accurate — enterprise tenant consuming 60% of resources pays proportionally; no more cross-subsidization; pricing aligned with actual usage
  • Threshold alerts — 80% usage → warning email; 95% → urgent Slack; 100% → admin notified; tenants never surprised by hard limits
  • Admin overview — ops team sees all tenants sorted by usage; identifies tenants approaching limits; proactive outreach for upsell; no more reactive firefighting

Related use cases