This document describes how we integrate, release, and deploy Telemetry Tracker (API + dashboard). SDK packages (@telemetry-tracker/*) use separate semver in packages/*/package.json and pnpm publish:packages — see README.
User-facing changes are recorded in CHANGELOG.md.
| Branch | Role |
|---|---|
develop |
Integration branch — GitHub default; feature PRs merge here; CI must pass |
main |
Production — official hosted cloud and semver GitHub Releases |
feature/*, fix/* |
Short-lived; open PRs against develop |
flowchart LR
Feature[feature/fix branch]
Develop[develop]
Main[main]
Hosted[Railway production]
Tag[GitHub Release vX.Y.Z]
Feature -->|PR| Develop
Develop -->|milestone release PR| Main
Main --> Hosted
Main --> Tag
Deploy ≠ release. Railway production deploys from main. Self-hosters and GitHub Release consumers follow semver tags on main.
There is no separate staging deploy yet — validate on develop locally before milestone promotion.
Feature and fix PRs merge into develop. Branch protection (Settings → Branches → develop):
| Rule | Mechanism |
|---|---|
| PR required | Branch protection — no direct pushes to develop |
| CI must pass | Required status check: build (.github/workflows/ci.yml) |
| Bugbot | Required status check: bugbot-review — temporarily no-op (BUGBOT_REVIEW_ENABLED=false); see CONTRIBUTING.md |
| Branch up to date | Require branches to be up to date before merging |
| Human approvals | 0 — GitHub cannot require “approval only when author ≠ maintainer”; use maintainer-review check instead |
| Maintainer gate | Required status check: maintainer-review — passes automatically when PR author is @unjica; otherwise requires your APPROVED review |
GitHub settings (required): On the develop rule, set Required approvals to 0 and add maintainer-review and bugbot-review to required status checks (with build). Do not require Cursor Bugbot directly — fork PRs often never receive that app check. Keep bugbot-review required while paused so the check still runs as success.
Your own PRs to develop: maintainer-review passes automatically; bugbot-review currently passes without waiting for Cursor Bugbot (re-enable via BUGBOT_REVIEW_ENABLED=true).
Adding status checks: GitHub lists checks only after they have run at least once on a PR. Open or update a PR, wait for build, bugbot-review, and maintainer-review to finish, then edit the develop rule if a check is missing.
Bugbot findings default to a neutral check conclusion — they comment but do not block merge unless fail on unresolved issues is enabled in the Cursor Bugbot dashboard. Pause the GitHub App in the Cursor dashboard if you want to stop PR comments while the gate is disabled.
GitHub cannot require “your approval only when you are not the PR author” (authors cannot approve their own PRs). main uses:
| Rule | Mechanism |
|---|---|
| PR required | Branch protection |
| CI must pass | Required check: build |
| Bugbot (recommended) | Required check: bugbot-review on develop; on main optional — see CONTRIBUTING.md |
| Maintainer approval when author ≠ @unjica | Required check: maintainer-review (workflow) |
Your own PRs to main |
maintainer-review passes automatically; merge after build is green |
| Review requests | CODEOWNERS notifies @unjica on PRs to main |
After the maintainer-review workflow is on main, add maintainer-review to required status checks (Settings → Branches → main, or maintainer API update).
Releases are milestone-driven:
- Track work in a GitHub milestone (e.g. “v1.2 — …”).
- Merge completed work into
develop; keep CHANGELOG.md[Unreleased]up to date. - When the milestone is complete and CI is green, promote
develop→main(release PR or fast-forward). - Tag
main, publish GitHub Release, migrate production DB, verify deploy. - Sync
developwithmain— always mergemainback intodevelopafter promotion (see checklist). Squash merges, merge commits, and post-promotion commits onmain(e.g. CHANGELOG finalization) leavedevelopbehind otherwise.
Hotfixes on production: branch from main, fix, merge to main, tag a patch version, then merge main → develop so branches stay aligned. Assign PRs and release PRs to a milestone per Milestones vs hotfixes.
- Do not merge release PRs (
develop→main, or hotfix →main) until all required checks are green (build, and ondevelopalsobugbot-review/maintainer-reviewas configured). - Stacked / multi-PR features: the final integration PR that lands the feature on
developmust run the full API + dashboard test suites (not a subset). Prefer one green CI pass on that integration PR before release promotion. - Assign feature PRs to a milestone whenever possible (active product line or patch-line milestone) so shipped work stays auditable.
- Prefer one production smoke-test pass (health, migrations, critical paths for the milestone — see Post-deploy verification and
scripts/smoke-production.sh/ notification channel checks) before closing a milestone. - Taxonomy: milestones = release lines (e.g.
v1.14.x — Notifications); parent issues = long-lived vision/roadmap; child issues = implementation that ships inside a milestone. Closing a milestone does not require closing the parent vision issue — parents should stay open as living roadmap even after children merge.
Milestone-driven releases (typical MINOR / MAJOR promotion) follow the cadence above: scope work in a GitHub milestone, merge into develop, then promote develop → main.
PATCH releases (production hotfixes, docs-only promotions, and other small semver bumps on main) use the same promote → tag → GitHub Release → sync develop flow, but they are not required to open a new product milestone for every patch.
For audit trail, PATCH and docs hotfixes should still:
- Assign the integrating PR(s) on
developto a milestone — typically the active patch-line milestone (e.g.v1.4.x — Platform releases). - Assign the
develop→mainrelease PR to that same milestone. - Update the milestone description with the shipped semver (e.g. v1.4.3, v1.4.4) when the release completes.
If the patch-line milestone is closed, you may still assign merged PRs to it retroactively, or open a new patch-line milestone if you prefer a clean bucket for the next patches.
Product update emails remain policy-driven (MARKETING-EMAIL.md); they send on line close of a completed minor (whole vX.Y.*), not when opening the next milestone’s first .0. Docs-only PATCH releases usually skip subscriber email.
| Artifact | Version | Tag / registry |
|---|---|---|
| App (API + dashboard) | Semver on main |
v1.0.0, v1.1.0, … |
| SDKs | Per-package in packages/*/package.json |
npm (pnpm publish:packages) |
App releases use vX.Y.Z:
| Component | Bump name | When |
|---|---|---|
| X | MAJOR | Breaking ingest/HTTP contract, auth model, required env vars, destructive migrations |
| Y | MINOR | Features, backward-compatible migrations, dashboard UX (typical milestone release) |
| Z | PATCH | Bug fixes and hotfixes on main |
Product update emails (see MARKETING-EMAIL.md) send when closing a completed minor line (last intended vX.Y.z before the next milestone), summarizing all vX.Y.* since the previous product email — not when tagging the first .0 of a new line. Mid-line Z-only hotfixes stay skip.
Always document migrations, new env vars, and breaking changes in CHANGELOG and the GitHub Release notes.
- GitHub milestone complete (or release scope agreed)
- CHANGELOG.md
[Unreleased]section complete - CI green on
develop(pnpm lint,pnpm test,pnpm -r run build) - Self-host upgrade notes ready (migrations, env vars)
- If this closes a milestone / is the last intended release of
vX.Y.*: confirmCHANGELOG.mdcovers the whole line and plan to run production migrations before pushing the tag — then close thevX.Y.x — …GitHub milestone after the final tag so the product update email auto-sends (tag push alone does not email) - If this opens a new minor (
vX.Y.0) while the previous line was already emailed (or will be emailed on its final tag): no product email for the new line yet
- Finalize CHANGELOG — rename
[Unreleased]to[X.Y.Z] - YYYY-MM-DD. Prefer doing this in thedevelop→mainrelease PR sodevelopandmainstay aligned; if you commit onmainafter promotion, you must syncdevelopin step 9. - Deploy — Railway rebuilds
mainautomatically when the release PR merges; see Deploy runbook. - Production DB — run migrations before tagging on MINOR (Y) / MAJOR (X) releases (and before a line-close product email). The release email workflow reads production Postgres but does not migrate (use the public
DATABASE_URLin GitHub secrets — see MARKETING-EMAIL.md). - Post-deploy — verification.
- Tag — after deploy and migrations are green. Tag push starts the release-email workflow but does not send for opening
.0or mid-line patches — product email auto-sends when you close thevX.Y.xmilestone after the final tag (MARKETING-EMAIL.md). Push the tag only after the release PR is merged tomainsoCHANGELOG.mdcontains the version section when any email workflow runs (an early tag push can fail once, then succeed on retag):git checkout main && git pull origin main git tag -a v1.1.0 -m "Telemetry Tracker v1.1.0" git push origin v1.1.0
- GitHub Release — publish notes from RELEASE_NOTES_TEMPLATE.md (highlights + upgrade steps; link to CHANGELOG for detail):
See the template for section guidance and a filled v1.1.0 example. Do not duplicate the entire CHANGELOG — keep the release body scannable for deployers.
VERSION=1.2.0 PREVIOUS=1.1.0 cp docs/RELEASE_NOTES_TEMPLATE.md /tmp/release-notes-v${VERSION}.md # Edit: Highlights from CHANGELOG [X.Y.Z]; migrations since tag v${PREVIOUS}; env/SDK sections if needed gh release create "v${VERSION}" \ --title "Telemetry Tracker v${VERSION}" \ --notes-file "/tmp/release-notes-v${VERSION}.md"
- SDK — if ingest/SDK contract changed, bump
packages/*/package.jsonandpnpm publish:packages. - Product update email — when this tag is the last intended release of a minor line, close the
vX.Y.x — …GitHub milestone after the tag exists. That triggers Release product email with derivedversion/previous_versionandline_close=true(wholevX.Y.*line; ledger keyX.Y). Opening a newvX.Y.0and mid-line patches do not send. If the line was already emailed, the ledger skips duplicates. Manual dispatch (dry_run/ retry) remains available (MARKETING-EMAIL.md). After the send completes, note date and recipient count in the GitHub Release. For exceptional single-version PATCH backfill, use--force— see MARKETING-EMAIL.md. - Sync
develop— mergemainintodevelopand push after every release (milestone promotion or hotfix). Required whenevermainhas commits not ondevelop— including squash merges, merge commits from the release PR, and any post-promotion edits onmain:git checkout develop && git pull origin develop git merge origin/main git push origin develop
Production uses three Railway services (+ optional workers/cron):
| Service | Root directory | Builder |
|---|---|---|
| PostgreSQL | — | Railway Postgres |
| API | apps/api |
Railpack (not Dockerfile) |
| Dashboard | repo root (empty) | Dockerfile |
| Retention cron (optional) | apps/api |
Cron → node dist/jobs/run-retention.js |
| Alert rules evaluator cron (optional) | apps/api |
Cron → node dist/jobs/run-alert-rules-evaluator.js |
| Alert webhook worker (optional) | apps/api |
Always-on → node dist/jobs/run-alert-webhook-worker.js |
- Automatic: push/merge to
main. Each service rebuilds when its watch paths change. - Manual: Railway dashboard → service → Redeploy.
On push and pull requests to develop and main, CI runs (.github/workflows/ci.yml):
pnpm lintprisma migrate deploy(against CI Postgres only)pnpm testwithRUN_DB_INTEGRATION_TESTS=truepnpm -r run build- telemetry-core dist drift check
Do not promote to main, tag, or deploy production if CI is failing.
CI does not migrate your production database during normal builds. Apply migrations after API deploy and before pushing a MINOR (Y) / MAJOR (X) tag (see On main after promotion). The Release product email workflow connects to production Postgres for the send ledger only — use Railway’s public database URL in GitHub secrets, not postgres.railway.internal.
DATABASE_URL="postgresql://..." pnpm --filter api exec prisma migrate deployUse Railway Postgres public URL from your laptop, or a Railway one-off shell with DATABASE_URL set.
Alternatively, add to the API start command (only if you accept migrate-on-every-start):
pnpm exec prisma migrate deploy && node dist/index.jsPrefer one-off or deploy hook if you want explicit control.
Set on each Railway service (not only a local .env). Core vars: DEPLOYMENT.md. Email & Stripe: BILLING.md. Railway setup: RAILWAY.md.
API (required in production):
DATABASE_URL,NODE_ENV=production,HOST=0.0.0.0HEALTH_CHECK_DATABASE=trueCORS_ORIGINSorDASHBOARD_ORIGIN= dashboard URLTELEMETRY_DASHBOARD_ORIGIN= dashboard URL (no trailing slash)
API (recommended):
RESEND_API_KEY,TELEMETRY_EMAIL_FROM— invites, password reset, notification emailTELEMETRY_ALLOW_REGISTRATION=false— after first user exists
API (never in production):
INGEST_ALLOW_UNAUTHENTICATEDTELEMETRY_ALLOW_UNAUTHENTICATED_READS
Dashboard:
API_URL= public API URL (no trailing slash)NEXT_PUBLIC_SITE_URL= public dashboard URL
Schedule nightly (e.g. 0 3 * * * UTC):
node dist/jobs/run-retention.jsSame DATABASE_URL as the API. See RAILWAY.md.
If CUSTOM Alert Rules use schedule-oriented conditions (HEARTBEAT, NO_EVENTS, SESSION_DROP, QUOTA_PERCENT, scheduled ERROR_RATE), add a Railway cron (manual wiring — not auto-provisioned):
node dist/jobs/run-alert-rules-evaluator.jsDefault schedule */5 * * * * (match ALERT_RULES_SCHEDULE_INTERVAL_MINUTES). Same DATABASE_URL as the API. Do not repurpose or alter any brief-worker service. See RAILWAY.md and ALERT-RULES.md.
If alert webhooks are enabled, keep a continuous worker running (not cron):
node dist/jobs/run-alert-webhook-worker.jsSame DATABASE_URL as the API. Confirm logs show "status":"idle" (or processing)
JSON. See RAILWAY.md and
ALERT-WEBHOOKS.md.
./scripts/verify-prod-config.sh
# Custom hosts: VERIFY_API_URL=... VERIFY_DASHBOARD_URL=... ./scripts/verify-prod-config.sh
# Uptime probe (also runs every 15 min via GitHub Actions on main):
./scripts/check-production-uptime.shOr individual curls:
curl -sS https://<api-host>/health
# → {"ok":true,"version":"1.6.2","database":"ok","database_latency_ms":5,"email":"configured"}
curl -sS -o /dev/null -w "%{http_code}" -X POST https://<api-host>/ingest/event \
-H "Content-Type: application/json" -d '{"app":"t","name":"t"}'
# → 401
curl -sS -o /dev/null -w "%{http_code}" https://<api-host>/api/errors
# → 401In the browser:
- Dashboard loads;
/dashboardredirects to login when logged out - Register (if allowed) → org → project → API key → ingest → Overview shows data
- Notifications bell and Appearance theme (v1.1+)
See also PRODUCTION-READINESS.md.
- Deploy Postgres, API, dashboard; run migrations.
- Open dashboard → Register (allowed when no users exist).
- Create organization, project, API key.
- Set
TELEMETRY_ALLOW_REGISTRATION=falseon API. - Schedule retention cron.
First production-ready self-hosted release. Full changelog: CHANGELOG.md#100---2026-06-26.
Includes:
- Multi-tenant ingest (events, errors, sessions, batch) with API keys and plan limits
- Next.js dashboard (overview, errors, events, sessions, org/team/keys settings)
- Email/password auth, org invites, password reset (Resend)
- RBAC (OWNER / EDITOR / VIEWER)
- Optional Stripe billing (checkout, portal, webhooks)
- SDKs
@telemetry-tracker/*v1.3.0 in-repo (published as@telemetry-tracker/core,@telemetry-tracker/next,@telemetry-tracker/node,@telemetry-tracker/react-native) - Retention job, deployment docs, CI with DB integration tests
Known limitations:
- Per-project ingest RPS is in-memory (single API process)
- Project alert webhooks for spike/quota — see ALERT-WEBHOOKS.md
- Open sessions without
ended_atare not pruned by retention until closed
Live reference deployment: telemetry-tracker.com (dashboard) / api.telemetry-tracker.com (API). Legacy: telemetry-api.tacko.io, telemetry-tracker.tacko.io.