Skip to content

feat(tenant): resolve X-Tenant-ID before auth and thread the tenant - #593

Merged
taitelee merged 11 commits into
mainfrom
tenant-plumbing
Sep 21, 2026
Merged

taitelee merged 11 commits into
mainfrom
tenant-plumbing

Conversation

@taitelee

@taitelee taitelee commented Sep 18, 2026 •

Copy link
Copy Markdown
Member

Summary

Story 1 of the multi-tenant epic: every request resolves to a tenant before authentication, and the tenant is threaded through every settings read. No behavior change for a deployment that sends no tenant header — only tenant 0 exists.

  • New internal/tenant: ID (a validated string — letters, digits, _, -, ≤ 64 bytes, safe as a folder name and an MQ subject token), Default = "0", Header = "X-Tenant-ID". Imports nothing from the repo.
  • settings.Registry (For(id)) over the one store settings.Open adopts, keyed 0. Open, reload and the watcher are untouched.
  • api.TenantMW runs ahead of auth on every /v1 route outside /v1/ops/*: absent or empty header is tenant 0, malformed or repeated is 400, unknown is 404; the resolved *settings.Store rides the request context. Handlers read it once and pass it down — the ingest, structured-query and pipe getters take the store as a parameter, and a tenant route reached without one answers 500 rather than fall back to a tenant. The async paths (worker, sweeper, hub, schema registry) are constructed with a tenant.ID and their getters take it, wired with tenant.Default in internal/app.
  • The probes, /version, the metrics path and /v1/ops/* stay tenant-exempt; the admin pipe reads serve the default tenant. X-Tenant-ID joins the CORS allow-headers list so browser clients can send it.
  • The slog cleanup deferred from refactor(mq): seal the MQ boundary behind an intent-level broker API #586: no constructor takes a *slog.Logger anymore (mq, the handlers, auth, discovery, sweeper, worker, plus settings.Open, chconn.Open and the config data-dir helpers); call sites use the context-aware calls. Tests reach log output through the new internal/testutil/logtest (Silence in TestMain, Capture for log-asserting tests, which run serially). testutil.NopLogger is removed.

The tenant work and the logger cleanup are separate commits; the rest are review rounds.

Test plan

  • make ci passes locally (unit, integration, e2e, coverage gates; Go total 92.8%)
  • internal/tenant: grammar table test (default, 19-digit id, length cap, dots, slashes, wildcards, non-ASCII)
  • api.TenantMW: no header / empty / 0 / unknown 404 / malformed 400 / repeated 400; tenant resolves before AuthMW; exempt routes ignore the header; tenant-route handlers 500 without a resolved store
  • internal/app: the wired registry answers 404/400 end to end; ops ignores the header
  • async registry miss: the DLQ switch reads as on, the schema refresh survives a zero interval at boot and mid-run (each test fails with its fix reverted)
  • logger: log-asserting tests in api, auth, discovery, config converted to logtest.Capture
  • Manual: curl -H 'X-Tenant-ID: acme' /v1/health → 404; no header → as before

Follow-ups

  • Which tenant an ops read addresses is story 2's: ?tenant= on the reload route and the pipe reads, with the bearerToken query-rewrite fix, the per-tenant admin gate, and the SDK option to send it.
  • An unknown tenant on an async path reads as the getter's zero value (nil policy, gap window 0), with two fail-safes: the worker's DLQ switch reads as on (an unreadable message is parked, never dropped) and the schema refresh keeps its cadence rather than take a zero interval. Impossible while only tenant 0 exists; story 3 defines a removed tenant's semantics.
  • Auth's policy source (story 9) and the chconn-backed getters (story 6) are not threaded.

Related Issues

Part of #583 (story 1). Finishes the logger cleanup deferred from #586.

@coderabbitai

coderabbitai Bot commented Sep 18, 2026 •

Copy link
Copy Markdown

Review Change StackReview Change Stack

Understand this PR’s impact

Explore downstream dependencies and potential security impact with Blast Radius.

View blast radius →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: b9a25467-675e-47eb-ad3f-4a2e0dd6664a

📥 Commits

Reviewing files that changed from the base of the PR and between 990f713 and dc0b3a3.

📒 Files selected for processing (73)
  • .github/labeler.yml
  • AGENTS.md
  • CHANGELOG.md
  • docs/src/content/docs/access-control.mdx
  • docs/src/content/docs/api.md
  • docs/src/content/docs/architecture.md
  • docs/src/content/docs/deployment.md
  • docs/src/content/docs/development.md
  • docs/src/content/docs/reverse-proxy.mdx
  • docs/src/content/docs/sdk/index.mdx
  • docs/src/content/docs/sdk/reference.md
  • docs/src/content/docs/sdk/streaming.md
  • docs/src/content/docs/settings-directory.mdx
  • internal/api/boot_chain_test.go
  • internal/api/dlq.go
  • internal/api/dlq_test.go
  • internal/api/errors.go
  • internal/api/errors_test.go
  • internal/api/ingest.go
  • internal/api/ingest_seams_test.go
  • internal/api/ingest_test.go
  • internal/api/main_test.go
  • internal/api/pipes.go
  • internal/api/pipes_test.go
  • internal/api/router.go
  • internal/api/router_test.go
  • internal/api/settings.go
  • internal/api/settings_test.go
  • internal/api/stream_test.go
  • internal/api/structured_query.go
  • internal/api/structured_query_test.go
  • internal/api/tenant.go
  • internal/api/tenant_helpers_test.go
  • internal/api/tenant_test.go
  • internal/app/app.go
  • internal/app/app_test.go
  • internal/app/wire.go
  • internal/auth/auth.go
  • internal/auth/auth_test.go
  • internal/auth/main_test.go
  • internal/chconn/chconn.go
  • internal/config/persistence.go
  • internal/config/persistence_test.go
  • internal/discovery/discovery.go
  • internal/discovery/discovery_test.go
  • internal/discovery/main_test.go
  • internal/discovery/timestamp.go
  • internal/discovery/timestamp_test.go
  • internal/ingest/main_test.go
  • internal/ingest/sweeper.go
  • internal/ingest/sweeper_test.go
  • internal/ingest/worker.go
  • internal/ingest/worker_test.go
  • internal/mq/embedded.go
  • internal/mq/embedded_test.go
  • internal/mq/main_test.go
  • internal/settings/main_test.go
  • internal/settings/registry.go
  • internal/settings/registry_test.go
  • internal/settings/store.go
  • internal/settings/store_test.go
  • internal/settings/watch.go
  • internal/stream/hub.go
  • internal/stream/hub_test.go
  • internal/stream/roweval_test.go
  • internal/stream/tenant_test.go
  • internal/tenant/tenant.go
  • internal/tenant/tenant_test.go
  • internal/testutil/logtest/logtest.go
  • internal/testutil/logtest/logtest_test.go
  • internal/testutil/testutil.go
  • tests/integration/boot_resilience_test.go
  • tests/integration/query_limits_test.go

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.

📜 Recent review details
🧰 Additional context used
📓 Path-based instructions (2)
**WH001 applies to every tracked Markdown file, with no carve-out**

📄 CodeRabbit inference engine (AGENTS.md)

Files:

  • docs/src/content/docs/sdk/reference.md
  • docs/src/content/docs/sdk/index.mdx
  • docs/src/content/docs/sdk/streaming.md
  • AGENTS.md
  • CHANGELOG.md
  • docs/src/content/docs/deployment.md
  • docs/src/content/docs/development.md
  • docs/src/content/docs/settings-directory.mdx
  • docs/src/content/docs/access-control.mdx
  • docs/src/content/docs/api.md
  • docs/src/content/docs/architecture.md
  • docs/src/content/docs/reverse-proxy.mdx
**`.mdx` is never auto-fixed by the generic markdownlint rules** In MDX, leave a blank line between a JSX tag and a code fence.

📄 CodeRabbit inference engine (AGENTS.md)

Files:

  • docs/src/content/docs/sdk/index.mdx
  • docs/src/content/docs/settings-directory.mdx
  • docs/src/content/docs/access-control.mdx
  • docs/src/content/docs/reverse-proxy.mdx
🧠 Learnings (5)
📓 Common learnings
Learnt from: CR
Repo: Wave-RF/WaveHouse

Timestamp: 2026-09-21T15:05:27.435Z
Learning: Every code change should update the corresponding docs in the same PR.
📚 Learning: 2026-06-26T12:23:22.696Z
Learnt from: EricAndrechek
Repo: Wave-RF/WaveHouse PR: 346
File: internal/stream/subscriber_test.go:9-28
Timestamp: 2026-06-26T12:23:22.696Z
Learning: In this Go repository, prefer table-driven tests (e.g., `[]struct{...}` with `t.Run(...)`) only for tests that cover multiple scenarios/inputs and can be cleanly enumerated. Do not artificially rewrite a clear single-scenario sequential behavioral-flow test into a table-driven form just to fit the pattern; if there’s only one meaningful scenario, keep the test as a straightforward linear flow (as in `TestSubscriber_SendDeliversThenDropsWhenFull`).

Applied to files:

  • internal/tenant/tenant_test.go
  • internal/app/app_test.go
  • internal/discovery/discovery_test.go
  • internal/api/tenant_test.go
📚 Learning: 2026-06-10T15:01:09.027Z
Learnt from: EricAndrechek
Repo: Wave-RF/WaveHouse PR: 312
File: docs/src/content/docs/development.md:0-0
Timestamp: 2026-06-10T15:01:09.027Z
Learning: In this repo’s Markdown review (all .md files), do not flag capitalization/style issues for literal paths starting with ".github/" (or any substring that is a path beginning with ".github/"). Treat ".github" as the correct lowercase dotfile directory name, even when it appears inside prose or code spans; automated checks such as LanguageTool’s "(GITHUB)" rule commonly produce false positives for this literal filesystem path.

Applied to files:

  • CHANGELOG.md
📚 Learning: 2026-08-13T12:17:52.620Z
Learnt from: EricAndrechek
Repo: Wave-RF/WaveHouse PR: 470
File: docs/src/content/docs/reverse-proxy.mdx:137-144
Timestamp: 2026-08-13T12:17:52.620Z
Learning: For Wave-RF/WaveHouse documentation, verify claims about implementation control flow against the authoritative implementation source (for example, internal/auth/auth.go) rather than relying solely on docs/** content. Documentation may lag behind or paraphrase behavior, so control-flow claims should be confirmed in source code.

Applied to files:

  • docs/src/content/docs/settings-directory.mdx
  • docs/src/content/docs/access-control.mdx
📚 Learning: 2026-05-23T01:23:59.268Z
Learnt from: EricAndrechek
Repo: Wave-RF/WaveHouse PR: 174
File: internal/api/ingest_test.go:111-111
Timestamp: 2026-05-23T01:23:59.268Z
Learning: In WaveHouse Go tests in internal/api/**/*_test.go, use internal/testutil.AssertJSONErrorResponse(t, w) for HTTP error-path JSON assertions. Do not use (or reintroduce) package-local assertJSONErrorResponse helpers. AssertJSONErrorResponse verifies the response Content-Type is application/json, includes the X-Content-Type-Options: nosniff header, and that the JSON body contains an "error" field.

