Independent availability radar for Hetzner Cloud server types.
The app polls Hetzner Cloud, stores raw availability observations in Postgres, derives current stock state, renders a public dashboard, and syncs mailing-list subscribers into Resend Contacts for marketing dispatches.
- Next.js 16 App Router
- React 19
- TanStack Query, server-prefetched and hydrated
- Postgres
- Drizzle ORM and Drizzle Kit
- Zod + T3 Env
- Biome
- pnpm
- One-shot worker poll every 5 minutes (
pnpm worker/Dockerfile.worker). - Hetzner
GET /v1/server_typesintegration. - Raw poll history in
availability_observations. - Current per server-type/location state in
availability_current. - Daily flicker tracking in
daily_availability_state. limiteddisplay state when latest stock is available but same UTC day also saw sold-out.- ISR for
/and/api/availabilitywith 5 minute revalidation. - Resend-backed subscriber capture and React Email marketing dispatches.
- DB-backed homepage with no mock-data dependency.
Base status:
- location exists and
available: true->available - location exists and
available: false->sold-out - tracked location missing from server type ->
not-offered - parse/fetch uncertainty ->
unknown
Display status:
- latest
sold-outstayssold-out - latest
availableplus same UTC day saw both available and sold-out ->limited - latest
availablewithout flicker ->available not-offeredandunknownpass through
Server types are discovered from the live Hetzner API by family prefix, so
current active types such as CX23, CX33, CX43, and CX53 are shown instead
of stale legacy codes. Family metadata in src/lib/server-families.ts controls
which discovered families are visible publicly and which are dispatch-enabled.
Run pnpm families:audit to detect new Hetzner server families that need local
metadata review.
Create .env.local from env.example:
cp env.example .env.localRequired variables:
DATABASE_URL=postgres://postgres:password@localhost:5432/hetzner_cloud_radar
HETZNER_API_TOKEN=
RESEND_API_KEY=
RESEND_FROM_EMAIL=dispatches@hetzner.thegoated.dev
RESEND_MARKETING_SEGMENT_ID=
RESEND_SOLD_OUT_TOPIC_ID=
RESEND_RESTOCK_TOPIC_ID=
SKIP_ENV_VALIDATION=falseRESEND_FROM_EMAIL must be a verified sender/domain in Resend. It defaults to
dispatches@hetzner.thegoated.dev if omitted. RESEND_MARKETING_SEGMENT_ID,
RESEND_SOLD_OUT_TOPIC_ID, and RESEND_RESTOCK_TOPIC_ID are optional. If topic
IDs are configured, new contacts are created with matching opt-in/opt-out topic
preferences and dispatch emails are tagged with the matching topic.
Do not commit .env.local or real tokens.
Install dependencies:
pnpm installCreate the local database and run migrations:
pnpm db:setup:localStart the app:
pnpm devOpen:
http://localhost:3000
One-shot worker (local or Railway cron via Dockerfile.worker):
pnpm workerNeeds DATABASE_URL + HETZNER_API_TOKEN (plus Resend vars if you want dispatch emails).
Public read routes should be cached at the CDN:
//api/availability/dispatches/feed.atom
The app sends Cache-Control for those routes with a 5 minute
fresh TTL, 15 minute stale-while-revalidate window, and 24 hour stale-if-error
window.
Do not apply the cache rule to /api/subscribe, /api/unsubscribe, or
/api/unsubscribe/feedback.
Schema lives in:
src/lib/db/schema.ts
Tables:
poll_runsserver_typeslocationsavailability_observationsavailability_currentdaily_availability_statemarketing_dispatch_sends
Generate migrations:
pnpm db:generateApply migrations:
pnpm db:migrateOpen Drizzle Studio:
pnpm db:studioPublic read endpoint:
GET /api/availability
Returns the same shape used by the dashboard:
familiesobservedAtobservedDatepollCadencetopLineeventssupplyHistoryusingFallback
Subscribe endpoint:
POST /api/subscribe
Body:
{
"email": "you@example.com",
"wantsSoldOut": true,
"wantsRestock": true
}The endpoint syncs the address and preferences to Resend Contacts, Segments, and Topics. The app does not store subscriber email addresses locally.
src/app/ Next.js routes and UI
src/app/api/availability/ Public availability JSON
src/app/api/subscribe/ Resend-backed subscriber endpoint
src/lib/availability/ Hetzner polling and read model
src/lib/db/ Drizzle client and schema
src/lib/marketing/ Resend contact sync
src/scripts/ Local DB setup helpers
worker.ts One-shot poll entrypoint
Dockerfile.worker Worker image
drizzle/ Generated migrations
Run:
pnpm lint
pnpm exec tsc --noEmit
pnpm buildKnown lint output: Biome currently warns about !important rules in
src/app/globals.css for reduced-motion overrides. The command exits with code
0.
- User accounts
- Admin UI
- Public API docs
- Alerting
- Automated broadcast creation/sending
- Long-form historical dispatch generation