Decisions, lessons, blockers. Append-only. Update as we go.
Decided
git3.techno-world.net/lrosales/broad-announce (private).companies.fcm_shared).info | warning | critical | inminent_colapse.
Only inminent_colapse bypasses quiet hours.(source_id, dedupe_key), attach
dedupe_count so user sees "×N in 60s" not N pushes.Open (was) → Resolved 2026-06-13
individuals row + generates one-time invite code;
user runs /start <invite_code> in the Telegram bot; bot
matches the code, links telegram_chat_id to the
pre-existing individual, and burns the code. Stricter path:
prevents drive-by bot self-registration, keeps
individuals auditable.title/body strings (or use the locale on the
subscription to key into their own lookup table before
calling our API). Alert schema carries locale and
title/body already-resolved. We can add a translation
layer in v2 if customers ask.Lessons (already)
alerts.<company_id> is fine; alerts.<company_id>.<source_id>
would let us scale router consumer groups per source. Locked
in §6 of ARCHITECTURE.md.2026-06-13 — added gRPC ingest (option a)
buf generate build step.StreamAlerts(Alert) → Ack carries the same dedupe_count
contract as HTTP/WS/MQTT.proto/broadannounce/v1/ingest.proto; server
stub in internal/grpcserver/, reusable client in
internal/grpcclient/. Auth = API key in metadata + optional
mTLS. Per-stream rate limit + 256 in-flight cap = natural
backpressure.2026-06-13 — M10 split: option C
loadgen/ tool is the new home for that.2026-06-13 — three open questions resolved
testfakes/). M10 must not burn 50k FCM credits.deliverd binary in the bench profile, not a NATS
subject drop. More realistic: we exercise the real broker +
router + the message shape deliverd would consume.loadgen ships as a single Docker image with all
four binaries; entrypoint picks one via the image's
command: field in the compose / k8s spec.2026-06-13 — Source protection (throttling) is in
rate_limit_per_sec, max_payload_bytes,
max_concurrent_connections, quarantine_*).2026-06-13 — M0 shipped (12 commits, 2888 LoC)
What landed:
cmd/{ingestd,routerd,deliverd,admind}/ — four Go service mainscmd/ingestd/http.go — HTTP POST handler implementing
SPEC §22 layers 1, 3, 4, 5 + Stripe-style HMAC authinternal/alert — Alert v1 type + Validate() (183 LoC + 103
LoC tests)internal/broker — NATS JetStream wrapper, three streams
(ALERTS/DELIVERIES/DLQ) auto-createdinternal/dedupe — 60s SET NX EX + INCR (Redis-required tests
pass against a host-local Redis)internal/ratelimit — per-second INCR bucket (Redis-required
tests pass)internal/observability — slog + Prometheus registry, the
IngestdMetrics struct matches SPEC §22 metric namesinternal/httpserver — shared /health + /metrics scaffoldinternal/config — env-driven Common + Ingestdloadgen/cmd/http/ — loadgen-http with --mode normal,
HMAC signing, 70/25/4/1 severity mix, dedupe-pct knobloadgen/go.mod — separate module per SPEC §21, replace
directive points at the parent moduledocker-compose.yml + Dockerfile — single-host stack, all
5 binaries in one imageM0_VERIFICATION.md — 8-step smoke testdeploy/prometheus/prometheus.yml — scrapes all 5 servicesWhat's NOT in M0 (and not supposed to be):
Module path: git3.techno-world.net/lrosales/broad-announce.
Loadgen module path: git3.techno-world.net/lrosales/broad-announce/loadgen.
All pushed: 7cd922c..49b2dba on master.
2026-06-13 — Port convention
Project rule: app HTTP services use 8800–8899 (ingestd 8800,
routerd 8801, deliverd 8802, admind 8803, loadgen metrics 8891,
fakefcmd 8820). Canonical ports stay (5432, 4222, 6379, 1883,
9090, 3000). Reason: 8080+ collides with workstation tooling.
SPEC §18 now has a port-conventions sub-section. Commit: d76aa0b.
2026-06-13 — M1 code-complete (8 commits, awaiting live smoke)
What landed:
migrations/001_init.up.sql — companies, individuals,
fcm_tokens (the M1 minimum schema; full SPEC §4 lands in M2
as additional migrations)migrations/002_deliveries.up.sql — deliveries table
(status: pending|sent|failed|dlq; payload jsonb for M8 replay)migrations/seed.sql — idempotent; 1 company, 1 individual,
1 tokeninternal/postgres — pgxpool wrapper with retry-on-startupcmd/seed — applies *.up.sql in lexical order, then seed.sqltestfakes/fakefcmd — 70 lines, /health + /v1/.../messages:send,
--fail-rate knobinternal/routing — Resolver with ResolveTokens (M1 broadcast
via single SQL join; M2 swaps for the rules engine)cmd/routerd — M1 main: subscribes to alerts.>, resolves
recipients, publishes one deliveries.fcm. per token
cmd/deliverd — M1 main: subscribes to deliveries.fcm.>,
builds FCM HTTP v1 message body (M3 swap is a no-op at this
layer), posts to BA_FAKECMD_URL, writes a deliveries rowWhat we agreed to defer (per the user):
What's NOT in M1 (and not supposed to be):
Pushed: 264d284..9cf68a1 on master (5 commits for M1 code,
plus the d76aa0b port shift).
The user still has to actually run docker compose up and the
M1 verification steps before M1 is fully done.
2026-06-13 — M2 shipped (recipient resolution, rules engine)
What landed (3 commits, ~600 LoC Go + ~250 LoC SQL):
migrations/003_subscriptions_groups.up.sql (and .down.sql):
5 new tables — sources, groups, group_members,
subscriptions, routing_rules. FKs to companies /
individuals. Idempotent.migrations/seed_m2.sql: 2 more individuals (Bob, Carol), 1
group (sre: Alice+Bob), 1 source row, 3 subscriptions
covering the 3 scenarios, 1 routing rule.cmd/seed/main.go: runner now applies seed.sql then
seed_m2.sql (lexically ordered) so re-running is safe.internal/routing/routing.go: full rewrite. New Resolver
with ResolveTargets(ctx, *alert.Alert) ([]Target, error).
Single SQL round-trip via a 5-CTE query:
internal/alert/alert.go: added Severity.Rank() and
MinSeverityRank() helpers. Two new unit tests in
alert_test.go.internal/routing/routing_test.go: new file, 10 subtests
for inQuietHours (same-day, wrap-around, always-quiet,
edge cases at window start/end).cmd/routerd/main.go: M2 main. Calls ResolveTargets(alert),
hard-fails on zero recipients (logs WARN, acks — no DLQ for
M2; that's M3+). Emits the new envelope shape with
Channel + Endpoint instead of the M1 FCMToken+Locale.cmd/deliverd/main.go: envelope struct updated to match
the M2 routerd output. Only field renames; the FCM HTTP v1
body shape is unchanged.M2_VERIFICATION.md: 7-step plan + hard-fail scenario.scripts/m2_smoke.sh: automated runner for steps 2-6.M2_SMOKE_LOG.md: per-step results, honest flags.Live smoke results (all green):
WARN msg="zero recipients, dropping", no delivery
9 deliveries, 0 failures, 0 retries.Per the user's M2 Q1/Q2/Q3 answers:
dlq.no_recipients.<company> subject).WHERE f.channel = 'fcm' in the resolver).What's NOT in M2 (and not supposed to be):
time.LoadLocation(tz) fails)Pushed: 280e048 on master (3 commits: a29c2d8 + 1e9eca9 + 280e048).
2026-06-14 — M3 shipped (Telegram delivery + bot commands)
What landed (~2k LoC Go + ~80 LoC SQL across these commits):
migrations/004_telegram.up.sql (+ .down.sql):
telegram_bots table (one row per (company, bot); M3
supports one bot per company), and 4 new columns on
individuals: telegram_chat_id, telegram_user_id,
telegram_invite_code, mute_until. Two partial unique
indexes for fast /start <code> lookup and "is this
telegram_user_id already linked" check.migrations/seed_m3.sql: inserts the per-company
telegram_bots row, sets telegram_invite_code on all
three individuals, and pre-links Alice to a fake
Telegram account (chat_id=1001, user_id=900001) so the
very first POST exercises both fcm and telegram delivery
paths. Also expands every subscription's channel_mask
to ["fcm","telegram"] (M2 was ["fcm"]).cmd/seed/main.go: runner now applies seed.sql,
seed_m2.sql, seed_m3.sql in lexical order. All three
are idempotent.cmd/deliverd/ → cmd/deliverd-fcm/: rename. The M2
deliverd is now deliverd-fcm — per the user's M3 Q2
answer (two per-channel binaries, not one with a registry).cmd/deliverd-telegram/: new binary. Subscribes to
deliveries.telegram.<company_id>, builds a
severity-prefixed text message, posts to the Bot API's
sendMessage endpoint, writes a deliveries row.
Single-attempt (no retry, no DLQ). M9 adds the chain.cmd/telegramd/: new binary. Loads the active bot list
from telegram_bots at startup, long-polls
getUpdates, dispatches commands to the handler, and
replies via sendMessage. One process per deployment;
per-bot sharding can come later if needed.internal/telegram/: 3 files, ~640 LoC.
client.go — BotClient interface + HTTP impl.commands.go — text → Command parser
(/start, /subscribe, /unsubscribe,
/preferences, /status, /mute, /unmute).
/mute accepts 2h, 30m, 90s, or until 18:00.handler.go — Command → DB updates. /start is
atomic claim-by-invite-code; /subscribe upserts a
subscription with the requested min_severity.commands_test.go — 7 subtests, 100% pass.internal/routing/routing.go: the resolver's single CTE
now UNION-ALLs an fcm_rows branch (joins on
fcm_tokens) and a tg_rows branch (filters on
telegram_chat_id IS NOT NULL). Same cost as M2; one
extra row in the join key set. Hard-fail on zero
targets unchanged.testfakes/faketgmd/: ~350 LoC. Fake Bot API server
with /admin/queue (queue an incoming update),
/admin/sent (read every sendMessage call), and
/admin/reset (clear state). In-memory only. Used
purely for the smoke test.docker-compose.yml + Dockerfile: add
deliverd-telegram (port 8821), telegramd (port 8822),
faketgmd (port 8830). Grafana is now 3001:3000
because :3000 is held by gogs on this host.M3_VERIFICATION.md: 8-step manual + automated walkthrough.M3_SMOKE_LOG.md: per-step results, honest flags.scripts/m3_smoke.sh: automated runner for steps 2–8.Live smoke results (all green):
/start acme-bob-002 via faketgmd admin queue
→ handler atomically claims the code, telegram_user_id=900002,
bot reply "Linked. Welcome, Bob SRE."/preferences → bot reply "Your subscriptions: …"14 deliveries across 4 unique alert_ids, 0 failures, 0 retries.
Per the user's M3 Q1/Q2/Q3 answers:
["fcm"] → M3's ["fcm","telegram"] via the
seed migration).What's NOT in M3 (and not supposed to be):
Pushed: