Few weeks ago I was pushing a fix for a small project I made related to a minecraft server i play :p. My data updates just stopped... Turns out I burned through the free Actions minutes three days earlier. GitHub doesn't email you, they just silently stop running your jobs.
I checked Vercel the next day. 91% bandwidth. Two days from getting throttled.
That's when I realised I was doing manual laps of 4 different billing pages every week just to feel safe. GitHub, Vercel, Supabase, Railway and each buried under a different nav, none of them proactively alerting you. I just started college and wanted to build something meaningful, So with the help of Claude Code I built Stackwatch. Here's how it actually works.
The polling worker
The core is a standalone Node.js worker running on Railway. It's dead simple: a cron job (node-cron) that fires every 5 minutes and loops through every connected integration in the database.
The clever bit is tier-aware polling. Free users get 15-minute intervals, Pro gets 5. The worker runs on a 5-minute tick but filters out integrations that synced too recently for their tier:
const dueIntegrations = integrations.filter((i) => {
const tier = tierMap.get(i.user_id) ?? "free";
const interval = tier === "free" ? FREE_POLL_INTERVAL_MS : PRO_POLL_INTERVAL_MS;
if (!i.last_synced_at) return true;
return now - new Date(i.last_synced_at).getTime() >= interval;
});
One worker, two polling rates, no separate queues.
Storing API keys
Users paste their tokens, which get encrypted before hitting the database. I went with AES-256-GCM so I get authenticated encryption and the auth tag catches tampering. Each encryption generates a fresh random IV, and the stored value is iv:authTag:ciphertext. Decryption validates the tag before returning anything:
`const ALGORITHM = "aes-256-gcm";
export function encrypt(plaintext: string): string {
const iv = randomBytes(12);
const cipher = createCipheriv(ALGORITHM, key, iv);
const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
const authTag = cipher.getAuthTag();
return '${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted.toString("hex")}';
}`
The encryption key is a 64-char hex env var (32 bytes). Raw API keys never touch logs.
Auth and data isolation
Auth is Supabase Auth via email/password, magic link, GitHub and Google OAuth. Every table has Row Level Security enabled so users can only ever read their own rows. The worker uses a service-role key (bypasses RLS intentionally) because it needs to poll all users. The frontend client uses the anon key and relies on RLS.
Alerts
When usage crosses a threshold (default 80%, user-configurable per metric) the worker fires alerts via Resend (email), Slack webhooks, or Discord webhooks. It stores a record in alert_history and won't re-alert on the same metric until it drops below threshold and crosses it again to prevent spam.
Frontend
Next.js App Router, TypeScript throughout. Server components by default, client components only where there's interactivity. The dashboard auto-refreshes every 5 minutes. Usage history graphs are built with Recharts also: if you use a formatted date string (like "Mar 21") as your Recharts dataKey and you have multiple snapshots on the same day, the tooltip snaps to the first point of that date. Fix is to use the raw ISO timestamp as the dataKey and format it only in tickFormatter and labelFormatter.
Stack summary
- Next.js (App Router) on Vercel
- Supabase for auth, database, and RLS
- Railway for the polling worker
- Resend for email
- Recharts for usage graphs
TypeScript everywhere, no exceptions
It's live at https://stackwatch.pulsemonitor.dev the free tier covers one account per service, which is enough for most solo founders. Happy to answer questions about any part of the build.
Top comments (0)