Applied to files:

  • internal/api/tenant_test.go
🪛 ast-grep (0.45.3)
internal/config/persistence.go

[warning] 28-28: A log/format call (log.Print/Printf/Println, the Fatal/Panic variants, fmt.Sprintf, or a structured logger's Info/Warn/Error/Debug method) is given a message built by concatenating a string literal with a non-literal value such as request data. Unsanitized, attacker-controlled input written to logs enables log forging / CRLF injection: an attacker can inject newlines to spoof log entries or break log parsers. Do not concatenate raw input into the log message; pass it as a separate structured field/argument (e.g. 'log.Printf("user: %s", user)' or 'logger.Info("login", "user", user)') and strip or escape newline characters first.
Context: slog.Error(kind+" init failed", fields...)
Note: [CWE-117] Improper Output Neutralization for Logs.

(log-injection-request-data-concat-go)

🪛 LanguageTool
docs/src/content/docs/sdk/reference.md

[style] ~59-~59: This word has been used in one of the immediately preceding sentences. Using a synonym could make your text more interesting to read, unless the repetition is intentional.
Context: ...ream, since repeating the request won't usually talk whatever rejected it round — the e...

(EN_REPEATEDWORDS_USUALLY)


[style] ~59-~59: Consider an alternative for the overused word “exactly”.
Context: ...roxy. That silent-downgrade behavior is exactly why auth is re-read on every connecti...

(EXACTLY_PRECISELY)

docs/src/content/docs/sdk/streaming.md

[style] ~132-~132: Since ownership is already implied, this phrasing may be redundant.
Context: ...ed more of on this path; see Supplying your own fetch. ...

(PRP_OWN)

AGENTS.md

[grammar] ~48-~48: Please add a punctuation mark at the end of paragraph.
Context: ...th a tenant.ID and their getters take it - Go 1.26, strict formatting (`gof...

(PUNCTUATION_PARAGRAPH_END)

CHANGELOG.md

[typographical] ~13-~13: Consider using an em dash in dialogues and enumerations.
Context: - **Requests resolve to a tenant before a...

(DASH_RULE)


[style] ~13-~13: This sentence is over 40 words long. Consider splitting it up, as shorter sentences make the text easier to read.
Context: - Requests resolve to a tenant before authentication, and the tenant is threaded through every settings read (internal/tenant/ (new, + tests), internal/settings/registry.go (new, + tests), internal/api/tenant.go (new, + tests), internal/api/{router,ingest,structured_query,pipes}.go, internal/ingest/{worker,sweeper}.go, internal/stream/hub.go, internal/discovery/discovery.go, internal/app/{app,wire}.go): story 1 of the multi-tenant epic (#583), with no behavior change for a deployment that sends no tenant header. internal/tenant defines the id — a va...

(TOO_LONG_SENTENCE)

docs/src/content/docs/deployment.md

[style] ~335-~335: Since ownership is already implied, this phrasing may be redundant.
Context: ....json` applies, and scoping a caller to their own rows stays that policy's job, from a va...

(PRP_OWN)


[style] ~360-~360: Consider using a more formal/concise alternative here.
Context: ...eHouse used to ignore it; now any value other than 0 names an unknown tenant, so **every...

(OTHER_THAN)

docs/src/content/docs/settings-directory.mdx

[typographical] ~192-~192: Consider using an em dash in dialogues and enumerations.
Context: - mq.max_bytes_gb (seed default 50) —...

(DASH_RULE)

docs/src/content/docs/architecture.md

[style] ~99-~99: Since ownership is already implied, this phrasing may be redundant.
Context: ...th's: ReplayProjector tracks drift in its own state and the two are not reconciled ([...

(PRP_OWN)

🔇 Additional comments (73)
tests/integration/boot_resilience_test.go (1)

20-20: LGTM!

Also applies to: 69-69, 91-91

tests/integration/query_limits_test.go (1)

20-20: LGTM!

Also applies to: 95-95, 100-100, 102-102, 108-110

docs/src/content/docs/sdk/streaming.md (1)

130-130: LGTM!

Also applies to: 136-136

CHANGELOG.md (1)

13-13: LGTM!

Also applies to: 25-25, 65-65

docs/src/content/docs/reverse-proxy.mdx (1)

197-197: LGTM!

Also applies to: 208-208

docs/src/content/docs/sdk/index.mdx (1)

338-338: LGTM!

Also applies to: 380-380

docs/src/content/docs/sdk/reference.md (1)

33-33: LGTM!

Also applies to: 59-59

docs/src/content/docs/settings-directory.mdx (1)

192-192: LGTM!

AGENTS.md (1)

29-29: LGTM!

Also applies to: 32-32, 46-48, 78-81, 444-445

docs/src/content/docs/architecture.md (1)

70-71: LGTM!

Also applies to: 80-80, 93-93, 99-99, 107-107, 167-167, 185-192, 207-208, 301-302

docs/src/content/docs/deployment.md (1)

331-361: LGTM!

docs/src/content/docs/development.md (1)

350-350: LGTM!

Also applies to: 471-471, 498-498

docs/src/content/docs/access-control.mdx (1)

215-218: LGTM!

Also applies to: 367-367

docs/src/content/docs/api.md (1)

153-153: LGTM!

Also applies to: 619-619

internal/api/dlq.go (1)

17-18: LGTM!

Also applies to: 27-27

internal/api/errors.go (1)

44-44: LGTM!

Also applies to: 59-59, 82-82, 103-103

internal/auth/auth.go (1)

69-70: LGTM!

Also applies to: 79-81, 116-117, 121-121, 145-147, 159-159, 203-209, 243-256, 276-279

internal/chconn/chconn.go (1)

79-80: LGTM!

internal/config/persistence.go (1)

24-24: LGTM!

Also applies to: 29-29, 42-42, 50-50, 56-56, 58-58, 64-64

internal/mq/embedded.go (1)

19-24: LGTM!

Also applies to: 27-28, 31-32, 35-36, 39-40, 43-44, 82-85, 102-102, 133-133, 336-336, 445-451

internal/settings/store.go (1)

28-28: LGTM!

Also applies to: 44-45, 73-90

internal/settings/watch.go (1)

6-6: LGTM!

Also applies to: 54-55, 93-93

internal/testutil/logtest/logtest.go (1)

1-59: LGTM!

internal/testutil/testutil.go (1)

17-17: LGTM!

Also applies to: 31-31

internal/testutil/logtest/logtest_test.go (1)

1-29: LGTM!

internal/mq/main_test.go (1)

1-14: LGTM!

internal/settings/main_test.go (1)

1-13: LGTM!

internal/discovery/timestamp.go (1)

4-4: LGTM!

Also applies to: 99-99, 107-107

internal/auth/auth_test.go (1)

17-17: LGTM!

Also applies to: 41-45, 48-53, 321-321, 333-333, 490-490, 508-508, 518-524, 541-542, 554-555, 563-564, 571-572, 631-631, 662-662

internal/auth/main_test.go (1)

1-15: LGTM!

internal/config/persistence_test.go (1)

13-13: LGTM!

Also applies to: 18-23, 26-26, 41-44, 55-58, 67-72, 81-83, 89-95, 106-108

internal/mq/embedded_test.go (1)

15-16: LGTM!

Also applies to: 19-19, 293-293, 511-511

internal/settings/store_test.go (1)

21-21: LGTM!

Also applies to: 91-91, 95-95, 125-125

.github/labeler.yml (1)

16-20: LGTM!

internal/settings/registry.go (1)

1-22: LGTM!

internal/settings/registry_test.go (1)

1-24: LGTM!

internal/tenant/tenant.go (1)

1-51: LGTM!

internal/tenant/tenant_test.go (1)

1-54: LGTM!

internal/api/tenant_helpers_test.go (1)

1-34: LGTM!

internal/api/tenant_test.go (1)

1-165: LGTM!

internal/api/router.go (1)

12-13: LGTM!

Also applies to: 36-39, 128-170, 259-259, 275-275, 354-354

internal/api/router_test.go (1)

16-16: LGTM!

Also applies to: 24-24, 36-36, 52-52, 67-67, 84-84, 100-100, 115-115, 176-176, 325-342, 415-421, 472-495, 511-521, 572-582, 651-659, 676-684, 775-785

internal/api/tenant.go (1)

1-84: LGTM!

internal/app/app_test.go (1)

27-27: LGTM!

Also applies to: 147-187

internal/api/boot_chain_test.go (1)

19-19: LGTM!

Also applies to: 90-90

internal/api/dlq_test.go (1)

28-28: LGTM!

Also applies to: 48-48, 62-62, 82-90, 107-107, 118-126

internal/api/errors_test.go (1)

16-18: LGTM!

Also applies to: 47-61, 87-89, 108-109, 130-139, 157-159, 168-168, 185-195

internal/api/ingest_seams_test.go (1)

51-55: LGTM!

Also applies to: 69-73, 88-93, 132-141, 171-171, 181-181, 201-207, 227-233

internal/api/ingest_test.go (1)

24-24: LGTM!

Also applies to: 26-26, 61-65, 111-111, 121-121, 134-134, 138-138, 148-148, 153-153, 162-162, 166-166, 176-178, 182-182, 192-194, 199-199, 205-205, 218-218, 222-222, 232-232, 236-236, 246-247, 261-261, 271-272, 286-286, 295-297, 314-314, 323-323, 325-325, 343-343, 357-359, 376-376, 401-403, 419-419, 446-448, 464-464, 479-481, 497-497, 508-510, 528-528, 540-542, 554-555, 564-564, 573-574, 583-583, 598-599, 607-607, 617-618, 628-628, 644-645, 656-656, 668-670, 676-676, 687-689, 692-692, 699-699, 709-711, 719-719, 736-737, 750-750, 759-760, 771-771, 823-823, 831-831, 852-852, 877-877, 885-885, 902-903, 915-915, 939-939, 943-943, 957-958, 967-967, 986-987, 993-993, 1003-1004, 1007-1007, 1017-1018, 1034-1034, 1050-1051, 1065-1065, 1077-1078, 1098-1098, 1117-1118, 1123-1123, 1134-1135, 1143-1143, 1318-1320, 1368-1370, 1387-1389, 1434-1434, 1439-1439, 1458-1459, 1464-1464, 1478-1478, 1483-1483, 1508-1510, 1553-1554, 1562-1563, 1572-1574, 1583-1583, 1589-1589, 1605-1605, 1616-1616, 1630-1631, 1636-1636, 1650-1651, 1655-1655, 1665-1665, 1674-1674, 1690-1690, 1696-1696, 1710-1710, 1718-1718, 1735-1735, 1748-1748, 1766-1766, 1773-1773, 1800-1800, 1804-1804, 1816-1816, 1821-1821, 1833-1833, 1839-1839, 1851-1851, 1858-1858, 1884-1884, 1888-1888, 1922-1922, 1926-1926, 1953-1953, 1960-1960, 2005-2005, 2010-2010, 2039-2039, 2046-2046, 2057-2057, 2063-2063, 2121-2121, 2129-2129, 2149-2149, 2151-2159, 2169-2169, 2181-2181, 2185-2185, 2196-2196, 2204-2204, 2248-2248, 2250-2250, 2253-2253, 2268-2268, 2270-2270, 2273-2273, 2301-2301, 2316-2316, 2359-2359, 2361-2361, 2387-2387, 2411-2411, 2419-2419, 2441-2441, 2454-2454, 2470-2471, 2484-2484, 2499-2500, 2503-2503, 2532-2533, 2536-2536, 2555-2556, 2559-2559, 2578-2578, 2580-2581, 2589-2589, 2612-2613, 2620-2620, 2671-2672, 2675-2675

internal/api/main_test.go (1)

1-16: LGTM!

internal/api/pipes_test.go (1)

46-51: LGTM!

Also applies to: 65-69, 83-85, 98-100, 115-120, 130-140, 153-173, 185-195, 206-215, 224-240, 249-267, 280-296, 305-321, 334-345, 356-370, 381-391, 402-411

internal/api/settings.go (1)

16-20: LGTM!

Also applies to: 36-36, 48-48

internal/api/settings_test.go (1)

13-13: LGTM!

Also applies to: 43-45

internal/api/stream_test.go (1)

14-14: LGTM!

Also applies to: 26-26, 54-54, 79-79, 118-118

internal/api/structured_query.go (1)

18-18: LGTM!

Also applies to: 27-42, 56-59, 69-76, 110-115, 133-137

internal/api/ingest.go (1)

21-21: LGTM!

Also applies to: 43-49, 64-65, 138-141, 154-154, 161-161, 169-169, 182-187, 223-231, 256-256, 267-276, 287-287, 303-308, 323-323, 340-340, 372-379, 400-400, 479-494, 523-523, 533-541, 564-564, 589-589, 609-609, 639-650, 664-669, 683-683, 698-708

internal/api/pipes.go (1)

15-15: LGTM!

Also applies to: 24-36, 46-53, 63-63, 74-79, 96-100

internal/discovery/discovery.go (1)

11-11: LGTM!

Also applies to: 181-201, 243-243, 302-310, 429-448, 457-459

internal/ingest/sweeper.go (1)

10-10: LGTM!

Also applies to: 24-29, 32-39, 57-57, 61-61, 64-64

internal/ingest/worker.go (1)

22-22: LGTM!

Also applies to: 61-74, 134-135, 180-183, 240-240, 279-279, 464-476, 490-497, 569-581, 683-683, 696-696, 711-711, 726-726, 735-735, 766-766, 776-776

internal/stream/hub.go (1)

13-13: LGTM!

Also applies to: 31-32, 79-91, 436-436, 446-453, 478-480

internal/app/app.go (1)

89-93: LGTM!

internal/app/wire.go (1)

28-32: LGTM!

Also applies to: 57-67, 70-85, 87-100, 173-173, 200-200, 243-246, 269-273, 322-322, 335-335, 373-373, 433-433, 492-495, 508-510, 521-529

internal/stream/hub_test.go (1)

13-21: LGTM!

Also applies to: 40-40, 81-81, 114-114, 144-144, 319-319, 352-352, 432-432, 485-485, 529-529, 555-555, 575-575, 592-592, 630-630, 646-646, 670-670, 685-685, 711-711, 728-735, 756-756, 777-777, 795-795, 826-826, 866-892, 924-924, 955-955, 1004-1004, 1049-1049, 1083-1083, 1118-1118, 1249-1249, 1275-1275, 1306-1306, 1342-1342, 1378-1380, 1396-1396, 1416-1416, 1440-1440, 1473-1473

internal/api/structured_query_test.go (1)

18-18: LGTM!

Also applies to: 44-44, 93-93, 107-107, 119-119, 149-149, 166-166, 175-175, 192-192, 202-202, 223-223, 236-236, 250-254, 293-293, 321-321, 353-353, 370-370, 385-385, 401-401, 416-416, 462-462, 481-481, 501-501, 528-528

internal/stream/tenant_test.go (1)

1-11: LGTM!

internal/discovery/discovery_test.go (1)

4-18: LGTM!

Also applies to: 53-56, 81-81, 114-114, 134-134, 155-155, 357-365, 379-379, 578-578, 597-640, 653-660, 714-714, 787-787

internal/discovery/main_test.go (1)

1-16: LGTM!

internal/discovery/timestamp_test.go (1)

6-12: LGTM!

Also applies to: 97-97, 134-134, 159-159, 190-190, 256-256, 278-278, 304-309, 310-315

internal/ingest/main_test.go (1)

1-16: LGTM!

internal/ingest/sweeper_test.go (1)

10-10: LGTM!

Also applies to: 23-23, 40-40, 54-54, 66-66

internal/ingest/worker_test.go (1)

31-38: LGTM!

Also applies to: 50-50, 123-123, 137-137, 155-155, 198-198, 248-248, 272-272, 299-306, 889-889, 1053-1053, 1089-1089, 1146-1146, 1184-1184, 1317-1317, 1485-1485, 1506-1506, 1599-1600, 1651-1652

internal/stream/roweval_test.go (1)

10-10: LGTM!

Also applies to: 61-61, 89-89, 105-105


📝 Summary

Summary by CodeRabbit

  • New Features

    • Added tenant-aware routing through X-Tenant-ID, including validation, default selection, and clear 400/404 responses before authentication.
    • Added browser CORS support for tenant selection.
  • Changed

    • Standardized structured logging through the default logger.
    • Settings reloads retain valid changes when queue-limit updates fail.
  • Bug Fixes

    • SSE replay now applies policy changes to subsequent events.
  • Documentation

    • Documented tenant routing, CORS usage, caching considerations, and tenant-aware architecture.

Walkthrough

Changes

The change adds validated tenant identifiers, registry-backed tenant resolution before authentication, tenant-aware API and asynchronous paths, default slog logging, shared log-test utilities, and per-event SSE policy replay.

Tenant routing and application wiring

Layer / File(s) Summary
Tenant contracts and routing
internal/tenant/*, internal/settings/registry.go, internal/api/tenant.go, internal/api/router.go
Defines tenant parsing and registry lookup. Resolves tenant stores before authentication. Preserves tenant-exempt routes and adds tenant-aware CORS handling.
Tenant-aware handlers and workers
internal/api/*, internal/app/*, internal/discovery/*, internal/ingest/*, internal/stream/*
Passes tenant-specific stores, policies, pipes, refresh intervals, and DLQ settings through synchronous and asynchronous paths.
SSE replay behavior
internal/stream/hub.go, internal/stream/hub_test.go
Reads policy for each replayed event and tests policy changes during gap-fill replay.

Default logging migration

Layer / File(s) Summary
Production logging
internal/auth/*, internal/api/*, internal/config/*, internal/discovery/*, internal/ingest/*, internal/mq/*, internal/settings/*
Removes injected logger fields and parameters. Uses package-level slog calls with operation context where available.
Test logging support
internal/testutil/logtest/*, internal/*/main_test.go, internal/testutil/testutil.go
Adds default-logger silence and capture helpers. Updates tests and removes testutil.NopLogger.
Documentation and integration updates
AGENTS.md, docs/src/content/docs/*, CHANGELOG.md, tests/integration/*
Documents tenant selection, logging, replay policy behavior, SDK headers, caching, and settings reload behavior.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~60 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant TenantMW
  participant Registry
  participant AuthMW
  participant TenantAwareHandler
  Client->>TenantMW: Send X-Tenant-ID
  TenantMW->>Registry: Resolve tenant store
  Registry-->>TenantMW: Return store or error
  TenantMW->>AuthMW: Attach store to request context
  AuthMW->>TenantAwareHandler: Authenticate and invoke route
  TenantAwareHandler->>Registry: Read tenant-specific policy and settings
Loading

Suggested reviewers: ericandrechek

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.67% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 271 functions across 50 files. (23 skippe… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the primary change: resolving X-Tenant-ID before authentication and threading tenant context through the application.
Description check ✅ Passed The description directly explains the tenant-resolution changes, logger cleanup, tests, scope, and follow-ups. It is relevant to the changeset.
Full details: Docstring Coverage

Explanation

Docstring coverage is 58.67% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 271 functions across 50 files. (23 skipped: 13 unsupported, 10 over the file limit.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
✨ Simplify code
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added documentation Improvements or additions to documentation go Pull requests that update go code area/api HTTP handlers, routing, middleware area/ingest Ingest pipeline (Bento, batching, DLQ) area/query Structured query AST, SQL builder area/docs Documentation, site/, README area/infra CI, build, deploy, Docker, release area/app Process wiring (internal/app): component build, run, release labels Sep 18, 2026
@github-actions

github-actions Bot commented Sep 18, 2026 •

Copy link
Copy Markdown

📚 Docs preview is live → https://79b7db19-wavehouse-docs.wave-rf.workers.dev

  • Commit — dc0b3a3: docs(tenant): scope the stray-header 404 to routes outside /v1/ops
  • Author — @taitelee
  • Committed — 2026-09-21 10:31 (UTC-04:00)
  • Deployed — 2026-09-21 11:23 EDT

@github-code-quality

github-code-quality Bot commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

Code Coverage Overview

Languages: Go

Go

The overall line coverage in commit dc0b3a3 in the tenant-plumbing branch remains at 92%, unchanged from commit 990f713 in the main branch.

Show a line coverage summary of the most impacted files.
File main 990f713 tenant-plumbing dc0b3a3 +/-
internal/policy/source.go 100% 0% -100%
internal/mq/embedded.go 86% 85% -1%
internal/app/wire.go 86% 87% +1%
internal/api/pipes.go 85% 86% +1%
internal/auth/auth.go 95% 97% +2%
internal/settings/watch.go 71% 74% +3%
internal/settings/store.go 95% 100% +5%
internal/api/tenant.go 0% 100% +100%
internal/tenant/tenant.go 0% 100% +100%
internal/settings/registry.go 0% 100% +100%

Updated September 21, 2026 14:35 UTC

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

⚠️ Outside diff range comments (1)

🟠 Major · Resolve policy for each replayed event. · hub.go:454

internal/stream/hub.go:454
🔒 Security & Privacy | 🛡️ Analyzed with Security Review | 🟠 Major | ⚡ Quick win

Authorization Bypass

Reachability: External
Exploitability: Difficult
CWE: CWE-863 — Incorrect Authorization

Resolve policy for each replayed event. PolicySource requires reads per event so a settings reload applies to the next event. ReplayProjector captures one policy before creating its reusable closure, so a revocation during gap-fill does not affect subsequent replayed events.

Proposed fix
 func (h *Hub) ReplayProjector(role string, sub *Subscriber) func(raw []byte) []Frame {
-	p, filter := h.snapshotPolicy()
 	var colSpecs map[string]policy.ColumnSpec
 	specsFor := ""
 	lastSig := ""
 	return func(raw []byte) []Frame {
+		p, filter := h.snapshotPolicy()
 		ev := newEventView(raw)

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: b8d9e5d6-9cea-4f5b-b25f-f3aeb746dfce

📥 Commits

Reviewing files that changed from the base of the PR and between 990f713 and 60e4d60.

📒 Files selected for processing (67)
  • .github/labeler.yml
  • AGENTS.md
  • CHANGELOG.md
  • docs/src/content/docs/api.md
  • docs/src/content/docs/architecture.md
  • docs/src/content/docs/development.md
  • docs/src/content/docs/sdk/index.mdx
  • internal/api/boot_chain_test.go
  • internal/api/dlq.go
  • internal/api/dlq_test.go
  • internal/api/errors.go
  • internal/api/errors_test.go
  • internal/api/ingest.go
  • internal/api/ingest_seams_test.go
  • internal/api/ingest_test.go
  • internal/api/main_test.go
  • internal/api/pipes.go
  • internal/api/pipes_test.go
  • internal/api/router.go
  • internal/api/router_test.go
  • internal/api/settings.go
  • internal/api/settings_test.go
  • internal/api/stream_test.go
  • internal/api/structured_query.go
  • internal/api/structured_query_test.go
  • internal/api/tenant.go
  • internal/api/tenant_helpers_test.go
  • internal/api/tenant_test.go
  • internal/app/app.go
  • internal/app/app_test.go
  • internal/app/wire.go
  • internal/auth/auth.go
  • internal/auth/auth_test.go
  • internal/auth/main_test.go
  • internal/chconn/chconn.go
  • internal/config/persistence.go
  • internal/config/persistence_test.go
  • internal/discovery/discovery.go
  • internal/discovery/discovery_test.go
  • internal/discovery/main_test.go
  • internal/discovery/timestamp.go
  • internal/discovery/timestamp_test.go
  • internal/ingest/main_test.go
  • internal/ingest/sweeper.go
  • internal/ingest/sweeper_test.go
  • internal/ingest/worker.go
  • internal/ingest/worker_test.go
  • internal/mq/embedded.go
  • internal/mq/embedded_test.go
  • internal/mq/main_test.go
  • internal/settings/main_test.go
  • internal/settings/registry.go
  • internal/settings/registry_test.go
  • internal/settings/store.go
  • internal/settings/store_test.go
  • internal/settings/watch.go
  • internal/stream/hub.go
  • internal/stream/hub_test.go
  • internal/stream/roweval_test.go
  • internal/stream/tenant_test.go
  • internal/tenant/tenant.go
  • internal/tenant/tenant_test.go
  • internal/testutil/logtest/logtest.go
  • internal/testutil/logtest/logtest_test.go
  • internal/testutil/testutil.go
  • tests/integration/boot_resilience_test.go
  • tests/integration/query_limits_test.go

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.

📜 Review details
⏰ Context from checks skipped due to timeout. (2)
  • GitHub Check: Coverage
  • GitHub Check: E2E tests
🧰 Additional context used
📓 Path-based instructions (3)
only `internal/mq` imports NATS/JetStream (`github.com/nats-io/…`)

📄 CodeRabbit inference engine (AGENTS.md)

Files:

  • internal/mq/main_test.go
  • internal/mq/embedded.go
  • internal/mq/embedded_test.go
WH001 applies to every tracked Markdown file, with no carve-out

📄 CodeRabbit inference engine (AGENTS.md)

Files:

  • docs/src/content/docs/development.md
  • docs/src/content/docs/architecture.md
  • docs/src/content/docs/sdk/index.mdx
  • docs/src/content/docs/api.md
  • AGENTS.md
  • CHANGELOG.md
**`.mdx` is never auto-fixed by the generic markdownlint rules**

📄 CodeRabbit inference engine (AGENTS.md)

Files:

  • docs/src/content/docs/sdk/index.mdx
🧠 Learnings (4)
📚 Learning: 2026-06-26T12:23:22.696Z
Learnt from: EricAndrechek
Repo: Wave-RF/WaveHouse PR: 346
File: internal/stream/subscriber_test.go:9-28
Timestamp: 2026-06-26T12:23:22.696Z
Learning: In this Go repository, prefer table-driven tests (e.g., `[]struct{...}` with `t.Run(...)`) only for tests that cover multiple scenarios/inputs and can be cleanly enumerated. Do not artificially rewrite a clear single-scenario sequential behavioral-flow test into a table-driven form just to fit the pattern; if there’s only one meaningful scenario, keep the test as a straightforward linear flow (as in `TestSubscriber_SendDeliversThenDropsWhenFull`).

Applied to files:

  • internal/tenant/tenant_test.go
  • internal/app/app_test.go
  • internal/api/tenant_test.go
📚 Learning: 2026-05-23T01:23:59.268Z
Learnt from: EricAndrechek
Repo: Wave-RF/WaveHouse PR: 174
File: internal/api/ingest_test.go:111-111
Timestamp: 2026-05-23T01:23:59.268Z
Learning: In WaveHouse Go tests in internal/api/**/*_test.go, use internal/testutil.AssertJSONErrorResponse(t, w) for HTTP error-path JSON assertions. Do not use (or reintroduce) package-local assertJSONErrorResponse helpers. AssertJSONErrorResponse verifies the response Content-Type is application/json, includes the X-Content-Type-Options: nosniff header, and that the JSON body contains an "error" field.

Applied to files:

  • internal/api/tenant_test.go
📚 Learning: 2026-08-11T21:55:53.726Z
Learnt from: jfwoods
Repo: Wave-RF/WaveHouse PR: 434
File: clients/go/stream_test.go:159-176
Timestamp: 2026-08-11T21:55:53.726Z
Learning: For Go files in this repository, do not report direct type assertions solely because the `forcetypeassert` rule is commented out in `.golangci.yml`. Only flag a type assertion when there is an independent correctness, safety, or maintainability issue.

Applied to files:

  • internal/api/tenant.go
📚 Learning: 2026-06-10T15:01:09.027Z
Learnt from: EricAndrechek
Repo: Wave-RF/WaveHouse PR: 312
File: docs/src/content/docs/development.md:0-0
Timestamp: 2026-06-10T15:01:09.027Z
Learning: In this repo’s Markdown review (all .md files), do not flag capitalization/style issues for literal paths starting with ".github/" (or any substring that is a path beginning with ".github/"). Treat ".github" as the correct lowercase dotfile directory name, even when it appears inside prose or code spans; automated checks such as LanguageTool’s "(GITHUB)" rule commonly produce false positives for this literal filesystem path.

Applied to files:

  • CHANGELOG.md
🪛 ast-grep (0.45.3)
internal/config/persistence.go

[warning] 28-28: A log/format call (log.Print/Printf/Println, the Fatal/Panic variants, fmt.Sprintf, or a structured logger's Info/Warn/Error/Debug method) is given a message built by concatenating a string literal with a non-literal value such as request data. Unsanitized, attacker-controlled input written to logs enables log forging / CRLF injection: an attacker can inject newlines to spoof log entries or break log parsers. Do not concatenate raw input into the log message; pass it as a separate structured field/argument (e.g. 'log.Printf("user: %s", user)' or 'logger.Info("login", "user", user)') and strip or escape newline characters first.
Context: slog.Error(kind+" init failed", fields...)
Note: [CWE-117] Improper Output Neutralization for Logs.

(log-injection-request-data-concat-go)

🪛 LanguageTool
AGENTS.md

[grammar] ~48-~48: Please add a punctuation mark at the end of paragraph.
Context: ...th a tenant.ID and their getters take it ## Key Design Decisions The invariant...

(PUNCTUATION_PARAGRAPH_END)

CHANGELOG.md

[style] ~13-~13: This sentence is over 40 words long. Consider splitting it up, as shorter sentences make the text easier to read.
Context: ...ons. --> ## Unreleased ### Added - Requests resolve to a tenant before authentication, and the tenant is threaded through every settings read (internal/tenant/ (new, + tests), internal/settings/registry.go (new, + tests), internal/api/tenant.go (new, + tests), internal/api/{router,ingest,structured_query,pipes}.go, internal/ingest/{worker,sweeper}.go, internal/stream/hub.go, internal/discovery/discovery.go, internal/app/{app,wire}.go): story 1 of the multi-tenant epic (#583), with no behavior change for a deployment that sends no tenant header. internal/tenant defines the id — a va...

(TOO_LONG_SENTENCE)

🔇 Additional comments (38)
internal/api/errors.go (1)

44-44: LGTM!

Also applies to: 59-59, 82-82, 103-103

internal/auth/auth.go (1)

69-70: LGTM!

Also applies to: 79-81, 116-117, 121-121, 145-147, 159-159, 203-209, 243-256, 276-279

internal/config/persistence.go (1)

24-24: LGTM!

Also applies to: 29-29, 42-42, 50-50, 56-56, 58-58, 64-64

internal/settings/watch.go (1)

6-6: LGTM!

Also applies to: 54-55, 93-93

internal/config/persistence_test.go (1)

13-13: LGTM!

Also applies to: 18-23, 26-26, 41-44, 55-58, 67-72, 81-83, 89-95, 106-108

internal/testutil/logtest/logtest.go (1)

1-59: LGTM!

internal/testutil/logtest/logtest_test.go (1)

1-29: LGTM!

tests/integration/query_limits_test.go (1)

20-20: LGTM!

Also applies to: 95-102, 108-110

internal/auth/auth_test.go (1)

17-17: LGTM!

Also applies to: 41-45, 48-53, 321-321, 333-333, 490-490, 508-508, 518-524, 541-542, 554-555, 563-564, 571-572, 631-631, 662-662

internal/stream/roweval_test.go (1)

10-10: LGTM!

Also applies to: 61-61, 89-89, 105-105

internal/chconn/chconn.go (1)

79-80: LGTM!

internal/discovery/discovery.go (1)

11-11: LGTM!

Also applies to: 181-186, 194-196, 200-200, 243-243, 302-302, 310-310, 435-435, 444-446

internal/discovery/timestamp.go (1)

4-4: LGTM!

Also applies to: 99-99, 107-107

internal/ingest/sweeper.go (1)

10-10: LGTM!

Also applies to: 24-28, 31-32, 34-34, 37-37, 57-57, 61-61, 64-64

internal/ingest/worker.go (1)

22-22: LGTM!

Also applies to: 67-74, 134-135, 183-183, 240-240, 279-279, 464-464, 469-470, 476-477, 490-491, 497-498, 569-569, 577-581, 683-683, 696-696, 711-711, 726-727, 735-736, 766-766, 776-777

internal/mq/embedded.go (1)

19-24: LGTM!

Also applies to: 27-28, 31-32, 35-36, 39-40, 43-44, 82-85, 102-102, 133-133, 336-336, 445-451

internal/settings/store.go (1)

28-28: LGTM!

Also applies to: 44-45, 73-90

internal/discovery/discovery_test.go (1)

16-18: LGTM!

Also applies to: 53-56, 81-81, 114-114, 134-134, 155-155, 357-357, 365-365, 379-379, 578-578, 613-614, 668-668, 741-741

internal/ingest/worker_test.go (1)

31-31: LGTM!

Also applies to: 38-38, 123-123, 137-137, 155-155, 272-272, 299-300, 306-306, 889-889, 1053-1053, 1317-1317, 1485-1485, 1506-1506, 1599-1600, 1651-1652

internal/testutil/testutil.go (1)

17-17: LGTM!

Also applies to: 31-31

tests/integration/boot_resilience_test.go (1)

20-20: LGTM!

Also applies to: 69-69, 91-91

internal/api/main_test.go (1)

1-15: LGTM!

internal/discovery/main_test.go (1)

1-15: LGTM!

internal/discovery/timestamp_test.go (1)

11-12: LGTM!

Also applies to: 97-97, 134-134, 159-159, 190-190, 256-256, 278-278, 304-304

internal/ingest/main_test.go (1)

1-15: LGTM!

internal/ingest/sweeper_test.go (1)

10-10: LGTM!

Also applies to: 23-23, 40-40, 54-54, 66-66

internal/mq/embedded_test.go (1)

15-19: LGTM!

Also applies to: 293-293, 511-511

internal/mq/main_test.go (1)

1-14: LGTM!

internal/settings/main_test.go (1)

1-13: LGTM!

internal/settings/store_test.go (1)

21-21: LGTM!

Also applies to: 91-91, 95-95, 125-125

internal/auth/main_test.go (1)

1-15: LGTM!

docs/src/content/docs/development.md (1)

350-350: LGTM!

docs/src/content/docs/sdk/index.mdx (1)

380-380: LGTM!

internal/settings/registry_test.go (1)

1-24: LGTM!

internal/tenant/tenant.go (1)

1-50: LGTM!

internal/tenant/tenant_test.go (1)

1-53: LGTM!

internal/app/app_test.go (1)

27-27: LGTM!

Also applies to: 147-175

internal/app/wire.go (1)

28-32: LGTM!

Also applies to: 57-62, 78-92, 157-157, 184-184, 227-230, 253-257, 306-306, 319-319, 357-361, 421-421, 480-483, 496-498, 509-517

Comment thread internal/api/tenant_test.go Outdated
Comment thread internal/api/tenant.go Outdated
Comment thread internal/stream/hub.go
@github-project-automation github-project-automation Bot moved this from Backlog to In review in WaveHouse Task Board Sep 18, 2026
@taitelee

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 18, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

⚠️ Outside diff range comments (1)

🟡 Minor · Fix the malformed go:embed wording. · AGENTS.md:46

AGENTS.md:46
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the malformed go:embed wording.

The text renders as "go:embedded seed". Use “embedded seed” or "go:embed-embedded seed”.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 9b449254-5034-401c-b03b-345219fc455d

📥 Commits

Reviewing files that changed from the base of the PR and between 60e4d60 and f53507f.

📒 Files selected for processing (17)
  • AGENTS.md
  • CHANGELOG.md
  • docs/src/content/docs/access-control.mdx
  • docs/src/content/docs/api.md
  • docs/src/content/docs/architecture.md
  • docs/src/content/docs/development.md
  • docs/src/content/docs/reverse-proxy.mdx
  • docs/src/content/docs/sdk/index.mdx
  • docs/src/content/docs/sdk/reference.md
  • docs/src/content/docs/sdk/streaming.md
  • docs/src/content/docs/settings-directory.mdx
  • internal/api/structured_query.go
  • internal/api/tenant.go
  • internal/api/tenant_test.go
  • internal/app/wire.go
  • internal/stream/hub.go
  • internal/stream/hub_test.go

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.

📜 Review details
🧰 Additional context used
📓 Path-based instructions (1)
In MDX, leave a blank line between a JSX tag and a code fence.

📄 CodeRabbit inference engine (AGENTS.md)

Files:

  • docs/src/content/docs/settings-directory.mdx
  • docs/src/content/docs/reverse-proxy.mdx
  • docs/src/content/docs/access-control.mdx
  • docs/src/content/docs/sdk/index.mdx
🧠 Learnings (4)
📓 Common learnings
Learnt from: taitelee
Repo: Wave-RF/WaveHouse

Timestamp: 2026-09-18T14:35:32.276Z
Learning: In `internal/api/tenant.go`, `opsStore` must strictly parse the complete `r.URL.RawQuery` with `url.ParseQuery`. If parsing fails, it returns `400` with `invalid ?tenant: malformed query string`. This intentional behavior applies to admin-only operations pipe reads, even when the malformed pair is not identifiable as the `tenant` parameter.
📚 Learning: 2026-08-13T12:17:52.620Z
Learnt from: EricAndrechek
Repo: Wave-RF/WaveHouse PR: 470
File: docs/src/content/docs/reverse-proxy.mdx:137-144
Timestamp: 2026-08-13T12:17:52.620Z
Learning: For Wave-RF/WaveHouse documentation, verify claims about implementation control flow against the authoritative implementation source (for example, internal/auth/auth.go) rather than relying solely on docs/** content. Documentation may lag behind or paraphrase behavior, so control-flow claims should be confirmed in source code.

Applied to files:

  • docs/src/content/docs/settings-directory.mdx
  • docs/src/content/docs/reverse-proxy.mdx
  • docs/src/content/docs/access-control.mdx
📚 Learning: 2026-05-23T01:23:59.268Z
Learnt from: EricAndrechek
Repo: Wave-RF/WaveHouse PR: 174
File: internal/api/ingest_test.go:111-111
Timestamp: 2026-05-23T01:23:59.268Z
Learning: In WaveHouse Go tests in internal/api/**/*_test.go, use internal/testutil.AssertJSONErrorResponse(t, w) for HTTP error-path JSON assertions. Do not use (or reintroduce) package-local assertJSONErrorResponse helpers. AssertJSONErrorResponse verifies the response Content-Type is application/json, includes the X-Content-Type-Options: nosniff header, and that the JSON body contains an "error" field.

Applied to files:

  • internal/api/tenant_test.go
📚 Learning: 2026-06-26T12:23:22.696Z
Learnt from: EricAndrechek
Repo: Wave-RF/WaveHouse PR: 346
File: internal/stream/subscriber_test.go:9-28
Timestamp: 2026-06-26T12:23:22.696Z
Learning: In this Go repository, prefer table-driven tests (e.g., `[]struct{...}` with `t.Run(...)`) only for tests that cover multiple scenarios/inputs and can be cleanly enumerated. Do not artificially rewrite a clear single-scenario sequential behavioral-flow test into a table-driven form just to fit the pattern; if there’s only one meaningful scenario, keep the test as a straightforward linear flow (as in `TestSubscriber_SendDeliversThenDropsWhenFull`).

Applied to files:

  • internal/stream/hub_test.go
🪛 LanguageTool
docs/src/content/docs/sdk/streaming.md

[style] ~132-~132: Since ownership is already implied, this phrasing may be redundant.
Context: ...ed more of on this path; see Supplying your own fetch. ...

(PRP_OWN)

docs/src/content/docs/sdk/reference.md

[style] ~59-~59: Consider an alternative for the overused word “exactly”.
Context: ...roxy. That silent-downgrade behavior is exactly why auth is re-read on every connecti...

(EXACTLY_PRECISELY)

CHANGELOG.md

[style] ~13-~13: This sentence is over 40 words long. Consider splitting it up, as shorter sentences make the text easier to read.
Context: ...ons. --> ## Unreleased ### Added - Requests resolve to a tenant before authentication, and the tenant is threaded through every settings read (internal/tenant/ (new, + tests), internal/settings/registry.go (new, + tests), internal/api/tenant.go (new, + tests), internal/api/{router,ingest,structured_query,pipes}.go, internal/ingest/{worker,sweeper}.go, internal/stream/hub.go, internal/discovery/discovery.go, internal/app/{app,wire}.go): story 1 of the multi-tenant epic (#583), with no behavior change for a deployment that sends no tenant header. internal/tenant defines the id — a va...

(TOO_LONG_SENTENCE)

docs/src/content/docs/architecture.md

[typographical] ~99-~99: Consider using an em dash in dialogues and enumerations.
Context: - hub.go — Hub, the event fan-out. ...

(DASH_RULE)


[style] ~99-~99: Since ownership is already implied, this phrasing may be redundant.
Context: ...th's: ReplayProjector tracks drift in its own state and the two are not reconciled ([...

(PRP_OWN)

🔇 Additional comments (16)
AGENTS.md (1)

32-32: LGTM!

Also applies to: 47-48, 78-81, 444-445

docs/src/content/docs/development.md (1)

350-350: LGTM!

Also applies to: 471-471, 498-498

CHANGELOG.md (1)

25-26: LGTM!

Also applies to: 65-66

docs/src/content/docs/sdk/reference.md (1)

33-33: LGTM!

Also applies to: 59-59

docs/src/content/docs/access-control.mdx (1)

363-363: LGTM!

docs/src/content/docs/sdk/streaming.md (1)

130-130: LGTM!

Also applies to: 136-136

internal/api/tenant.go (1)

7-7: LGTM!

Also applies to: 98-108

internal/api/tenant_test.go (1)

58-58: LGTM!

Also applies to: 182-183, 194-196, 202-202

docs/src/content/docs/api.md (1)

61-61: LGTM!

Also applies to: 176-176, 642-642, 784-784, 788-788

internal/stream/hub.go (1)

446-450: LGTM!

Also applies to: 478-480

internal/stream/hub_test.go (1)

13-13: LGTM!

Also applies to: 866-886

docs/src/content/docs/architecture.md (1)

71-71: LGTM!

Also applies to: 80-80, 93-93, 99-99, 107-107, 167-167, 185-192, 207-208, 301-302

docs/src/content/docs/reverse-proxy.mdx (1)

197-197: LGTM!

docs/src/content/docs/sdk/index.mdx (1)

338-338: LGTM!

Also applies to: 380-380

internal/api/structured_query.go (1)

38-40: LGTM!

internal/app/wire.go (1)

73-74: LGTM!

Also applies to: 79-79, 87-94, 360-364

Comment thread docs/src/content/docs/settings-directory.mdx Outdated
@taitelee

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 18, 2026 •

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@taitelee

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 18, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Sep 18, 2026
@EricAndrechek EricAndrechek added the area/tenant Tenant id, header resolution, per-tenant settings (internal/tenant) label Sep 18, 2026
@taitelee

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 19, 2026 •

Copy link
Copy Markdown
⚠️ Action not completed

Already reviewed the last commit. Use @coderabbitai full review to rerun a review of the entire changeset.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@taitelee

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Sep 19, 2026 •

Copy link
Copy Markdown
✅ Action performed

Full review finished.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Sep 19, 2026

@EricAndrechek EricAndrechek left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the full diff at f1bbc279 against epic #583's story 1, plus both CodeRabbit rounds and CI.

Scope first, because that's where the defects cluster. Story 1's checklist is delivered essentially in full: internal/tenant, the middleware (validate → default 0 → resolve → 404 → context), running before auth, handlers taking the store as an explicit argument, the getters threaded through api/ingest/discovery/async, wired to the default tenant in wire.go, and the slog cleanup owed from #586. Nothing material is missing.

Three things went beyond it, and every merge-relevant defect is in one of them:

  1. ?tenant= on the ops pipe reads — story 2 scope per the epic, and the PR body says as much. The one MUST-level bug (bearerToken defeating opsStore's strict parse) lives entirely here. Worth considering dropping it so story 2 lands it together with the reload route, the bearerToken fix, and a router-level regression test.
  2. Vary: X-Tenant-ID — unlisted scope from round two. The code is right — Add not Set, and the new test genuinely pins the composition — but the sentence documenting it overstates the guarantee.
  3. settings.Registry — story 2 scope, but unavoidable, since story 1's middleware must resolve against something. Shipping it as a stub is why no test can catch a wrong-store derivation.

One epic gap I'd like settled before merge: no story in #583 owns the admin-authorization axis. wire.go:63 binds RequireAdmin's policy source to tenant 0's store, so tenant 0's policies.json becomes a fleet-wide authorization authority the moment a second tenant exists. Story 9 covers the JWKS verifier only, not the policy source. Detail on that line.

Everything else is latent until story 3/5/6, or a docs fix. Full context on each comment, including which story owns it.

Comment thread docs/src/content/docs/api.md Outdated
Comment thread internal/api/pipes.go Outdated
Comment thread internal/api/pipes.go Outdated
Comment thread internal/api/pipes.go
Comment thread internal/api/pipes.go
Comment thread internal/discovery/discovery.go
Comment thread internal/ingest/worker.go
Comment thread internal/api/tenant_helpers_test.go
Comment thread internal/api/router.go
Comment thread internal/auth/auth.go
@taitelee

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Sep 21, 2026 •

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@taitelee
taitelee marked this pull request as ready for review September 21, 2026 15:20
@taitelee
taitelee requested review from a team and EricAndrechek September 21, 2026 15:20
@taitelee
taitelee merged commit d8092d4 into main Sep 21, 2026
29 checks passed
@taitelee
taitelee deleted the tenant-plumbing branch September 21, 2026 15:20
@github-project-automation github-project-automation Bot moved this from In review to Done in WaveHouse Task Board Sep 21, 2026
taitelee added a commit that referenced this pull request Sep 22, 2026
…598)

## Summary

Story 2 of the multi-tenant epic: a settings directory can hold one
folder per tenant, and the registry owns every reload. No behavior
change for a directory that holds the four files — the same findings
byte for byte, the same boot refusal, the same keep-previous on a
rejected reload, the same watcher and `SIGHUP`, the same `wavehouse
validate` exit codes.

- `settings.Validate` reads the shape off the root: any of the four file
names makes it flat (`ValidateDir`, the old `Validate`, untouched);
otherwise a folder makes it nested — folder names through the tenant-id
grammar, each folder through `ValidateDir`, findings led by the folder
(`acme/policies.json`). The shapes never mix. A folder whose name is not
a tenant id is reported and skipped; an entry that cannot be stat'ed (a
dangling symlink) is a finding about the root.
- `settings.Open` returns the `Registry`, which owns `Reload`,
`ReloadTenant`, the `AfterAdopt` hooks and the watcher; `Store` is a
passive holder. A nested directory fails closed per tenant, at boot and
on reload alike: a rejected folder stops its tenant (a bare `503` on
tenant routes, which resolve before authentication), the rest carry on,
and there is no previous-snapshot fallback. A whole-tree reload mirrors
the folders and names a removed tenant in the log. A finding about the
directory itself refuses boot and rejects a reload whole. A nested
directory gets no watcher (`Registry.Watch` refuses one); `SIGHUP`
reloads the whole tree in both shapes.
- `POST /v1/ops/settings/reload` and `GET /v1/ops/pipes[/{name}]` take
an optional `?tenant=`, parsed strictly (`400` for a query that does not
parse or an empty, repeated or malformed id; `404` unknown; absent means
the whole tree on the reload and tenant `0` on the reads). It lands with
the `auth.bearerToken` fix deferred from #593 — the `?token=` strip no
longer repairs a query that does not parse — pinned through `NewRouter`
with the real authenticator.
- Over a nested directory the `/v1/ops/*` gate admits the operator key
alone and a token admin gets `403`; `NewRouter` decides that from the
registry's shape, whatever policy source was wired. Boot warns when a
nested directory has no operator key, since `SIGHUP` is then the only
reload.
- The resources a process still has one of (ClickHouse connection,
dedupe store, MQ budget, verifier, CORS list) follow the settings tenant
`0` last adopted, and their hooks run only when tenant `0` is adopted,
so a rejected or removed `0` folder leaves them as they were. The
keepalive wheel runs at the shortest `keepalive_interval` among the
tenants being served (#597).
- SDK: `wh.pipes.list()`, `wh.pipes.get()` and `wh.settings.reload()`
take a `tenant` option (`OpsRequestOptions`), sent as `?tenant=`.
- Decided with Eric on 2026-09-21, not yet in the #583 body: the
rejected tenant's `503` is generic; the reload body is unchanged and a
`422` can mean adopted in part; a failure about the directory itself
rejects the reload whole; one shared keepalive wheel, with #597 tracking
per-tenant keepalive; a badly named folder is skipped; a whole-tree
reload mirrors the folders; `Dependencies.PolicySource` stays and is
ignored when the registry is nested.

## Test plan

- [x] `make ci` passes locally (unit, integration, e2e, coverage gates)
- [x] Flat root: `Validate` equals `ValidateDir` across six root shapes,
findings and document alike
- [x] Nested: fail closed per tenant at boot and on reload; recovery by
reload into the same `*Store`; folder mirroring; a root-level failure
changing nothing (four shapes); a shape change refused in both
directions; symlinked and dangling tenant folders; an unreadable folder
- [x] `TestNewRouter_HandlersReceiveTheRequestTenantsStore`:
`assert.Same` on the store each handler's getters received, two tenants
alternating through one router (fails under two wrong-store mutations)
- [x] Nested ops gate matrix (2 shapes × 4 callers × 3 routes); the
strict `?tenant=` table on all three routes;
`TestNewRouter_MalformedTenantSurvivesTheTokenStrip` (fails against the
pre-fix `bearerToken`)
- [x] `internal/app`: a nested boot without a `0` folder; the operator
reloading one tenant by name; the process-wide resources following
tenant `0` through another tenant's reload, a rejected `0` folder and a
removed one (dedupe, MQ budget, CORS); the no-operator-key boot warning
- [x] SDK unit tests, and e2e through the real server (`tenant: "0"`,
unknown `404`, malformed and empty `400`)
- [ ] Manual: over `acme/` + `globex/`, `curl -H 'X-Tenant-ID: acme'
/v1/pipes/x` → `404 pipe not found`; no header → `404 unknown tenant: 0`

## Follow-ups

- A second tenant is not isolation yet (stories 5, 6, 9): one
ClickHouse, one verifier (a token is accepted under any tenant's
header), and MQ topics that carry no tenant — so `/v1/stream` delivery
is by table alone and is authorized by tenant `0`'s policy, and the most
permissive tenant's `default_role` is the effective floor.
- A nested directory that serves no tenant `0` boots unconfigured: no
ClickHouse address, `/livez` degraded, the MQ byte budget uncapped
(inert, since nothing publishes). Until story 6.
- A lost tenant `0` (its folder rejected or removed by a reload): the
five settings read through `defaultSetting` keep their last adopted
values, but the async paths read tenant `0` as unset — the hub reads no
policy (every stream subscriber's rows withheld), the sweeper's gap
window is zero (gap-fill history purged), the DLQ switch reads on — and
`perTenant` logs an error per event. Story 3 owns the async paths' miss
semantics; left exactly as story 1 left them.
- A whole-tree reload can catch a folder halfway through being written
and reject its tenant until a later reload (story 3, where whole-tree
reloads become routine).
- #597: per-tenant keepalive. The wheel is also recomputed only after an
adoption.
- `GET /v1/ops/schema`, `POST /v1/ops/schema/refresh`, `GET
/v1/ops/dlq/stats` and `POST /v1/ops/query` ignore `?tenant=` (stories 5
and 6).
- The operator key is stamped with tenant `0`'s admin role name (story
9). The dedupe store keys on the event id alone (story 7). The pre-auth
`400`/`404`/`503` split distinguishes tenant ids, `/v1/health` included
(story 3).
- A `settings.dir` pointed at a directory of unrelated subfolders boots
as nested with nothing served; `wavehouse validate` exits 1 for a nested
directory with one bad folder where boot would still serve the rest.
- Docs for story 11: creating a nested directory by hand, a runnable
control-plane loop, that a non-`0` folder must still carry every
required key, and `config.yaml`'s flat-only `settings:` comment.
- #583 body: amend story 3's first sentence (a whole-tree reload already
mirrors the folders) and add the decisions above to the Decisions
section.

## Related Issues

Part of #583 (story 2). Lands the `?tenant=` and `bearerToken` work
deferred from #593.

<!--
Checklist for the author (not kept in the squash commit message):

- `make ci` passes locally
- Docs updated per AGENTS.md "Documentation & Consistency Sync" rules
- CHANGELOG.md [Unreleased] entry added
- Tests cover new / changed behavior (70 % minimum, 80 %+ preferred)

The PR title is the squash commit subject — use Conventional Commits
(`feat:`, `fix:`, `docs:`, `refactor:`, `test:`, `chore:`, `ci:`,
`deps:`, `build:`, `perf:`, `revert:`, `style:`). The PR body below is
the squash commit message, so keep it tight.
-->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/api HTTP handlers, routing, middleware area/app Process wiring (internal/app): component build, run, release area/docs Documentation, site/, README area/infra CI, build, deploy, Docker, release area/ingest Ingest pipeline (Bento, batching, DLQ) area/query Structured query AST, SQL builder area/tenant Tenant id, header resolution, per-tenant settings (internal/tenant) documentation Improvements or additions to documentation go Pull requests that update go code

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants