Extend bindProductionAuth and bindDevSeedAuth to 7-arg signatures.
Fix RecordingRealtimeBroadcaster scope type to structurally match ChannelScope.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Type socket.data.user via AppSocketData/AppSocket generics (no more any)
- Wrap Gate 1 authenticator call in try/catch so exceptions reject the
connection with connect_error instead of being swallowed
- Fix Gate 3 userScoped channel authorize: derive params.userId from the
authenticated socket user so owner-inbound is accepted
- Await io.close(callback) in stop() to ensure full shutdown
- Remove unused httpServer field from constructor (io already holds the ref)
- Extract CHANNEL_ROOM_PREFIX/channelRoom helper to channel-room.ts;
replace three `ch:${name}` magic strings; re-export from index
- Add JSDoc to ChannelScope explaining userScoped params/template convention
- Fix "allows subscribe + invokes handler with ctx" test: hoist received
outside beforeEach and assert ctx shape
- New test: rejects connection when authenticator throws
- New test: userScoped channel accepts inbound from owner, rejects from anon
Long-form design for the bidirectional realtime layer the user
brainstormed on 2026-05-08. Builds on ADR-015 (events and jobs);
hybrid pattern where IRealtimeBroadcaster is layered alongside
IEventBus, with a bridge for forwarding allowlisted bus events to
realtime channels.
v1 ships: @repo/core-realtime package, custom Node server in
apps/web-next, cookie-session auth, four scope kinds, two
generators (gen realtime channel|handler), three new anchors per
feature, RecordingRealtimeBroadcaster test helper, realtime-ping
proof-of-life. Dashboard, DB-backed roles/permissions, multi-instance
fanout, and a generic core-package generator are deferred — captured
in §13 / §14 / §15.
Companion HTML explainer at realtime-auth-explainer.html (project root,
uncommitted) walks the four-checkpoint auth model visually. ADR-016
will be created during implementation.
di-explainer and data-flow-explainer narratives still showed the
pre-ADR-014 1-arg bindProductionBlog(config) form. Updated both
explainers to (config, tracer, logger, bus, queue), and adjusted the
narrative arc so the production-swap step mentions the
resolveEventsAndJobs* preamble.
adding-a-feature's Step 16 sketch was the same 1-arg shape; replaced
with the canonical 5-arg signature and a short note that the bus/queue
params are accept-and-forward until gen event consume / gen job
generators inject usage at the anchors. Pointer to a real feature's
bind-production.ts for the complete reference.
Final sweep is clean — no stale (container, config) or 1-arg
bindProduction signatures remain in docs/.
Six per-package AGENTS.md tables and the di-explainer wiring trace all
showed pre-ADR-014 binder shapes. Refreshed to the post-ADR-015
reality:
- Per-package "Public exports" tables now show the
(config, tracer, logger, bus, queue) production signature and the
(tracer, logger, bus, queue) dev-seed companion. auth and
marketing-pages also list their newly-public ./di/container,
./di/symbols, and (marketing-pages only) ./services/mailer +
./services/recording-mailer subpaths.
- di-explainer's wiring trace adds the resolveEventsAndJobs* step in
bindAll(), the (config, tracer, logger, bus, queue) binder
signature, and the // <gen:event-handlers> / // <gen:jobs>
injection sites.
No code change; pre-existing AGENTS.md staleness predates ADR-014 +
ADR-015 — this commit catches both up.
Three follow-up corrections from the docs audit:
- vertical-feature-spec § 13 dropped the events/jobs bullet from the
"Out of scope (deferred)" list — they shipped, the bullet didn't
belong there. The spec already mentions ADR-015 elsewhere (§ 4
optional folders).
- vertical-feature-spec § 14 success criterion replaced "Any
cross-feature import fails pnpm lint" with the post-ADR-015 reality:
cross-feature imports are restricted to event contracts (the feature
boundary tag now accepts feature-tagged deps), but rule E1
(no-handler-reexport) keeps consumer handlers / use cases /
repositories private.
- dependency-flow now shows the resolveEventsAndJobs* step in bindAll
and the (config, tracer, logger, bus, queue) binder signature, plus
the // <gen:event-handlers> / // <gen:jobs> anchor lines where the
generators inject subscribe / register calls.
Implements ADR-015 in 52 commits. Highlights:
- New @repo/core-events package: IEventBus, defineEvent + EventDescriptor,
InMemoryEventBus, PayloadJobsEventBus.
- New @repo/core-shared/jobs subpath: IJobQueue, InMemoryJobQueue (with
register), PayloadJobQueue.
- Recording test helpers (RecordingEventBus, RecordingJobQueue) in
@repo/core-testing/instrumentation, using local-type-alias pattern to
avoid build-graph cycles.
- ESLint rules E1 (no-handler-reexport) and J (no-direct-payload-jobs).
- Feature binders accept (bus, queue); apps/web-next bindAll resolves
Payload-backed in production and in-memory in dev-seed.
- Three Plop generators: gen event publish, gen event consume, gen job.
Each one threads through fixed // <gen:*> anchor comments enforced by
a CI guard.
- Proof-of-life flow: signing up in @repo/auth publishes
userSignedUpEvent on the shared bus; @repo/marketing-pages subscribes
and enqueues a send-welcome-email job; the in-memory queue dispatches
it to the wrapped job which records on RecordingMailerService. e2e
test at apps/web-next/src/__tests__/sign-up-welcome-email.test.ts.
- Documentation: ADR-015, docs/guides/events-and-jobs.md, AGENTS.md +
CLAUDE.md + scaffolding-guide updates, vertical-feature-spec deferred
lines retired.
Workspace green at the merge boundary: typecheck (15/15), lint (16/16),
test (17/17), turbo boundaries (clean).
Three issues uncovered by the full pnpm typecheck/test/boundaries pass
and resolved here:
- core-testing was importing IEventBus / IJobQueue from core-events /
core-shared, creating two boundary violations (tooling → core) and a
build-graph cycle. Inlined the type aliases (mirroring how
RecordingTracer / RecordingLogger handle ITracer / ILogger).
recording-event-bus.test.ts replaces defineEvent() with an inline
descriptor literal so no runtime import is needed either. core-events
and core-shared are removed from core-testing dependencies.
- turbo.json: typecheck and test no longer dependsOn ^typecheck / ^build.
Each package's tsc / vitest resolves cross-package types via
node_modules independently, and dropping the topological dep avoids the
spurious cycle warning that appeared once core-testing started
importing core-events / core-shared.
- turbo.json: feature.dependencies.allow gains "feature". Cross-feature
event flow (ADR-015) requires a consumer feature to import the
publisher's event contract directly. The dangerous form (importing
the publisher's handler/use-case/repo) is still blocked by E1's
no-handler-reexport ESLint rule and the missing public exports.
- TaskConfig<"slug-string"> → TaskConfig<{ input; output }> in the gen
job task template (and the shipped send-welcome-email.task.ts) since
runtime-generated slugs aren't keys of TypedJobs['tasks'].
Also folds in the spec correction noted in the plan's known follow-up
\#1: subscribe takes consumerFeature: string as a second arg between
descriptor and handler. § 3.3 and § 5.4 updated.
End-to-end proof-of-life. bindAllDevSeed wires InMemoryEventBus +
InMemoryJobQueue across all features. signUpController publishes
auth.user.signed-up; marketing-pages' handler enqueues
marketing-pages.send-welcome-email; the in-memory queue dispatches
the wrapped job which records on RecordingMailerService.
Adds ./di/container, ./di/symbols, ./services/mailer, and
./services/recording-mailer to auth + marketing-pages exports so
the e2e test can resolve from the per-feature containers without
deep-importing.
sendWelcomeEmailJob takes IMailerService, validates the typed input
(userId + email), and delegates to mailer.sendWelcome. The
onAuthUserSignedUpHandler now takes IJobQueue and enqueues
"marketing-pages.send-welcome-email" with the event payload.
Both binders bind a RecordingMailerService at the IMailerService
symbol (production placeholder until a real adapter ships) and pass
mailer / queue into the wrapped factories. Dev-seed additionally
queue.register()s the slug so the in-memory queue dispatches via
the wrapped job; production relies on the generated Payload event-task
to resolve the wrapped handler from the container.
Generated handler + Payload event-task via gen event consume,
threaded through symbols / both binders / cms re-export at the
configured anchors. bus.subscribe wires the in-memory delivery in
dev-seed; the __events.auth.user.signed-up.marketing-pages Payload
task closes the production-bus loop.
Also fixes two generator-level issues found during Phase 8:
- Drop publisher prompt's `when` clause so --args can supply the
4th positional argument (Plop limitation: --args cannot bypass
conditional prompts). Validate runs only in consume mode.
- Switch event-task.ts.hbs from TaskConfig<"slug-string"> to
TaskConfig<{ input: ...; output: object }> since runtime-generated
event slugs are not keys of TypedJobs['tasks'].
signUpUseCase now takes an IEventBus and publishes userSignedUpEvent
after creating the user (synthesizing email from username since auth
is username-based). Use case mocks-default in module.ts get a fresh
InMemoryEventBus per resolution; bind-production / bind-dev-seed wire
the shared bus passed by bindAll. Tests updated to inject
RecordingEventBus, including a new test that asserts publish on
success and silence on failure.