Documentation for Publish Helper and Growth Envelope

This report is a collection of best practices for NATS. want you to review the APIs that we expose in this library to see that they are simple and yet powerful capturing, uh, important use cases across services. I also want you to review how this can be used in your pipeline's entire API and entire db core. ensure that we don't break changes, and we actually provide a useful, um, logic.
The library read is complete and tests pass. Version/adoption picture so far: go-nuts is at v0.4.1; entire-api pins v0.4.0, mirror-pipeline pins v0.3.0, and entiredb doesn't import go-nuts at all yet (still on its internal copy that go-nuts was extracted from). I'm waiting on the three repo-mapping agents to report back with the detailed NATS usage in each consumer before writing up the full review.
1. Dependencies (go.mod)
github.com/entireio/go-nuts v0.4.0— /Users/nodo/work/tasks/go-nuts-extension/entire-api/go.mod:19 (go.sum matches v0.4.0)github.com/nats-io/nats.go v1.51.0— go.mod:32 (note: one minor behind the go-nuts repo's own bump to v1.52.0)github.com/nats-io/nats-server/v2 v2.14.3— go.mod:31, used only for embedded test servers (multicellular harness + a few unit tests)
2. Process topology and connection lifecycle
Three binaries (docs/architecture.md:59-75): cmd/api dials no NATS (cmd/api/main.go:5); cmd/repo-activity and cmd/user-activity are workers wired in internal/server.
Local connection — internal/server/server.go
dialNATS=nuts.Connect(ctx, cfg.URL, nuts.WithName(serviceName))(server.go:251-253); comment says nuts.Connect replaced hand-rollednatsdial(rotation-aware mTLS fromENTIRE_INTERNAL_TLS_*env vars, reconnect-forever, 2s reconnect wait, lifecycle log handlers) (server.go:246-250; also cluster_conns_test.go:66).serveWorkerdials once,defer nuts.Drain(ctx, nc, serviceName, nil, natsDrainBackstop)(server.go:218-222);natsDrainBackstop = 12s, sized to exceed nuts DrainTimeout (5s) + nats.go publish flush (5s) (server.go:240-244). Health probe checksnc.IsConnected()(server.go:226-231).- Config:
NATS{URL: ENTIRE_NATS_URL, TLS*: ENTIRE_INTERNAL_TLS_*}(internal/config/config.go:138-146);ACTIVITY_NATS_REGIONSjurisdiction→URL map (config.go:376-385).
Remote (cross-region) connections — buildClusterConns (server.go:713-757): one nuts.Connect(ctx, url, nuts.WithName("activity-fwd-"+jurisdiction), nuts.WithRetryOnFailedConnect()) per remote jurisdiction (server.go:739-742), jetstream.New on each (server.go:719, 747); on error drains dialed conns (server.go:714-718, 744); shutdown cleanup drains remotes via nuts.Drain (server.go:369-373).
3. Per-file publish/consume map
All streams are provisioned declaratively in fleet (nack CRs); this binary only publishes and binds consumers. No KV, no object store, no request-reply anywhere; the only core-NATS subscription is the advisory reaper below.
| File | Role | Stream / subject / durable | Ack policy & shutdown |
|---|---|---|---|
internal/ingest/pipeline.go | Live ref consumer (pull/Fetch loop) | repo_refs_v1, durable activity-ingester, filter repo_refs_v1.> (pipeline.go:26, 96-103) | Hand-rolled: Fetch batch loop w/ faultBackoff (145-157, 291-300), FNV partitioned worker pool, dispatch-timeout NakWithDelay(2s) (169-175), Term on poison (218), NakWithDelay(backoffFor) exp 1s→300s (247, 305-323), MaxDeliver 8. Shutdown: ctx cancel → close worker chans + wg.Wait() (185-192) |
internal/ingest/jobs.go | Index-job publisher + two consumers (live + backfill) | Streams repo_index_jobs / repo_index_jobs_backfill (37-40), subject <stream>.<repoID> (248), durables activity-index-jobs[-backfill] (44, 87-92). Publisher: JS PublishMsg w/ Nats-Msg-Id dedup + natsmsg.Inject (283-295) | Consume callback → semaphore worker pool (493-512); heartbeat msg.InProgress ticker (1070-1087); Term on decode/schema (555, 561), Ack on repo-gone (582), Term + recordJobExhausted on final delivery (599-609), NakWithDelay(backoffFor) otherwise (615). MAX_DELIVERIES advisory reaper: core-NATS QueueSubscribe("$JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES.<stream>.<durable>", …) + stream.GetMsg/DeleteMsg (411-487). Stop() stops ConsumeContext, unsubscribes, waits in-flight (516-536) |
internal/analysis/jobs.go | Analysis publisher + consumer | Stream repo_checkpoint_analysis_jobs (27), durable analysis-facets (30), MaxDeliver 5, AckWait 60s (32-42, 197-204). Publish w/ dedup id + natsmsg.Inject (115-130) | heartbeat (336-352), Term poison/schema (239, 245), Ack on absent checkpoint (274-280), NakWithDelay(30s) (286, 304); Stop on ctx.Done (213-216, 223-228) |
internal/forwarder/publisher.go | Cross-region publisher (user activity → recipient's home region) | Subject user_activity_v1.<jur> into that region's stream, chosen from jurisdiction→JetStream map (256-263, 281-284); Nats-Msg-Id dedupe id (333-353); natsmsg.Inject (292); JS publish bounded by publishTimeout = 5s (31, 306-308) | Return-nil = caller acks (permanent drops), return err = caller naks (199-315). Conn lifecycle owned by server.go |
internal/forwarder/consumer.go | user-activity worker consumer | Stream user_activity_v1, durable activity-consumer, filter user_activity_v1.<jur> (25, 83-89), MaxDeliver 8 | Consume (93); Term poison/missing-recipient (127, 132), Ack foreign placement (144), Nak on DB error (157, 215); go func(){<-ctx.Done(); Stop()} (98-101) |
internal/settingsrelay/relay.go | Outbox→NATS broadcast publisher | Drains repo_settings_outbox rows; broadcasts stored (subject, payload, MsgID) (repo_settings.v1.<localJur>) to EVERY jurisdiction's JetStream concurrently (269-296); publishTimeout = 5s (60) | Ticker loop, per-tick budget, returns nil on ctx cancel (112-128); row deleted only after ALL dests PubAck (212-258). No trace inject (payload from outbox) |
internal/settingsconsumer/consumer.go | Settings fan-out consumer | Stream repo_settings_v1, durable repo-settings-consumer, binds whole stream repo_settings.v1.> (50-60) | go-nuts jsconsumer: jsconsumer.Start (97), jsconsumer.Process prologue (115); Nak transient, Ack no-placement/LWW-noop (141-208); run.Stop() (107-110) |
internal/repolifecycle/consumer.go | Repo seed/delete consumer | Stream resource_lifecycle_v1, durable repo-lifecycle-consumer, filter resource_lifecycle_v1.repo.* (23-42, contract.go:31-36) | go-nuts jsconsumer: Start (153), Process (179); Ack/Nak/Term in onEvent (184-247) |
internal/mirrorlifecycle/consumer.go | Mirror-placement consumer + directed publisher | Same stream, durable mirror-lifecycle-consumer, filter ...mirror.* (29, 37-47); directed settings push publishes into the target cell's repo_settings stream via dests[targetJurisdiction].PublishMsg, MsgID-keyed, publishTimeout = 5s (54, 278-324) | go-nuts jsconsumer: Start (122), Process (148); Ack config-gap/no-op, Nak transient (234-258) |
internal/reattribute/consumer.go | Identity re-attribution consumer (also re-publishes /me via forwarder.Publisher) | Same stream, durable author-reattribute-consumer, FilterSubjects github_handle.*, verified_email.* (26, 122-127) | Hand-rolled: heartbeat (149-164), 10-min msgTimeout (41, 218), Term poison (188), Ack non-actionable (194, 209), Nak transient (229) |
internal/userlifecycle/consumer.go | Account-purge consumer (user-activity worker) | Same stream, durable user-lifecycle-consumer, FilterSubjects resource_lifecycle_v1.user.* (24, 65-71) | Hand-rolled; Term/Ack/Nak (109-144) |
internal/githubmeta/consumer.go | GitHub repo metadata consumer | Stream github_meta_v1, durable github-meta-consumer, filter github_meta.v1.<jur>.<cluster> (22, 71-78; contract.go:25-34) | Hand-rolled; Term poison/missing-key (117, 124), Nak DB (133) |
internal/githubpr/consumer.go | GitHub PR metadata consumer | Stream github_pr_v1, durable github-pr-consumer, filter github_pr.v1.<jur>.<cluster> (22, 71-78; contract.go:26-34) | Same posture (105-143) |
internal/server/backfill.go | CLI publisher (repo-activity backfill) | Enqueues JobKindBackfill onto repo_index_jobs_backfill (69-82) | nuts.Connect + defer nuts.Drain (61-65) |
internal/server/enqueue_analysis.go | CLI publisher (enqueue-analysis) | Publishes analysis jobs (58-70) | nuts.Connect/Drain (50-54) |
internal/server/analysis_replay.go | CLI publisher (replay-deferred-analysis) | Re-enqueues deferred label jobs (76-87) | Lazy dialNATS + defer nuts.Drain (66-70) |
internal/multicellular/harness/nats.go | Test harness | Embedded nats-server per cell (one island per jurisdiction), provisions 6 streams (37-44); raw nats.Connect(… RetryOnFailedConnect, MaxReconnects(-1)) (64-66) | t.Cleanup(nc.Close, ns.Shutdown) (106) |
Test-only raw nats.Connect: forwarder/integration_test.go:102-107, ingest/jobs_dedupe_test.go:128, ingest/jobs_reaper_test.go:29, server/cluster_conns_test.go:29, httpapi/pipeline_integration_test.go:116. internal/facetrelay touches NATS only indirectly via forwarder.Publisher.
4. go-nuts APIs used (exact call sites)
nuts.Connect— server.go:252, 739; backfill.go:61; enqueue_analysis.go:50nuts.WithName— server.go:252, 740; backfill.go:61; enqueue_analysis.go:50nuts.WithRetryOnFailedConnect— server.go:741nuts.Drain— server.go:222, 371, 716; backfill.go:65; enqueue_analysis.go:54; analysis_replay.go:70jsconsumer.Config/Start/Process/Runner— repolifecycle/consumer.go:23, 133, 153, 179; mirrorlifecycle/consumer.go:37, 94, 122, 148; settingsconsumer/consumer.go:50, 77, 97, 115. All three setTracer: otelsetup.Tracer()to keep span attribution on the service tracer.natsmsg.Inject— forwarder/publisher.go:292; ingest/jobs.go:291; analysis/jobs.go:125natsmsg.StartConsumerSpan— ingest/pipeline.go:208; ingest/jobs.go:547; analysis/jobs.go:231; forwarder/consumer.go:118; githubmeta/consumer.go:107; githubpr/consumer.go:107; reattribute/consumer.go:177; userlifecycle/consumer.go:99
NOT used anywhere: natsmsg.ExtractHeader (directly — docs/observability.md:60-63 describes it, but only via StartConsumerSpan), natsmsg.KeepInProgress, nuts.ShutdownGroup, nuts.IsShutdownFetchErr, go-nuts/backoff, go-nuts/natsmsgtest, jsconsumer in the other seven consumers.
5. Hand-rolled logic where go-nuts is not used
- Consumer scaffolding duplicated 7×: forwarder, githubmeta, githubpr, reattribute, userlifecycle, ingest (pipeline + jobs), analysis each hand-roll
jetstream.New→CreateOrUpdateConsumer→Consume/Fetch→go func(){<-ctx.Done(); Stop()}→ Term/Nak/Ack policy — the exact patternjsconsumer.Start/Processcovers for the three migrated consumers (migration comments at repolifecycle/consumer.go:30-32 etc. say "unchanged by the migration"). - Ack-heartbeat tickers (
msg.InProgressevery AckWait/2) hand-rolled three times: ingest/jobs.go:1070-1087, analysis/jobs.go:336-352, reattribute/consumer.go:149-164 — precisely whatnatsmsg.KeepInProgressprovides. - Backoff: per-message exponential nak delay
backoffFor(1s doubling, 300s cap) at ingest/pipeline.go:305-323 (shared with jobs.go:615); fetch-fault backofffaultBackoff(1s<<n, 60s cap) at pipeline.go:291-300; fixedjobBackoff = 30sat analysis/jobs.go:41 — none usego-nuts/backoff. - Fetch-loop error handling: pipeline.go:139-183 hand-classifies Fetch faults (no
IsShutdownFetchErr). - MAX_DELIVERIES advisory reaper for WorkQueue orphans: core-NATS
QueueSubscribe+GetMsg/DeleteMsg, ingest/jobs.go:411-487. - Publish bounding: three copies of a 5s
publishTimeoutaroundPublishMsgbecause remote conns reconnect forever and nats.go buffers publishes — forwarder/publisher.go:31 + 306; settingsrelay/relay.go:60 + 284; mirrorlifecycle/consumer.go:54 + 309. - Test harness dials raw
nats.ConnectwithRetryOnFailedConnect/MaxReconnects(-1)(harness/nats.go:64-66) instead of nuts.Connect.
6. Docs
- docs/jobs.md — the job/pipeline inventory: two WorkQueue index-job streams (
repo_index_jobs,repo_index_jobs_backfill) +repo_checkpoint_analysis_jobs; job-kind table (commit/checkpoint/rollup/backfill/snapshot_refresh, lines 28-35); full consumer/durable/gate table (94-105);CONSUME_*gate semantics; the api process binds no consumers (6-8). - docs/architecture.md — binary/NATS matrix (59-75); two-stage chunked ingest (288-333); "There is no NATS supercluster" — per-region islands, direct publish into destination region (371-380); delivery/redelivery/dead-letter posture table: MaxDeliver 8, NakWithDelay exp backoff cap 300s, Term on poison, no DLQ (561-593); env table
ENTIRE_NATS_URL/ACTIVITY_NATS_REGIONS/ENTIRE_INTERNAL_TLS_*(648-658). - docs/observability.md — trace propagation "via message headers (
go-nuts/natsmsg, the shared-module lift of the oldinternal/otelnats) — Inject on every publish, ExtractHeader on every JetStream consume" (60-64); consumer/producer span table (47-58); baggage convention (36-43). - CLAUDE.md — points NATS/cross-cell changes at the
internal/multicellularreal-transport suite (lines 5-14). - Runbooks touching NATS ops: activity-backfill.md, backfill-kill-switch.md, checkpoint-analysis-{disable,enqueue,usage-cap}.md, settings-backfill.md, repo-seeding-backfill.md, outbox-relay-backlog.md.
7. Job/pipeline system
Yes — docs/jobs.md is exactly that: a NATS-WorkQueue job system. Live ref events (repo_refs_v1) are classified into typed, self-continuing paged jobs on repo_index_jobs (subject-per-repo, Nats-Msg-Id dedup, continuations re-enqueued by the consumer itself, ingest/jobs.go:750-757), with a separate throttled repo_index_jobs_backfill stream, plus an LLM-isolated repo_checkpoint_analysis_jobs stream, transactional outboxes drained to NATS (settingsrelay, facetrelay), and cross-region forwarding on user_activity_v1.</result>
<usage><subagent_tokens>189508</subagent_tokens><tool_uses>39</tool_uses><duration_ms>333994</duration_ms></usage>
</task-notification>
The entire-api map is in — rich material: three consumers already migrated to jsconsumer, but seven more still hand-roll the exact scaffold go-nuts provides, plus three hand-rolled ack-heartbeat tickers (natsmsg.KeepInProgress exists for this) and an unused go-nuts/backoff despite three local backoff implementations. Still waiting on the entiredb and mirror-pipeline maps before I write the full review.
NATS / JetStream + go-nuts usage map — mirror-pipeline
1. go.mod
github.com/entireio/go-nuts v0.3.0—/Users/nodo/work/tasks/go-nuts-extension/mirror-pipeline/go.mod:9github.com/nats-io/nats.go v1.50.0—go.mod:12(alsogithub.com/nats-io/nuid v1.0.1,go.mod:13, used byfailindex.RekickMsgfor fresh msg-ids)- Vendored go-nuts (
vendor/github.com/entireio/go-nuts/, confirmed viavendor/modules.txt) contains only the root package (connect.go,drain.go,shutdown.go,doc.go). The subpackagesjsconsumer,natsmsg,backoff,natsmsgtestdo not exist in v0.3.0 — the vendoreddoc.goexplicitly says: "Consumer scaffolding (jsconsumer), message helpers (natsmsg), and the redelivery/DLQ policy (backoff) will land here as those services converge onto the module. Tracking: COR-925." - Everything uses the legacy
nats.JetStreamContextAPI — zero imports of the modernnats-io/nats.go/jetstreampackage outside vendor (fanoutengine's doc comment says the modernjetstream.Consumeshape "lands when this graduates to the standalone module",pkg/fanoutengine/engine.go:9-13).
2. Every non-vendor NATS code path
Streams / KV inventory (all provisioned declaratively by fleet/nack; binaries only bind/publish, except gitjobs/webhookevents which have EnsureStream used by dev/loadtest)
| Stream/bucket | Subjects | Contract owner |
|---|---|---|
webhooks_github_v1 (Interest retention, 5m dedup, 48h MaxAge, 5 GiB, DiscardNew) | webhooks.github.v1.<event> | pkg/webhookevents/events.go:17-57, stream.go:48-90 |
mirror_git_v1 (WorkQueue, 1s dedup, 6h MaxAge, 1 GiB) | mirror.git.v1.<jur>.<cluster>.<src>.<op> | pkg/gitjobs/jobs.go:18-48,344-354 |
github_meta_v1 | github_meta.v1.<jur>.<cluster> | pkg/metajobs/jobs.go:32-43 |
github_pr_v1 | github_pr.v1.<jur>.<cluster> | pkg/prjobs/jobs.go:37-48 |
github_push_author_v1 | github_push_author.v1.<jur>.<cluster> | pkg/pushauthorjobs/jobs.go:26-35 |
repo_ops_v1 | repo.ops.v1.<jur>.<cluster>.mirror_done | pkg/donejobs/jobs.go:35-54 |
github_meta_backfill (work queue) | github_meta_backfill.jobs | cmd/meta-fanout/backfill.go:31-37 |
github_pr_backfill (work queue) | github_pr_backfill.jobs | cmd/meta-fanout/backfill_prs.go:30-32 |
resource_lifecycle_v1 | resource_lifecycle_v1.> | cmd/meta-fanout/lifecycle.go:25-43 |
webhooks_github_dlq | webhooks.github.dlq.<source> | pkg/webhookdlq/dlq.go:48-64 |
KV bucket mirror_sync_failed | (KV) | pkg/failindex/failindex.go:28-29 |
Shared plumbing
internal/natspub/natspub.go— the shared JS publish core behind all typed publishers: producer span,Nats-Msg-Idheader, 5s bounded pub-ack wait vianats.Context(pubCtx)(natspub.go:26-40,80-131).AckWait = 5satnatspub.go:35.pkg/otelnats/nats.go— hand-rolled W3C trace-contextInject/Extractovernats.Header(nats.go:39-53). This is the local equivalent of a futurenatsmsg.Inject/Extract.internal/natsregions/natsregions.go— multi-region island transport:ParseConfig(FANOUT_NATS_REGIONSenv,:45),DialRemotesdials each remote vianuts.Connect(..., nuts.WithRetryOnFailedConnect(), nuts.WithNATSOptions(nats.DisconnectErrHandler…, nats.ReconnectHandler…))(:193-224),Health(local conn gates/readyz, remotes only feed a gauge,:111-145), genericResolve(:159).pkg/fanoutengine/— the shared consumer runtime (see §3/§4).engine.go,route.go,dlq.go,routememo.go.- Typed publishers (all delegate to natspub):
internal/mirrorjobs/publisher.go:30-51(mirror.git.publish, msg-idop/kind/owner/repo/ulid),pkg/metajobs/publisher.go:31,pkg/prjobs/publisher.go:31,pkg/pushauthorjobs/publisher.go:34,pkg/donejobs/publisher.go:30-42(msg-idulid/status),pkg/webhookevents/publisher.go:81-106(msg-id = GitHub delivery-id; also KEDA-scaling publish-latency metrics).
Services
cmd/webhook-ingest (HTTP → NATS producer). nuts.Connect at main.go:55, defer nuts.Drain(ctx, nc, "webhook-ingest", nil, 12s) at main.go:59, nc.JetStream() at :61. Publishes ForwardedWebhook to webhooks.github.v1.<event> via webhookevents.Publisher (forward.go:137); publish failure → HTTP 500 so GitHub redelivers (dedup by delivery-id). Readiness = nc.IsConnected() (main.go:81-86).
cmd/webhook-forwarder (consumer → HTTP POST to entire-web). Hand-rolled pull consumer, no fanoutengine: js.PullSubscribe(webhooks.github.v1.>, durable[default "webhook-forwarder"], ManualAck, AckWait 30s, MaxDeliver 8, InactiveThreshold 72h) (forwarder.go:128-133) with ensureInactiveThreshold reconcile via ConsumerInfo/UpdateConsumer (forwarder.go:100-121), serial Fetch(1, MaxWait 5s) loop using nuts.IsShutdownFetchErr (forwarder.go:176). Disposition: 2xx→Ack; 401/403→DLQ capture + Term; other 4xx→DLQ+Term; 5xx/network→NakWithDelay(30s) or DLQ+Term on final delivery (forwarder.go:287-351); panic→recover + NakWithDelay (:230-241). Lifecycle: nuts.Connect with webhookdlq.PermErrorHandler dial option when DLQ enabled, defer nuts.Drain(..., 12s) (main.go:76-80), and a bounded wait on forwarderDone before Drain closes the conn (main.go:148-158).
cmd/fanout (webhook → mirror_git_v1 git-sync jobs, cross-region). Uses fanoutengine with a worker pool: config at consumer.go:57-73 (subject webhooks.github.v1.>, env FANOUT_DURABLE required per region, AckWait 30s, MaxDeliver 8, NakDelay 30s, InactiveThreshold 72h, FANOUT_DISPATCH_CONCURRENCY/FANOUT_FETCH_BATCH pool with fetchBatch≤concurrency AckWait bound main.go:288-301). Resolver: push event → entire-core RepoLookup (mTLS HTTP, cached) → one gitSyncRoute per placement (dispatch.go:46-129); route publishes via mirrorjobs.NewPublisher(js).PublishGitSyncJob. Opt-in DLQ (ENTIRE_WEBHOOK_DLQ_ENABLED) → webhookdlq.Publisher + advisory Backstop goroutine (main.go:196-205). Multi-region: natsregions.DialRemotes → fanoutengine.Regions.ByKey[cluster_id] = remoteJS (main.go:150-173). Lifecycle: nuts.Connect at main.go:126, defer nuts.Drain(..., 12s) at :130, bounded consumer-done wait at :258-262.
cmd/core-fanout (push → github_push_author_v1). Same fanoutengine pattern, serial (no pool), subject webhooks.github.v1.push, durable default core-fanout (main.go:32,86-89; retry envelope consumer.go:23-50 — MaxDeliver 8 must keep redelivery span < the stream's 5m dedup window). nuts.Connect main.go:105, nuts.Drain :109.
cmd/meta-fanout (4 consumers + 2 enqueue subcommands; modes full/lifecycle, main.go:44-71):
- Live webhook fan: fanoutengine on
webhooks.github.v1.>, durable defaultmeta-fanout(main.go:38,254-266; configconsumer.go:60-68). Resolver handlesstar|fork|repository→metajobs.Jobroutes andpull_request→prjobs.Jobroutes (dispatch.go:55-263). github_meta_backfillconsumer: fanoutengine ongithub_meta_backfill.>, durablemeta-fanout-backfill,TermOnExhaustion: true(work-queue drain, COR-762) (backfill_consumer.go:26-30,259-267); worker fetches repo metadata from GitHub with STS-minted installation tokens, throttled per-replica.github_pr_backfillconsumer: same shape, durablemeta-fanout-backfill-prs,TermOnExhaustion: true(backfill_prs_consumer.go:26-30,238-246).- Lifecycle fan: hand-rolled consumer (not fanoutengine) on local
resource_lifecycle_v1.>, durablemeta-fanout-lifecycle(lifecycle.go:207-261):PullSubscribe(ManualAck, AckWait 30s, MaxDeliver 8), serial Fetch(1); re-publishes subject+payload verbatim into every remote region's stream withEntire-Lifecycle-Fanned-Byone-generation echo guard (lifecycle.go:53-63,326-331) and preserved/derivedNats-Msg-Id(lifecycle.go:356-372); no Term path; partial fan failure → NakWithDelay(30s). NOTE: itsconsumeLoop(lifecycle.go:244-261) does not callnuts.IsShutdownFetchErr— a shutdown-drain fetch error logs as "Lifecycle fetch error". Onmirror.createdit also best-effort enqueues a meta backfill (mirror_seed.go:66+). - Subcommands
backfill/backfill-prsenqueue jobs with a barejs.PublishMsg+Nats-Msg-Id = owner/repo:observedAtand 5s pub wait (backfill.go:86-101,141-145;backfill_prs.go:131-145), each with its ownnuts.Connect(WithName…)/nuts.Drain. - Lifecycle wiring:
nuts.Connectmain.go:172,nuts.Drain:176, per-consumer bounded drain loopmain.go:427-440, readiness gate depends on mode (main.go:463-479).
cmd/worker (mirror-worker; the heaviest consumer):
- Connection:
nuts.NewShutdownGroup(ctx)+defer g.Shutdown()(main.go:230-231),nuts.Connect(ctx, natsURL, nuts.WithName("mirror-worker"), nuts.WithNATSOptions(nats.ErrorHandler(advisoryPermErrorHandler(metrics))))(main.go:237-239),g.AddConn("mirror-worker", nc)(:244), loops registered viag.Go(:393,403) — the cancel→join→drain ordering (COR-923). - Consumer:
js.PullSubscribe("mirror.git.v1.<jur>.<cluster>.>", "mirror-git-v1-<jur>-<cluster>", ManualAck, AckWait 15m, MaxDeliver 10)(consumer.go:23-28,106-124), exponential subscribe-retry 1s→30s (:117-155), serialFetch(1, MaxWait 5s)withnuts.IsShutdownFetchErr(:167). KEDA scales on the same durable (consumer.go:102-107; pending gauge viajs.ConsumerInfoinmetrics.go:452-477). - Per message:
otelnats.Extract→ consumer span → panic-recover→NakWithDelay (:182-266); subject/payload validation Terms with failure-index record; in-progress ack extendermsg.InProgress()every AckWait/3 = 5m during long syncs (consumer.go:29-33,322,705-740); 30m sync budget (:34-42,331-336). - Disposition tree
disposeResult(:391-485): Ack (publishdonejobsreadycompletion before Ack,:401-407,501-523); Noop (empty source → ready + Ack, no index clear); Term (permanent → record failindex + best-effortfailedcompletion, fail-open on KV errors); Nak →NakWithDelay(30s), escalating to Term + failindexmax_deliverieson the final delivery (isLastDelivery,:602-607); Raced (concurrent-ref) → Nak without failed-completion. - Advisory backstop (
advisory.go):nc.ChanQueueSubscribeon$JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES.mirror_git_v1.>and…MSG_TERMINATED.mirror_git_v1.>in queue groupmirror-worker-failindex-advisory(:62-69,158-169), 1024-buffer channel drained by 16 workers (:81-91,197-217),js.GetMsg(stream, seq)to refetch the job (:240), then the samefailindex.RecordFailureCAS (idempotent by stream seq). Missing grants / slow-consumer drops surface via the connectionErrHandler(:339-359). - Failure index:
failindex.Open(js, bucket)binds KV, degrades to nil if unbound (main.go:351-361), with corrupt-entry and max-age-eviction handlers (main.go:328-350).
cmd/rekicker (CronJob). nuts.Connect(ctx, natsURL, nuts.WithNATSOptions(nats.ErrorHandler(permErrorHandler(metrics)))) + defer nc.Close() (no Drain; run-to-completion) (main.go:105-109), failindex.Open (:116), one pass: Keys(ctx) (a hand-driven kv.WatchAll(IgnoreDeletes, MetaOnly) listing with sentinel/close semantics, failindex.go:380-439), per-key Get, skip permanent/bad-subject, then js.PublishMsg(failindex.RekickMsg(ulid, fs)) — the stored subject+payload verbatim with Mirror-Rekick header + fresh nuid msg-id (rekick.go:223, failindex.go:47-53). Per-tick cap (500) + rate throttle (20/s) + 20-consecutive-error circuit breaker + 600s pass deadline (main.go:74-96, rekick.go:50,97-249). Dry-run mode via ENTIRE_REKICK_DRY_RUN.
cmd/mirror-pipeline-admin (operator CLI). dialJetStream = nuts.Connect(WithName("mirror-pipeline-admin"), WithNATSOptions(ErrorHandler(permErrorHandler))), caller nc.Close() (nats.go:33-51). Ops: DLQ scan via ephemeral js.SubscribeSync(subject, nats.OrderedConsumer(), nats.DeliverAll()) (dlq.go:114), js.GetMsg(webhooks_github_dlq, seq) (dlq.go:225), replay = js.PublishMsg of the stored envelope back onto webhooks.github.v1.> (dlq.go:379-387); failindex list/show/delete via failindex.Open/Keys/Get/Delete (failindex.go:51-58); manual rekick via js.PublishMsg(failindex.RekickMsg(...)) (rekick.go:109); backfill queue status/purge via js.StreamInfo/js.ConsumerInfo/js.PurgeStream(&nats.StreamPurgeRequest{Sequence}) (backfill.go:106,117,239). webhooks.go/githubapp.go are GitHub-API-only (no NATS).
Load tools: cmd/loadgen — nuts.Connect + nc.Close() (main.go:208-218), publishes synthetic mirror jobs via mirrorjobs.NewPublisher. cmd/loadtest-jetstream — nuts.Connect, gitjobs.EnsureStream (the only production-code AddStream/UpdateStream path, pkg/gitjobs/jobs.go:310-336; mirrored by webhookevents.EnsureStream, stream.go:14-40) and js.PurgeStream (main.go:50-75). cmd/mockgithub — no NATS.
pkg/webhookdlq/advisory.go — the DLQ crash backstop shared by fanout + forwarder: per-durable advisory subjects $JS.EVENT.ADVISORY.CONSUMER.{MAX_DELIVERIES,MSG_TERMINATED}.<stream>.<durable> (advisory.go:128-134), queue group <durable>-dlq-advisory (:140), ChanQueueSubscribe + worker-pool drain + js.GetMsg + DLQ publish, deduped against the inline capture by the same Nats-Msg-Id (dlq.go:166-172); PermErrorHandler for silent-rejection surfacing (:458+). The DLQ publisher itself deliberately hand-rolls its publish (not natspub) because it detaches the pub-ack deadline from the shutdown ctx and needs the PubAck.Duplicate bit (publisher.go:19-32,116-127).
3. go-nuts APIs used (v0.3.0 root package only)
| API | Call sites |
|---|---|
nuts.Connect | cmd/webhook-ingest/main.go:55, cmd/webhook-forwarder/main.go:76, cmd/fanout/main.go:126, cmd/core-fanout/main.go:105, cmd/meta-fanout/main.go:172, cmd/meta-fanout/backfill.go:141, cmd/meta-fanout/backfill_prs.go:131, cmd/worker/main.go:237, cmd/rekicker/main.go:105, cmd/mirror-pipeline-admin/nats.go:37, cmd/loadgen/main.go:208, cmd/loadtest-jetstream/main.go:87, internal/natsregions/natsregions.go:197 |
nuts.WithName | worker main.go:238, admin nats.go:38, meta-fanout backfills backfill.go:141/backfill_prs.go:131 |
nuts.WithNATSOptions | fanout main.go:126, forwarder main.go:76, worker main.go:239, rekicker main.go:105, admin nats.go:39, natsregions :199 — always to attach nats.ErrorHandler (perm/slow-consumer classifiers) or disconnect/reconnect handlers |
nuts.WithRetryOnFailedConnect | internal/natsregions/natsregions.go:198 (remote islands must not crashloop boot) |
nuts.Drain | ingest main.go:59, forwarder main.go:80, fanout main.go:130, core-fanout main.go:109, meta-fanout main.go:176 + backfill.go:145 + backfill_prs.go:135 — all with a 12s backstop documented as "> DrainTimeout (5s) + publish-flush (5s)" and paired with a bounded consumer-done wait before the deferred Drain |
nuts.IsShutdownFetchErr | cmd/worker/consumer.go:167, cmd/webhook-forwarder/forwarder.go:176, pkg/fanoutengine/engine.go:351,406 |
nuts.NewShutdownGroup / Go / AddConn / Shutdown | only cmd/worker: main.go:230-231,244,393,403 |
Not used anywhere (don't exist in v0.3.0): jsconsumer.*, natsmsg.*, backoff.*, natsmsgtest.*, TLSConfigFromFiles (TLS comes from env inside Connect).
Two lifecycle idioms coexist: worker uses ShutdownGroup (cancel→join→drain); all other long-lived services use defer nuts.Drain + an explicit bounded select on a consumer-done channel; CronJob/CLI binaries (rekicker, admin, loadgen, loadtest) use bare nc.Close().
4. Hand-rolled logic NOT in go-nuts (the extraction surface)
- Consumer scaffold (the future
jsconsumer): three near-identical copies —pkg/fanoutengine/engine.go:286-419(Runsubscribe-with-1s→30s-exponential-backoff,consumeLoopserial Fetch(1)/consumeLoopPoolunbuffered-channel worker pool,Ready()atomic for /readyz),cmd/worker/consumer.go:110-180,cmd/webhook-forwarder/forwarder.go:123-186, pluscmd/meta-fanout/lifecycle.go:207-261(which lacks the IsShutdownFetchErr check). - Disposition/classification: fanoutengine's
InputError→Term / plain error→NakWithDelay / final-delivery Term+DLQ (TermOnExhaustion) tree (engine.go:453-697); worker's richerclassifyDisposition(ack/noop/term/nak/raced,consumer.go:563-580) driven by asyncError{permanent}wrapper (syncer_errors.go). - Redelivery/backoff policy (the future
backoffpackage): flatNakWithDelay(30s)everywhere (no escalating schedule;nats.BackOffdeliberately dropped —forwarder.go:39-50),MaxDeliver8 (fanouts/forwarder, bounded by stream dedup-window invariants) or 10 (worker),isLastDelivery/isFinalDeliverychecks bymeta.NumDelivered. - Message helpers (the future
natsmsg):pkg/otelnatsInject/Extract (nats.go:39-53); consumer-span +messaging.*attribute stamping repeated in 4 places (engine.go:421-445, workerconsumer.go:186-221, forwarderforwarder.go:188-208, lifecyclelifecycle.go:263-283);startInProgressExtender(KeepInProgress equivalent, workerconsumer.go:718-740);natspub.Publishproducer core (internal/natspub/natspub.go:80-131). - Advisory backstops ×2 (worker failindex
cmd/worker/advisory.go; webhook DLQpkg/webhookdlq/advisory.go) — ChanQueueSubscribe + queue group + worker pool + GetMsg + idempotent record. - DLQ:
pkg/webhookdlq(verbatim-envelope dead letters, per-source subjects, sha256 fallback msg-id, fail-open capture) +fanoutengine.DLQsink interface (dlq.go:26-28). - Fail-index:
pkg/failindexKV CAS upsert with seq dedup, revision-guarded clear/evict, ctx-boundedKeyswatcher (failindex.go:206-439); re-driven bycmd/rekickerand self-cleared by the worker. - Route memo: in-process per-delivery region-success memo so a partial fan-out Nak doesn't re-publish already-acked regions past
mirror_git_v1's 1s dedup window (pkg/fanoutengine/routememo.go, usedengine.go:540-556). - Region routing:
internal/natsregions+fanoutengine.Regions(deliberately duplicated atroute.go:66-95to keep the engine free ofinternal/for future extraction).
5. Docs
README.md— full architecture diagram: GitHub webhook →webhook-ingest→webhooks_github_v1→ four independent durables (fanout→mirror_git_v1→worker→entiredb;meta-fanout→github_meta_v1/github_pr_v1;core-fanout→github_push_author_v1;webhook-forwarder→entire-web). Worker records terminal failures in KV; rekicker re-drives on cron.docs/design/mirror-sync-failure-index.md— three-layer recovery model rationale (why KV index over DLQ or O(repos) reconcile), the nack-owned KV bucket, in-process + advisory dual-write.docs/design/mirror-worker-capacity.md,webhook-ingest-autoscaling.md— KEDA scaling off the durable's NumPending and publish-latency p95.docs/multi-region/—multi-region-fanout-design.md(two-stage model: webhook→home sync, then EntireDB post-receive→replica fan),fanout-engine-design.md+fanout-engine-l1-design.md(the engine extraction; explicitly cites go-nuts/COR-925 as the destination module),fanout-route-memo.md,webhook-flowchart.md(per-stage ASCII flowcharts),multi-region-fanout-impl-plan.md. (natsregions.go:14references across-region-nats-cutover.mdthat is not present in docs/.)docs/runbooks/—webhook-dlq.md(ordered DLQ enablement: fleet stream + grants beforeENTIRE_WEBHOOK_DLQ_ENABLED),webhook-redelivery.md(GitHub-side redelivery),github-meta-backfill.md,github-pr-backfill.md,mirror-stuck-refs-without-objects.md.docs/loadtest-design.md/loadtest-local.md— loadgen/mockgithub/loadtest-jetstream harness.
Summary judgment for the caller: this repo is a heavy consumer of go-nuts's connection-lifecycle slice (Connect/Drain/IsShutdownFetchErr/ShutdownGroup, 13 Connect sites) but pins v0.3.0, which predates jsconsumer/natsmsg/backoff — all of that behavior currently lives hand-rolled here, chiefly in pkg/fanoutengine, internal/natspub, pkg/otelnats, the worker's consumer/advisory files, and the forwarder/lifecycle loops, which are the natural donors for the next go-nuts slices.</result>
<usage><subagent_tokens>243907</subagent_tokens><tool_uses>59</tool_uses><duration_ms>526662</duration_ms></usage>
</task-notification>
The mirror-pipeline map is in as well — it's the heaviest go-nuts consumer (13 Connect sites) but pinned to v0.3.0, which predates the jsconsumer/natsmsg/backoff subpackages entirely; its fanoutengine, otelnats, and worker heartbeat/backoff code are hand-rolled equivalents of exactly those. One report left (entiredb), then I'll write up the full review.
NATS / JetStream usage map — /Users/nodo/work/tasks/go-nuts-extension/entiredb
1. Dependencies (go.mod)
github.com/entireio/go-nutsis NOT imported. No hit ingo.mod,go.sum, anymodules/*/go.mod(analytics, authmtls, jwtverify, ratelimit, resend), or any.gofile.github.com/nats-io/nats.go v1.52.0—/Users/nodo/work/tasks/go-nuts-extension/entiredb/go.mod:40github.com/nats-io/nats-server/v2 v2.14.3—go.mod:39; used only to embed a JetStream server in tests (test/testutils/natstest.go:7)- Indirect:
nats-io/jwt/v2 v2.8.2,nkeys v0.4.16,nuid v1.0.1.
Instead of go-nuts, the repo has two internal helper packages that cover the same ground:
internal/natsx/— connection drain + supervised pull-consumer loopinternal/natsmsg/— trace-context extraction, AckWait heartbeat, DLQ helper (note: same package name as go-nuts'natsmsg, but a separate implementation underentire.io/internal/natsmsg)
2. Every NATS code path
2a. Shared infrastructure (hand-rolled, go-nuts-equivalent)
internal/natsx/natsx.go — natsx.Drain(ctx, name, nc, timeout) (natsx.go:31-51): installs a ClosedHandler, calls nc.Drain(), blocks until CLOSED or timeout (because nats.Conn.Drain() is async). Used as the cleanup for nearly every entire-core connection.
internal/natsx/pullconsumer.go — natsx.PullConsumer (pullconsumer.go:33-121): the supervise loop shared by all pull consumers. CreateOrUpdateConsumer → Consume with jetstream.PullMaxMessages(FetchBatch) + ConsumeErrHandler; on ConsumeContext.Closed() it recreates after exponential backoff (backoffSleep, pullconsumer.go:131-139, doubles to RetryBackoffMax, resets only after a healthy run ≥ max, pullconsumer.go:113-115); jetstream.ErrStreamNotFound logs Warn-not-Error (pullconsumer.go:68); on ctx cancel calls cc.Stop() (pullconsumer.go:105).
internal/natsmsg/:
tracecontext.go:16-21ExtractTraceContext(ctx, header);HeaderCarrierOTel TextMapCarrier adapter (tracecontext.go:25-45);ClampToInt64(tracecontext.go:49).keepinprogress.go:34-70KeepInProgress(inProgress, ackWait)— ticksmsg.InProgress()at AckWait/3, capped at 15 ticks (~5×AckWait,keepinprogress.go:18), idempotent blocking stop.deadletter.go:76-96DeadLetter(ctx, pub, dlqSubject, msg, reason)— republishes poison withNats-Dlq-Reason/Origin-Subject/Delivered/Stream-Seqheaders (deadletter.go:22-26), 15 s publish timeout (deadletter.go:17);SubjectTokensanitizer (deadletter.go:41-61). Callers Ack the original only if the DLQ publish succeeded (WorkQueue semantics — COR-944).natsmsgtest/fakemsg.go— fakejetstream.Msgfor tests.
2b. Connection sites (nats.Connect)
| Site | Binary | Options / lifecycle |
|---|---|---|
internal/entirecore/serve_mode_global.go:353 (connectNATSURL, :312-359) | entire-core (all modes + workers) | mTLS mandatory (:313-319), nats.DrainTimeout(5s), RetryOnFailedConnect(true), MaxReconnects(-1), ReconnectWait(2s), Disconnect/Closed/Reconnect/ReconnectErr handlers (:321-352); TLS config re-reads cert per handshake (buildNATSTLSConfig, :367-383). Shutdown: natsx.Drain per connection, natsDrainTimeout = 5s (:38). |
internal/entireserver/ref_events_publisher.go:77 (openRefEventsPublisher, :18-94) | entire-server (data plane) | Same option set minus DrainTimeout (:51-76); SVCTOK-cert-preferred mTLS (:34-45); returns legacy nc.JetStream() context; cleanup is plain nc.Close (:93). |
internal/ciwebhooks/serve.go:197 (dialNATS, :169-211) | entire-ci-webhooks | mTLS, RetryOnFailedConnect, MaxReconnects(-1) (:181-196); shutdown via defer natsx.Drain(ctx, "ci-webhooks", nc, 5s) (serve.go:59). |
test/testutils/natstest.go:34 (EmbeddedJetStream) | tests | Embedded nats-server/v2 with JetStream in t.TempDir(), torn down via t.Cleanup (ADR docs/adrs/20260704-embedded-nats-for-e2e-tests.md). |
entire-core opens a separate connection per subsystem, each with its own natsx.Drain cleanup: gitjobs publisher (serve_mode_global.go:256-275), lifecycle emitter (:284-303), perms-webhook consumer (perms_wiring.go:84-135), read-model consumer (perms_wiring.go:217-256), repo-ops consumers — 3 consumers, 1 conn (repoops_wiring.go:326-391), mirror-done (mirrordone_wiring.go:66-96), perms-backfill pub/consumer (permsbackfill_wiring.go:29-38, 65-93), verified-email (verifiedemail_wiring.go:43-87), transactional email (transactionalemail_wiring.go:123-138), perms-sweep worker (perms_sweep_worker.go:83-103), sweep enumerator CronJob (permsreconciler_runonce.go:108-121, defer nc.Close() — publish-only run-once). Shutdown ordering (COR-923): cancel bg ctx → join consumer loops bounded by subsystemStopTimeout = 15s (serve.go:38-41, :328-336) → deferred natsx.Drains run last; overall listener drain 25 s (serve.go:36).
2c. Streams, publishers, consumers
repo_refs_v1 (subjects repo_refs_v1.<repo-ulid>; fleet-owned stream, never created here — ref_events_publisher.go:87-90)
- Publisher:
refevents/— transactional outbox. Pushes enqueue rows in-tx (refevents/outbox.go, tableref_event_outbox,outbox.go:29); relay drains via taskloop every 1 s (internal/entireserver/serve.go:174-189), 64 hash shards each under a pg advisory lock (outbox.go:74-85, 113-127), batch 64, publish withPublishMsg(..., nats.Context(spanCtx))(outbox.go:484), delete-on-PubAck with detached 5 s commit grace (outbox.go:58-66); at-least-once, dedup vianats.MsgIdHdr= content-derivedNATSMsgID(refevents/refevents.go:235-237). A best-effort directEmitterexists for sim/tests (refevents.go:102-146; wired ininternal/sim/env.go:788-793againstinternal/sim/nats_recorder.go). - Consumer: ci-webhooks dispatcher (
internal/ciwebhooks/dispatcher/dispatcher.go) — durableci-webhooks, filterrepo_refs_v1.>(dispatcher.go:37-42), AckWait 2 m, MaxDeliver 10, NakDelay 5 s, FetchBatch 1 (:44-48), runs onnatsx.PullConsumer(:128-131); decode→fan out to Buildkite→Ack; 5xx/network →NakWithDelay; poison →Term. Health probe checksnc.IsConnectedequivalent (internal/ciwebhooks/probes/probes.go:35).
mirror_git_v1 (subjects mirror.git.v1.<jur>.<cluster>.github.sync_repo; stream owned by out-of-repo mirror-pipeline — core/mirrorrepo/gitjobs/jobs.go:32-35)
- Publisher only:
core/mirrorrepo/gitjobs/publisher.go:57-129PublishSyncRepo—Nats-Msg-Id=op/source-kind/owner/repo/ulid(:96-99), OTel producer span + header inject (:79-106), syncPublishMsgwithnats.Context. Called from mirror create (core/api/mirrors.go:860); publish failure returns 502 to the caller rather than 2xx (mirrors.go:769-787). Boot fails if no NATS URL in regional/standalone (serve_mode_global.go:260-263).
repo_ops_v1 (WorkQueue; subjects repo.ops.v1.<jurisdiction>.<cluster>.<op>; contract owned here — internal/entirecore/repoops/jobs.go:1-58)
- Stream contract:
WorkQueuePolicy,FileStorage,DiscardNew, dup window 10 s, MaxAge 48 h (repoops/stream.go:87-95,jobs.go:41-58);EnsureStreamboot-time create/reconcile, replicas fromENTIRE_REPO_OPS_STREAM_REPLICAS(stream.go:23-70); fleet "nack" operator owns it declaratively in prod. - Ops on the bus:
- teardown / suspend / resume — publisher:
repoops/publisher.go:55-131(global core's webhook fan-out,repoops_wiring.go:54-292; msg-idop/ULID/observedAtpublisher.go:110). Consumers: regional core, one durable per (op, jurisdiction):entirecore-repoops-<op>-<jur|all>, filterrepo.ops.v1.<jur|*>.*.<op>(repoops/consumer.go:26-39, 267-279); AckExplicit, AckWait 2 m, MaxDeliver -1 (unlimited) (consumer.go:46-60), NakDelay 5 s growing geometrically to 30 s (consumer.go:796-809), FetchBatch 1;EnsureConsumerpre-creates the shared durable at boot (consumer.go:312-317); handler: trace extract + consumer span (:346-354),KeepInProgressheartbeat (:412), last-writer-wins / attachment / installation-suspended gates (:436-673), poison →natsmsg.DeadLettertorepo.ops.dlq.v1.<op>.<reason>then Ack (:715-733), panic recovery dead-letters (:758-772). Wiring:repoops_wiring.go:320-392, run inserve.go:237-241. - mirror_done — consumer only (producer is mirror-pipeline worker):
internal/entirecore/mirrordone/filterrepo.ops.v1.<jur|*>.*.mirror_done(event.go:56-61), runs onnatsx.PullConsumer(consumer.go:157-166), flipsmirror_repos.statusready|failed; poison → DLQrepo.ops.dlq.v1.mirror_done.<reason>; wiredmirrordone_wiring.go:56-97against the in-cell NATS. - perms_backfill — publisher
internal/entirecore/permsbackfill/publisher.go(mirror create enqueues collaborator backfill,core/api/mirrors.go:406); consumer same regional process, filterrepo.ops.v1.<jur|*>.*.perms_backfill(permsbackfill/event.go:48-53), durable JetStream hop on purpose (event.go:14-16); wiringpermsbackfill_wiring.go. - perms_sweep — publisher: hourly
sync-accessible-reposCronJob enumerator (permsreconciler_runonce.go:107-166; subjectrepo.ops.v1.<jur|global>.sweep.perms_sweep, cluster slot = literal"sweep",permssweep/event.go:43-70; 15 s publish timeoutpublisher.go:31; per-run liveness canarypublisher.go:111-153). Consumer:perms-sweep-workerDeployment (perms_sweep_worker.go:45-140;cmd/entire-core/cli/permssweepworker.go) — shared durableentirecore-perms-sweep, N concurrent Consume slots (default 10,permssweep/consumer.go:26-115), AckWait 2 m + KeepInProgress (consumer.go:322), MaxDeliver 5 (jobs re-derivable), busy-lease Acks (ErrInstallationBusy), DLQrepo.ops.dlq.v1.perms_sweep.<reason>. ADR:docs/adrs/20260706-perms-sweep-nats-fanout.md.
- teardown / suspend / resume — publisher:
- Backlog observability: stream oldest-age gauge from
StreamInfo(repoops/backlog.go:49-71, COR-918 rail-loss alert).
webhooks_github_v1 (subjects webhooks.github.v1.<event>; US-only, nack-owned; producer is out-of-repo cmd/webhook-ingest)
- Metadata consumer:
internal/entirecore/permswebhook/durableentirecore-webhook-meta,FilterSubjects= installation / installation_repositories / repository (events.go:31-45), AckWait 2 m, MaxDeliver -1, FetchBatch 1, InactiveThreshold 72 h (interest-retention stream protection,consumer.go:57-73), runs onnatsx.PullConsumer(consumer.go:205-208), KeepInProgress during teardown fan-outs (consumer.go:276), poisonTerm(:305,328,348). Runs in global + standalone (serve_mode_global.go:196,serve_mode_standalone.go:224). - Read-model consumer: second durable
entirecore-webhook-perms(permswebhook/permsapply.go:25) for member/team/membership/organization events →github_user_repo_access+ SpiceDB projection; gated byENTIRE_GITHUB_PERMS_READ_MODEL_ENABLED(perms_wiring.go:183-256).
github_push_author_v1 (subjects github_push_author.v1.<jurisdiction>.<cluster>; per-region island, producer is mirror-pipeline core-fanout)
- Consumer:
internal/entirecore/verifiedemailwebhook/filtergithub_push_author.v1.<jur>.>(events.go:35-57),natsx.PullConsumer(consumer.go:163-166), writesverified_emails; runs in-process in regional serve (serve_mode_regional.go:259) or as the dedicatedverified-email-consumerDeployment (internal/entirecore/verified_email_consumer.go,cmd/entire-core/cli/verified_email_consumer.go). Backlog gauge via legacyConsumerInfo(verifiedemailwebhook/backlog.go).
email_v1 (subject email_v1.requested, filter email_v1.>, operator-owned — core/email/event.go:10-18)
- Producer: transactional outbox
email_outboxtable (core/email/outbox.go:18, 57-70), per-region relay claims own-jurisdiction rowsFOR UPDATE SKIP LOCKEDevery 1 s (outbox.go:121-203), core-publish withnats.MsgIdHdr = eventID(outbox.go:178-180). - Consumer:
core/email/consumer.godurableentirecore-email-<jurisdiction>(consumer.go:86-90), AckWait 2 m, MaxDeliver 50 (consumer.go:33), NakDelay 30 s,natsx.PullConsumer(consumer.go:134-143),ErrPoison→ Term, transient → NakWithDelay; max-deliver-exhausted metric (consumer.go:169-173). Backlog gauges fromConsumerInfo(core/email/backlog.go:31-70). Wiring + capture/drain feature gates:transactionalemail_wiring.go:20-169. Doc:docs/entire-core/transactional-email.md, runbookdocs/runbooks/transactional-email.md.
resource_lifecycle_v1 (subjects resource_lifecycle_v1.<resource_type>.<op> — core/resourceevents/resourceevents.go:34-40)
- Publisher only, core NATS (no JetStream, no PubAck), best-effort at-most-once by design (
resourceevents.go:5-12):Emitter.Publishover*nats.Conn(resourceevents.go:193-279), producer span + trace inject, failures logged and swallowed. ~30 call sites incore/api,core/coreapi,core/mirrorprimary/reconciler.go:65,internal/entirecore/authorbackfill/processor.go:660,verifiedemailwebhook/apply.go:136. Consumers live out-of-repo (entire-search, entire-api). Ops tool:entire-core broadcast-processing-primariesre-announcesrepo_group.primary_changed(cmd/entire-core/cli/broadcast_processing_primaries.go,internal/entirecore/broadcast_processing_primaries.go:71-78).
No NATS KV, object store, queue-group (core QueueSubscribe), or request-reply usage anywhere. The only core-NATS Subscribe is in a test (internal/natsx/natsx_test.go:39).
3. go-nuts APIs used
None. The library is entirely absent. (The internal natsmsg/natsx packages mirror go-nuts concepts — Drain, pull-consumer supervision, KeepInProgress, ExtractTraceContext, DeadLetter, backoff — but are locally implemented under entire.io/internal/....)
4. Hand-rolled logic replacing go-nuts (summary with cites)
- Connect/reconnect policy: duplicated in 3 places —
serve_mode_global.go:321-352,ref_events_publisher.go:51-76,ciwebhooks/serve.go:181-196(same options; ciwebhooks omitsClosedHandler/ReconnectErrHandlerandDrainTimeout). All:RetryOnFailedConnect, infinite reconnects, 2 s wait, slog handlers, per-handshake cert reload (buildNATSTLSConfig×3:serve_mode_global.go:367,ref_events_publisher.go:102,ciwebhooks/serve.go:213). - Drain:
natsx.Drain(natsx.go:31-51); entire-server's ref-events publisher uses barenc.Closeinstead (ref_events_publisher.go:93). - Shutdown grouping: subsystem manifest +
runSubsystems+ boundedwaitBounded(&bg, 15s)before deferred drains (internal/entirecore/serve.go:36-41, 328-336) — a hand-rolled ShutdownGroup equivalent. - Trace propagation:
natsmsg.ExtractTraceContexton the consume side; on the publish side each publisher has its own duplicatedheaderCarrier(repoops/publisher.go:134-152,gitjobs/publisher.go:133-151,resourceevents.go:282-300) or usesnatsmsg.HeaderCarrier(permssweep/publisher.go:80). - Ack/Nak/backoff policy: per-consumer, hand-rolled — geometric NakDelay backoff on unlimited-MaxDeliver work queues (
repoops/consumer.go:796-809), fixed NakDelay elsewhere; Term for structural poison on interest streams; DLQ+Ack for poison on the WorkQueue (natsmsg.DeadLetter). - Heartbeat:
natsmsg.KeepInProgress(capped extension) in repoops, permswebhook, permssweep handlers. - Consumer supervision/backoff:
natsx.PullConsumer.Run+backoffSleep(pullconsumer.go:56-139); permssweep has its own variant loop for N concurrent slots (permssweep/consumer.go:180-230). - Dedup: hand-set
Nats-Msg-Idheaders everywhere (gitjobs/publisher.go:24,repoops/publisher.go:28,permssweep/publisher.go:23,refevents.go:235,email/outbox.go:179). - No ordered consumers, no KV watchers. Cluster coordination (routing tables, repair queues, DHT membership) does not use NATS at all — it rides memberlist gossip (
:9999) and mTLS gRPC/HTTP between entire-server nodes (docs/repo-map.md:133;dht/repair_queue.gois gRPC/OTel only).
5. Docs describing NATS architecture
README.md:250-274— optionalrepo_refs_v1event stream;nats sub "repo_refs_v1.>"; disabled whenENTIRE_NATS_URLunset.LOCALDEV.md:12, 38-39, 88, 144-155— dev topology (mirror_git_v1), single-node dev NATS (ENTIRE_NATS_STREAM_REPLICAS=1), streams created bymise run dev:prereqs, prod streams owned by fleetapps/nats-streams.docs/repo-map.md:22, 62, 102, 127, 133-134— component map: ci-webhooks = NATS→Buildkite bridge; NATS optional for tlog events; explicitly: replication/repair/DHT use gRPC + memberlist, not NATS.- ADRs:
20260706-perms-sweep-nats-fanout.md(enumerator→worker fan-out on repo_ops_v1),20260704-embedded-nats-for-e2e-tests.md(test strategy),20260622-mirror-teardown-on-upstream-removal.md+20260703-teardown-on-app-uninstall.md(repo_ops teardown design),20260702-verified-email-consumer-deployment.md,20260629-cross-region-author-email-resolution.md(github_push_author islands),20260526-entire-webhook-mvp.md/20260609-inbound-buildkite-webhooks.md(ci-webhooks). - Runbooks:
docs/runbooks/ref-events-outbox-backlog.md,docs/runbooks/transactional-email.md;docs/entire-core/transactional-email.md. - Dev broker config:
deploy/dev/nats.conf— JetStream on, mTLSverify_and_mapmapping cert SANs to users (entiredb,spiffe://partial.to/local/{entire-core,mirror-worker,webhook-ingest,entire-api});docker-compose.yml:14-25(profilenats). AGENTS.mdandMILESTONES.mdcontain no NATS content.
6. Naming conventions & queue patterns
- Streams: snake_case with
_v1version suffix:repo_refs_v1,mirror_git_v1,repo_ops_v1,webhooks_github_v1,github_push_author_v1,email_v1,resource_lifecycle_v1. - Subjects: dotted, versioned, mostly placement-scoped:
repo.ops.v1.<jurisdiction>.<cluster>.<op>,mirror.git.v1.<jur>.<cluster>.<source>.<op>,webhooks.github.v1.<event>,github_push_author.v1.<jur>.<cluster>,repo_refs_v1.<repo-ulid>,resource_lifecycle_v1.<type>.<op>,email_v1.requested. DLQ:repo.ops.dlq.v1.<op>.<reason>. - Durables:
entirecore-<purpose>[-<op>][-<jurisdiction|all>](entirecore-repoops-teardown-eu,entirecore-webhook-meta,entirecore-webhook-perms,entirecore-email-<jur>,entirecore-perms-sweep), plusci-webhooks. Names are load-bearing/pinned (renames orphan durables —repoops/consumer.go:26-39). - Work-queue pattern:
repo_ops_v1is aWorkQueuePolicystream shared by 6 ops via non-overlapping filter subjects (a WorkQueue requirement), one durable per op shared by all replicas of a region (replicas split work through the shared durable — the JetStream equivalent of a queue group). No core-NATS queue groups exist. - Two NATS "planes": central NATS (
ENTIRE_NATS_URL) carrieswebhooks_github_v1+ the global→regionalrepo_ops_v1ops; per-cell/in-cell islands (ENTIRE_MIRROR_GIT_NATS_URL, fallbackENTIRE_NATS_URL, resolverinCellNatsURL()atinternal/entirecore/config.go:469-477) carrymirror_git_v1,mirror_done,perms_backfill,perms_sweep,github_push_author_v1,email_v1. - Delivery-guarantee tiers: transactional Postgres outbox → JetStream (refevents, email); durable JetStream publish with Msg-Id dedup + gated idempotent consumers (repo_ops, mirror_git); best-effort core-NATS at-most-once (resource_lifecycle).</result> <usage><subagent_tokens>238333</subagent_tokens><tool_uses>73</tool_uses><duration_ms>683879</duration_ms></usage>
All verification is done — the review is complete. Here it is.
go-nuts API review — against NATS best practices and actual usage in entire-api, entiredb, and mirror-pipeline
A note first: I couldn't find the best-practices report you mentioned anywhere in the workspace — nothing matching it exists in any of the six repos. I grounded the review in canonical NATS/JetStream guidance (official docs + nats.go v1.52 source, which I verified directly where it mattered) instead. If you share the report, I can cross-check it point-by-point.
TL;DR: The library is in very good shape — small, orthogonal, correct on the subtle stuff (drain flush budgets, rotation-aware mTLS, capped ack-extension, Term-on-poison), and every released version is backward compatible: I diffed v0.3.0..HEAD on the root package and v0.4.0..v0.4.1 on everything — zero API or behavior changes, upgrades are purely additive. The real findings are about coverage: the three services still hand-roll five patterns the library should own, and two missing jsconsumer knobs are hard blockers for migrating entiredb's consumers.
What the library gets right
The design matches best practice almost everywhere it has an opinion: reconnect-forever with fail-fast initial dial (connect.go:108-124), Drain-not-Close with the publish-flush budget correctly accounted on top of nats.DrainTimeout (drain.go:40-50 — a subtlety most codebases get wrong), cancel→join→drain ordering in ShutdownGroup, durable-required + AckExplicit + bounded AckWait/MaxDeliver in jsconsumer, capped KeepInProgress so a wedged handler still redelivers, Term-on-undecodable so poison doesn't spin, and per-handshake cert/CA reload with hostname re-verification and TLS 1.3 minimum. The root-vs-subpackage dependency split is genuinely useful, and natsmsgtest.FakeMsg is exactly the right test seam. Tests pass on HEAD.
Gaps, ranked by impact
1. There is no publish-side story, and it's the biggest duplication across all three services. Every service has hand-rolled the same publish core — producer span + trace Inject + Nats-Msg-Id dedup header + bounded pub-ack wait (universally 5s): mirror-pipeline's internal/natspub (behind six typed publishers), three separate copies of a 5s publishTimeout in entire-api (forwarder/publisher.go:31, settingsrelay/relay.go:60, mirrorlifecycle/consumer.go:54), and three duplicated headerCarrier implementations inside entiredb publishers (repoops/publisher.go:134-152, gitjobs/publisher.go:133-151, resourceevents.go:282-300). A small natsmsg publish helper (span + inject + msg-id + timeout, on the modern jetstream API) plus a StartProducerSpan symmetric to StartConsumerSpan would capture the single most-repeated pattern in the fleet. Inject alone covers only a third of what publishers actually do.
2. jsconsumer.Config is missing knobs that are hard migration blockers. InactiveThreshold — required by every consumer on the interest-retention webhook stream (entiredb's permswebhook/consumer.go:57-73 and mirror-pipeline's forwarder both set 72h, and the forwarder even hand-rolls a reconcile via ConsumerInfo/UpdateConsumer because the legacy API lacked it) — cannot be expressed, so those consumers literally cannot migrate. MaxAckPending is also unexposed (matters for the shared-durable, multi-replica work queues). Both are additive one-field changes. Note MaxDeliver: -1 (unlimited, used by entiredb repoops) does pass through EffectiveMaxDeliver correctly — that one's fine.
3. jsconsumer has no supervision, and both non-adopters built one. entiredb's natsx.PullConsumer recreates the consumer with exponential backoff when the consume loop dies, and tolerates ErrStreamNotFound at boot as warn-and-retry (pullconsumer.go:56-139); mirror-pipeline's fanoutengine retries subscribe 1s→30s. go-nuts Start fails hard once and never re-establishes if the ConsumeContext closes permanently. A Run-style supervised variant (recreate-on-closed with backoff) is the third thing standing between entiredb and adoption.
4. backoff.Policy is flat-only, but two of three services use growing delays. entire-api naks with exponential 1s→300s (ingest/pipeline.go:305-323), entiredb repoops with geometric 5s→30s (repoops/consumer.go:796-809). The package doc argues flat-is-deliberate, but the fleet disagrees with the doc. An optional delay schedule (e.g. a DelayFor(numDelivered) hook or base/factor/cap fields, zero-value = current flat behavior) would let those consumers adopt without changing their semantics. Non-breaking.
5. Connect installs no async ErrorHandler — and nats.go's default prints to stderr. I verified in nats.go v1.52: with AsyncErrorCB nil, defaultErrHandler writes slow-consumer/permission-violation errors to raw stderr, bypassing the structured logger entirely. The proof this belongs in the library: mirror-pipeline attaches its own nats.ErrorHandler via WithNATSOptions at six Connect sites (worker, rekicker, admin, forwarder, fanout, natsregions) — permission violations on missing grants are exactly how their DLQ/advisory failures surface. A default handler routing through cfg.logger (overridable, since extra options apply last) closes it. Same argument, smaller stakes, for a LameDuckModeHandler log line so server-initiated evacuations are visible during maintenance.
Smaller notes: (a) a handler panic inside jsconsumer.Process crashes the whole process — the Consume callback runs on nats.go's dispatch goroutine, outside ShutdownGroup.Go's recover. mirror-pipeline's worker and forwarder both recover-and-Nak per message, and entiredb dead-letters on panic; if those migrate, that protection silently disappears unless Process gains an opt-in recover or the doc says handlers must recover. (b) The MAX_DELIVERIES advisory backstop (ChanQueueSubscribe + queue group + worker pool + GetMsg + idempotent record) is hand-rolled three times with the same shape (entire-api ingest/jobs.go:411-487, mirror-pipeline worker/advisory.go and webhookdlq/advisory.go) — a strong candidate for the next slice, along with a DLQ helper (entiredb's natsmsg.DeadLetter with its standard headers is the cleaner of the two existing shapes). (c) The README's root example uses the legacy sub.Fetch API while the subpackages are all on modern jetstream — worth a note in the example that it's the legacy path.
Fit per service
entire-api (v0.4.0) — best adopter, half-finished. Connect/Drain everywhere, three consumers on jsconsumer — but seven more still hand-roll the identical scaffold, and three hand-rolled InProgress tickers duplicate KeepInProgress. One migration caveat: their tickers fire at AckWait/2 with no cap; KeepInProgress fires at AckWait/3 capped at ~5×AckWait. For the analysis consumer (AckWait 60s → 5min cap) check LLM job durations before switching, or a legitimately-slow job gains a redelivery mid-run. Also cheap wins: the ingest fetch loop (pipeline.go:139-183) doesn't use IsShutdownFetchErr, and go-nuts/backoff is unused pending gap #4.
entiredb — zero adoption, which is ironic since the library was extracted from it: internal/natsx + internal/natsmsg are near-identical parallel implementations (its KeepInProgress is byte-for-byte the same semantics — that migration is a no-op). Blockers are exactly gaps #2 and #3 plus the DLQ helper. Two behavior deltas to handle deliberately at migration: entiredb connects with RetryOnFailedConnect(true) everywhere, while nuts.Connect defaults fail-fast — the go-nuts posture is arguably better ops hygiene, but it changes boot semantics, so either pass WithRetryOnFailedConnect() or consciously accept crash-loop-on-misconfig. And natsx.Drain installs a ClosedHandler (clobbering any existing one) where nuts.Drain correctly uses a status listener — adoption is a strict improvement there. Separate small bug it would fix: entire-server's ref-events publisher tears down with bare nc.Close() (ref_events_publisher.go:93), dropping buffered publishes on shutdown.
mirror-pipeline (v0.3.0) — heaviest root-package user (13 Connect sites, the only ShutdownGroup user), but pinned before the subpackages existed; its fanoutengine/otelnats/natspub/worker-heartbeat code is the donor material for them. Bumping to v0.4.1 is verified-safe. One live bug adoption would fix: meta-fanout's lifecycle consume loop (lifecycle.go:244-261) skips the IsShutdownFetchErr check, so every rollout drain logs a spurious "Lifecycle fetch error" — the exact COR-923 noise the classifier exists to suppress.
Recommended order
- Additive
jsconsumer.Configfields (InactiveThreshold,MaxAckPending) + a supervisedRunvariant — unblocks entiredb. - Publish helper in
natsmsg(producer span + inject + msg-id + bounded ack) — retires the most duplicated code in the fleet. - Default
ErrorHandler(+ lame-duck log) inConnect— one-file change, immediately improves observability at every existing call site on next bump. - Backoff schedule extension, then DLQ/advisory-backstop slice as the follow-up milestone.
All four are non-breaking; nothing in the current surface needs to change shape. The existing API is the right size — the risk here isn't over-design, it's that the missing 20% keeps entiredb and half of entire-api on forks of the same logic, which is the situation the library exists to end.
All four review recommendations are implemented, tested, and committed on branch nodo/go-nuts-extension — six commits, each with the checkpoint trailer, full suite green with the race detector, lint clean.
What landed:
-
connect.go—Connectnow installs a defaultnats.ErrorHandlerrouting async errors (slow-consumer drops, permissions violations) through the configuredsloglogger instead of nats.go's stderr default, plus a lame-duck warn log. Both are overridable viaWithNATSOptionssince extras apply after defaults. Tested against a real embedded server: a denied subscription's async permissions violation and a realLameDuckShutdownboth produce the expected log lines. -
jsconsumer—ConfiggainsInactiveThresholdandMaxAckPending(plain pass-throughs, zero-value compatible; test asserts they land on the on-server consumer), which unblocks the interest-stream webhook consumers in entiredb and mirror-pipeline. -
jsconsumer.Run— the supervised variant ofStart: recreates the consume loop with exponential backoff (1s→30s, reset after a healthy run) when it closes underneath the caller, treats a not-yet-provisioned stream at boot as retry-not-crash, fails fast only on config errors, and joins the live loop on cancel so it slots intoShutdownGroup.Go. Tests cover consumer-deleted-and-recreated, stream-appears-late, and clean cancel — all against a real broker. One design detail: each attempt gets a child context soStart's cancel-watcher goroutines don't accumulate across recreates on a flapping consumer. -
natsmsg.Publisher+StartProducerSpan— the publish core every service had copied: producer span, trace inject,Nats-Msg-Iddedup, bounded pub-ack wait (default 5s). Tests verify stored-message headers, broker-side dedup (PubAck.Duplicate), that an empty msgID doesn't clobber a caller-set header, and that an unbound subject errors promptly. -
backoff.Factor/MaxDelay+DelayFor— the growing envelope both entire-api (1s→300s) and entiredb (5s→30s) actually run, now expressible; zero value keeps flat behavior exactly, and uncapped growth saturates instead of wrapping negative. -
README and
doc.goupdated, including the note that the root usage example is the legacy pull API.
Everything is additive — no existing signature or default changed, so v0.3.0/v0.4.0 consumers upgrade cleanly. I've left the branch local; say the word and I'll push it and open a PR. The DLQ helper + advisory-backstop slice (and the handler-panic posture question in Process) remain as the follow-up milestone — I've noted those and the per-service migration caveats in memory.
PR created: https://github.com/entireio/go-nuts/pull/6
Branch pushed through the entire mirror, six commits, all checkpoint trailers intact. The PR body covers the five changes, the real-broker test coverage, and flags the follow-up milestone (DLQ helper + advisory backstop + Process panic posture) so it doesn't get lost.
Local main is synced with the merge (c8bf094) and the working branch is deleted.
One thing gates everything downstream: the services can't pin these changes until there's a release tag — the new surface is all additive, so by the repo's own versioning pattern that would be v0.5.0 on c8bf094. Want me to tag and push it? After that, the natural follow-ups in priority order are: bump mirror-pipeline from v0.3.0 (its meta-fanout lifecycle loop's spurious rollout errors and six hand-attached ErrorHandlers become deletable immediately), bump entire-api from v0.4.0, and start the entiredb migration that the new Run/InactiveThreshold/MaxAckPending surface just unblocked.
v0.5.0 is tagged and live — pushed through the mirror and confirmed on GitHub pointing at the merge commit (c8bf094), so go get github.com/entireio/go-nuts@v0.5.0 will resolve for all three services.
Ready when you are for the adoption work — mirror-pipeline's bump from v0.3.0 is the highest-value first move (it picks up the entire subpackage surface at once, fixes the spurious rollout errors in the meta-fanout lifecycle loop, and makes its six hand-attached ErrorHandlers redundant).
Understood — leaving entire/checkpoints/v1 untouched. It's flagged and you've made the call; no further action there.
That closes the public-readiness pass. Where go-nuts stands:
- v0.5.0 — tagged, pushed, resolves through the public Go proxy (so pkg.go.dev will index it).
main/v0.5.0tree is library-only, MIT detected. - No secrets anywhere in history.
- One cosmetic gap: the GitHub repo has no description set — worth a one-liner for a published library (e.g. "Connection-lifecycle and JetStream consumer helpers for NATS in Go"). Want me to set it?
gh repo edit entireio/go-nuts --description "..."— quick and reversible, but it's public-facing so I'll leave the call to you.
Otherwise the library is fully published and ready to consume. The pending follow-up whenever you want it: bumping the three services onto v0.5.0 — mirror-pipeline (from v0.3.0) is the highest-value first move, since it picks up the whole subpackage surface at once, fixes the spurious rollout errors in the meta-fanout lifecycle loop, and makes its six hand-attached ErrorHandlers redundant.
Done. Description set to "Connection-lifecycle and JetStream consumer helpers for NATS in Go" and confirmed live on the repo.
That wraps the publishing work — go-nuts is public, described, v0.5.0 tagged and resolving through the public proxy. Ready for the adoption bumps (mirror-pipeline first) whenever you want to pick that up.
Mostly yes — the prose is clear and the subpackage section is accurate (those are the parts I updated). But reading it as an outsider landing on a public repo, one real gap and a couple of smaller things stand out:
The opening tagline is now stale and undersells the library. Line 7 still says "Connection-lifecycle helpers for NATS pull consumers … for connecting, draining, and shutting down cleanly." That was true before the extension, but the library now also ships a full JetStream consumer scaffold, a publish helper, trace propagation, and a backoff policy — half its value. The GitHub description I just set is actually more accurate than the README's own first sentence. "What it provides" then only lists the three root-package items, so the subpackages read as an afterthought at the bottom rather than headline features.
Two internal-isms read differently on a public repo than they did on a private one:
- Line 116: the bare
(COR-762)is an internal Linear ticket ref — meaningless noise to an external reader. (There are more of these indoc.goand code comments, but the README is the front page.) - Lines 84–86:
Connectdefaults to readingENTIRE_INTERNAL_TLS_*env vars. It's honestly documented, but it signals "internal library that happens to be public." If go-nuts is meant as genuinely general-purpose, one line acknowledging it's opinionated toward Entire's conventions (overridable via Options) would set expectations; if it's really just public-because-convenient, that's fine too — but the current framing presents a clean general library while defaulting to org-internal behavior.
Minor: the usage snippet references sub and handle without showing where they come from, and never shows the import (module is …/go-nuts, package is nuts). Fine for an illustrative excerpt, but a newcomer might stumble.
The one I'd actually push on is the tagline — it's a genuine accuracy problem, not taste. Want me to rewrite the intro + "What it provides" so the JetStream/publish/backoff capabilities are represented up front, and drop the COR-762 ref? I can leave the env-var framing alone unless you want it addressed.
The README is fixed. Changes:
- New tagline now sets full scope — a toolkit for NATS and JetStream services, calling out the consumer scaffold, publish helper, trace propagation, and redelivery policy up front instead of only "connect/drain/shutdown."
- "What it provides" → "Connection lifecycle (root package)" so the three root items read as the lifecycle core and the
## Subpackagessection is clearly the rest, not an afterthought. The#subpackagesanchor in the usage note still resolves. (COR-762)dropped — no internal ticket refs remain in the README.
Left the env-var framing and the illustrative snippet as-is, per what we agreed.
The edit is on main in the working tree, uncommitted. Since you've been landing everything via PR (and main is likely protected), want me to branch → commit → PR this, or are you handling it? It's doc-only, so no new tag needed — though if you want pkg.go.dev to show the updated README, it refreshes when the next version tag is pushed.
PR opened: https://github.com/entireio/go-nuts/pull/7
Doc-only, one commit with the checkpoint trailer. No new tag needed — though pkg.go.dev will refresh the rendered README on the next version tag whenever you cut one.