Skip to content

Repository files navigation

Tlak — Personal Blood Pressure Tracker

Warning

Vibe-coded end to end. Not a single line of this repo was read by a human.

The whole thing — Next.js app, DB schema, migrations, passkey ceremony, PWA service worker, PDF exporter, Zerops recipe, this README — was generated by an AI agent in one sitting from a plain-language description. Nobody reviewed the code. Nobody sanity-checked the SQL. Nobody looked at the auth flow the way an auth flow deserves to be looked at.

It's AI code and it looks the part. Expect quirks, uneven abstraction levels, over-engineered corners next to under-engineered ones, and the occasional load-bearing hack. Don't run it for anyone whose health decisions depend on it. Don't put strangers' medical data in it.

Want to make it better? Fork it and keep vibing — prompt the model of your choice, ship your own version. PRs welcome too, but no promises anyone here has the context to review them properly.

Tlak is a self-hostable, multi-tenant web app for tracking blood pressure, pulse, weight and lifestyle context over time. Passkey-only authentication (no passwords), doctor-ready PDF export, PWA with push reminders, ESH 2023 classification, Czech + English UI. Built with Next.js 15, Drizzle ORM and PostgreSQL. MIT-licensed.

⬇️ Deploy your own instance on Zerops

Import import.yaml from this repository into a new Zerops project (dashboard → Import project → paste). Or pick a specific environment shape from recipe/:

  • development — single-node Postgres, dev-tier scaling — for personal / self-hosted use
  • production — HA Postgres, multi-container app, autoscaling — for public deployments

Features

  • 📈 Charts & trends — Recharts with ESH 2023 hypertension categories (optimal / normal / high-normal / grade 1–3 / isolated systolic)
  • 🩺 Doctor-ready PDF export — printable summary + full log for the selected period
  • 🔐 Passkey / WebAuthn only — Face ID, Touch ID, Windows Hello. Zero passwords, zero SMTP dependency
  • 📱 PWA — installable to home screen, push notifications for morning + evening measurement reminders and weekly weight reminder
  • 🌍 i18n — Czech + English, per-user preference
  • 🌓 Light / dark / system theme
  • 🏋️ Weight + BMI tracking with configurable height
  • 📝 Contextual notes — arm, position, medication taken, lifestyle checkboxes (poor sleep / stress / exercise / caffeine / alcohol / salty meal)
  • 🔒 Multi-tenant — every user's data is isolated via user_id foreign keys

Tech stack

Layer Tool
Framework Next.js 15 (App Router, standalone output)
Language TypeScript
Styling Tailwind CSS v4
Auth Auth.js v5 (Credentials + @simplewebauthn/server) — passkey ceremony
Database PostgreSQL (16+) via postgres-js
ORM Drizzle ORM + drizzle-kit migrations
i18n next-intl
Theme next-themes
Charts Recharts
PDF @react-pdf/renderer (server-rendered)
Push Web Push API + web-push (VAPID)
Validation Zod

Deploying to Zerops

The zerops.yaml at the repository root drives the build + deploy pipeline. Two setups are provided:

  • prod — production-style: full next build producing the standalone bundle, migrations applied once per deploy via zsc execOnce, app runs node .next/standalone/server.js
  • dev — dev mode: source deployed as-is, idle container waiting for next dev started by the AI agent workflow (zerops_dev_server)

Required environment variables

Set these on the runtime service (dashboard → Environment variables, or via zerops.yaml run.envVariables). The import.yaml in this repo pre-seeds the ones that need to be auto-generated; the rest you set once after import.

Variable Purpose Auto-generated by import.yaml
AUTH_SECRET Auth.js JWT signing secret (64 random chars)
AUTH_URL Public URL of the runtime (needed for correct redirect after sign-out) — set after first deploy
AUTH_TRUST_HOST true — trust the Host header (needed behind Zerops's L7 balancer)
WEBAUTHN_RP_ID Domain (no scheme, no port) — must exactly match what browsers see. Passkeys are bound to this Relying Party ID
WEBAUTHN_ORIGIN https:// + domain — comma-separated list if you serve on multiple origins
WEBAUTHN_RP_NAME Display name shown by the OS during passkey ceremony ("Tlak")
VAPID_PUBLIC_KEY Web-push public key — generate with npx web-push generate-vapid-keys
VAPID_PRIVATE_KEY Web-push private key
VAPID_SUBJECT mailto:you@example.com — contact for push service
CRON_SECRET Bearer token protecting /api/cron/reminders
DB_HOST, DB_PORT, DB_USER, DB_PASS, DB_NAME Wired via ${db_*} references — no manual setup ✅ (via import.yaml)

Post-import checklist

  1. Enable a subdomain on the tlak service (Zerops dashboard → Public access → Enable zerops.app subdomain), or attach a custom domain.
  2. Set AUTH_URL, WEBAUTHN_RP_ID, WEBAUTHN_ORIGIN to match that domain.
  3. Generate VAPID keys locally with npx web-push generate-vapid-keys and set the three VAPID_* variables.
  4. Restart the runtime (env changes trigger this automatically).
  5. Open the URL, register your passkey. Done.

⚠️ Passkeys are bound to the domain. Changing WEBAUTHN_RP_ID invalidates existing passkeys — users need to re-register. Pick your final domain before onboarding real users.

Reminders cron

The endpoint /api/cron/reminders iterates all users and delivers push notifications based on their per-user schedule (morning / evening measurement, Sunday weight). It's protected by CRON_SECRET:

GET /api/cron/reminders?token=<CRON_SECRET>

Wire this to Zerops's cron (or any external scheduler) to fire hourly.


Architecture notes

Passkey flow

Auth.js v5 in this app uses two Credentials providers wrapping @simplewebauthn/server:

  • webauthn-register — receives the WebAuthn attestation from the browser, verifies it against a challenge stored in the webauthn_challenge table, creates the user + passkey row, returns a JWT session
  • webauthn-login — receives the assertion, looks up the credential by ID, verifies, bumps the counter, returns a JWT session

No Auth.js Adapter is used — session strategy is JWT, users table is the app's own store.

Database migrations

Migrations are generated by drizzle-kit (npm run db:generate) into drizzle/. On deploy, scripts/bundle-migrate.mjs bundles src/db/migrate.ts + postgres-js + the SQL files into a self-contained migrate.cjs via esbuild. zerops.yaml runs zsc execOnce ${appVersionId} -- node migrate.cjs which guarantees the migration runs exactly once per deployed version even with multiple replicas.

Multi-tenancy model

All user-owned tables (measurement, weight_entry, push_subscription, passkey) carry a user_id foreign key with ON DELETE CASCADE. Every server action + query filters by session.user.id. No row-level security in Postgres — enforced entirely in the app layer.

PWA + push

public/sw.js is a minimal service worker (no offline caching yet, just push handling). public/manifest.webmanifest marks the app as installable. iOS Safari 16.4+ only delivers web push when the app is installed to the home screen — an important UX note to communicate to users.

Local development

npm install
# Create .env from .env.example, fill in DB + WEBAUTHN_* values
npm run db:push       # sync schema to a local Postgres
npm run vapid:generate # emit VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY
npm run dev

License

MIT — see LICENSE.

Contributions welcome. Open an issue or a PR.

About

Self-hostable multi-tenant blood pressure tracker. Next.js 15 + Postgres + Drizzle, passkey auth, PWA push reminders, PDF export, cs/en, ESH 2023 classification. Zerops recipe included.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages