Skip to content

Independent deployment: compose-config endpoint and deployment docs - #1465

Merged
feruzm merged 4 commits into
developfrom
feature/self-hosted-independent-deploy
Aug 13, 2026
Merged

Independent deployment: compose-config endpoint and deployment docs#1465
feruzm merged 4 commits into
developfrom
feature/self-hosted-independent-deploy

Conversation

@feruzm

@feruzm feruzm commented Aug 12, 2026

Copy link
Copy Markdown
Member

First half of #1453 (the server and documentation side). The web fork that consumes this endpoint follows in a separate PR, since it cannot ship before this is deployed.

POST /v1/tools/compose-config

Runs TenantService.buildConfig on a customize payload and returns the composed document. It creates nothing: no tenant row, no reservation, no published config file, no payment lock. The route speaks the same vocabulary as createTenantSchema and shares the same rosters, so one payload composes identically down either path.

Managed-only markers are stripped before the document is returned, because each one misdirects an instance somebody else runs:

  • managed is the only signal an instance has that it is hosted here; on another domain it flips the Configuration Editor from Download to Save, and that Save calls a hosting API that is not theirs.
  • template replaces the whole site with the claim landing page.
  • claimPreview marks the read-only preview of an unclaimed subdomain.
  • hivesigner.clientId may be Ecency's own app, which only answers to redirect URIs registered for Ecency's domains, so an owner's login would fail on their own site.

Owner rule: a personal blog is owned by its own account; a community requires a separate owner, because a community account holds nobody's keys and an instance owned by itself would be permanently locked out of its own editor. The rest of resolveAndValidateTenant (account existence, community control) is deliberately not run, as nothing here is registered anywhere.

Rate limited on its own budget as well as the general one, since it is anonymous and each call can spend a chain lookup.

Tests assert on the response body and prove the absences: the DB client and ConfigService are mocked as throwing proxies, so a future edit that starts creating rows or publishing files fails loudly rather than silently putting a blog live.

Deployment drift

docker-compose.yml documented TAG while hardcoding image: ecency/self-hosted:latest, so every deployment following the guide silently ran latest (last published 2026-08-12 08:21, before the release-tag change) rather than the pinned build. It now requires TAG the way the managed stack does, and no longer carries a build: section that would ignore the pin and try to build from a monorepo the recipient does not have.

DEPLOYMENT.md corrections: a new tag-choosing section stating which tags move and which do not (no vX.Y.Z tag exists yet, so sha-<7> is what to pin today); journal and reader added to the template table with their real taglines and their structural limitations; the port corrected from 80 to 3000 throughout; Caddy promoted to the recommended HTTPS path with a working two-line example; the managed platform's topology labelled as reference rather than instruction, since its :3100 ports are not a self-hoster's; Option 4 no longer tells readers to add a Dockerfile ARG that already exists; upgrade and rollback described as the one-line tag change they are; the stale pricing table replaced by a pointer to the hosting page, which is what it should have been.

hosting/README.md listed a nginx/ directory that does not exist and omitted four that do. It now also notes that the unused traefik/ config sets X-Robots-Tag: noindex, nofollow, which would deindex every tenant blog if it were ever wired in.

Verified: 463 hosting API tests and npm run build (tsc), which CI does not run on PRs for this package. docker compose config validates with a TAG and fails loudly without one.

Summary by CodeRabbit

  • New Features

    • Added a self-hosted configuration endpoint for generating customized deployment settings.
    • Added validation for ownership, community setup, account names, and configuration options.
    • Added rate limiting to configuration tools.
    • Added support for immutable image tags and .env-based Compose configuration.
  • Documentation

    • Updated deployment, upgrade, troubleshooting, proxy, SEO, theme, hosting, pricing, and CI guidance.
    • Documented current reverse-proxy and HTTPS deployment workflows.
  • Bug Fixes

    • Removed managed-only settings from generated self-hosted configurations.
    • Improved handling of missing SEO files and empty configuration values.

Adds POST /v1/tools/compose-config, which runs the same config builder the
managed signup uses and returns the document without creating a tenant row,
a reservation or a published file, so an independent deployment can start
from the look its owner customized. Managed-only markers (managed, template,
claimPreview and an Ecency-owned hivesigner clientId) are stripped, since
each of them misdirects an instance someone else runs.

The standalone compose file required a TAG that it never read, so it always
ran :latest; it now demands one the way the managed stack does. Deployment
docs corrected to match what the app and images actually do.
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Aug 12, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Community-shaped username bypasses owner rule ✓ Resolved 🐞 Bug ≡ Correctness
Description
In tools.ts, the new compose-config route derives isCommunity only from `body.config?.type ===
'community'`, unlike tenants.ts where community status is also inferred from the username pattern to
prevent callers from omitting type and bypassing the separate-owner requirement. As a result, a
POST like { username: 'hive-125125' } with no config.type takes the personal-blog branch and
returns a composed config with owner === 'hive-125125', creating a community deployment that is
permanently locked out of its own editor because no user holds that account’s keys.
Code

apps/self-hosted/hosting/api/src/routes/tools.ts[R104-107]

+toolsRoutes.post('/compose-config', composeLimit, zValidator('json', composeConfigSchema), async (c) => {
+  const body = c.req.valid('json');
+  const username = body.username.toLowerCase();
+  const isCommunity = body.config?.type === 'community';
Relevance

●●● Strong

Strong precedent: derive community status from hive-* shape, not caller-controlled config.type, to
prevent owner-rule bypass.

PR-#1309

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
In tenants.ts, resolveAndValidateTenant explicitly treats a tenant as a community if either the
username matches COMMUNITY_NAME.test(username) or config.type === 'community', and even
documents that trusting caller-supplied config.type alone is unsafe because it allows omitting the
discriminator and skipping the community ownership check (a bypass previously accepted as a real
bug, e.g., PR #1309). In contrast, the new tools.ts route computes community status only from
config.type, then falls back to the blog branch when it’s missing and assigns owner = username,
which means a community-shaped username can be composed with the community account as owner; since
the SPA gates editor access on instanceConfiguration.owner, this results in an un-administerable
deployment.

apps/self-hosted/hosting/api/src/routes/tenants.ts[170-181]
apps/self-hosted/hosting/api/src/routes/tools.ts[104-129]
apps/self-hosted/src/features/auth/auth-provider.tsx[43-53]
PR-#1309

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
In `POST /v1/tools/compose-config`, community detection relies only on `body.config?.type === 'community'`, allowing callers to submit a community-shaped username (e.g., matching `hive-\d+`) without setting `config.type` (or setting it to `blog`) and thereby bypass the separate-owner rule for communities. This incorrectly routes the request through the personal-blog branch and sets `owner` to the community account itself, which the SPA uses for its ownership gate, producing a permanently locked-out deployment.
## Issue Context
The existing tenant creation/validation flow in `tenants.ts` already addresses this exact bypass by deriving `isCommunity` from `COMMUNITY_NAME.test(username) || body.config?.type === 'community'`, with an explicit comment explaining why trusting only the caller-supplied type is unsafe. `tools.ts` already imports `COMMUNITY_NAME` from `tenant-service.ts` but does not use it for this check; the compose endpoint should enforce the same semantics as tenant creation and include a regression test that covers an omitted discriminator.
## Fix Focus Areas
- apps/self-hosted/hosting/api/src/routes/tools.ts[104-129]
- apps/self-hosted/hosting/api/src/routes/tools.test.ts[90-116]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Community-shaped username bypasses owner rule ✓ Resolved 🐞 Bug ≡ Correctness
Description
In tools.ts, the new compose-config route derives isCommunity only from `body.config?.type ===
'community'`, unlike tenants.ts where community status is also inferred from the username pattern to
prevent callers from omitting type and bypassing the separate-owner requirement. As a result, a
POST like { username: 'hive-125125' } with no config.type takes the personal-blog branch and
returns a composed config with owner === 'hive-125125', creating a community deployment that is
permanently locked out of its own editor because no user holds that account’s keys.
Code

apps/self-hosted/hosting/api/src/routes/tools.ts[R104-107]

+toolsRoutes.post('/compose-config', composeLimit, zValidator('json', composeConfigSchema), async (c) => {
+  const body = c.req.valid('json');
+  const username = body.username.toLowerCase();
+  const isCommunity = body.config?.type === 'community';
Relevance

●●● Strong

Strong precedent: derive community status from hive-* shape, not caller-controlled config.type, to
prevent owner-rule bypass.

PR-#1309

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
In tenants.ts, resolveAndValidateTenant explicitly treats a tenant as a community if either the
username matches COMMUNITY_NAME.test(username) or config.type === 'community', and even
documents that trusting caller-supplied config.type alone is unsafe because it allows omitting the
discriminator and skipping the community ownership check (a bypass previously accepted as a real
bug, e.g., PR #1309). In contrast, the new tools.ts route computes community status only from
config.type, then falls back to the blog branch when it’s missing and assigns owner = username,
which means a community-shaped username can be composed with the community account as owner; since
the SPA gates editor access on instanceConfiguration.owner, this results in an un-administerable
deployment.

apps/self-hosted/hosting/api/src/routes/tenants.ts[170-181]
apps/self-hosted/hosting/api/src/routes/tools.ts[104-129]
apps/self-hosted/src/features/auth/auth-provider.tsx[43-53]
PR-#1309

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
In `POST /v1/tools/compose-config`, community detection relies only on `body.config?.type === 'community'`, allowing callers to submit a community-shaped username (e.g., matching `hive-\d+`) without setting `config.type` (or setting it to `blog`) and thereby bypass the separate-owner rule for communities. This incorrectly routes the request through the personal-blog branch and sets `owner` to the community account itself, which the SPA uses for its ownership gate, producing a permanently locked-out deployment.
## Issue Context
The existing tenant creation/validation flow in `tenants.ts` already addresses this exact bypass by deriving `isCommunity` from `COMMUNITY_NAME.test(username) || body.config?.type === 'community'`, with an explicit comment explaining why trusting only the caller-supplied type is unsafe. `tools.ts` already imports `COMMUNITY_NAME` from `tenant-service.ts` but does not use it for this check; the compose endpoint should enforce the same semantics as tenant creation and include a regression test that covers an omitted discriminator.
## Fix Focus Areas
- apps/self-hosted/hosting/api/src/routes/tools.ts[104-129]
- apps/self-hosted/hosting/api/src/routes/tools.test.ts[90-116]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Community-shaped username bypasses owner rule ✓ Resolved 🐞 Bug ≡ Correctness
Description
In tools.ts, the new compose-config route derives isCommunity only from `body.config?.type ===
'community'`, unlike tenants.ts where community status is also inferred from the username pattern to
prevent callers from omitting type and bypassing the separate-owner requirement. As a result, a
POST like { username: 'hive-125125' } with no config.type takes the personal-blog branch and
returns a composed config with owner === 'hive-125125', creating a community deployment that is
permanently locked out of its own editor because no user holds that account’s keys.
Code

apps/self-hosted/hosting/api/src/routes/tools.ts[R104-107]

+toolsRoutes.post('/compose-config', composeLimit, zValidator('json', composeConfigSchema), async (c) => {
+  const body = c.req.valid('json');
+  const username = body.username.toLowerCase();
+  const isCommunity = body.config?.type === 'community';
Relevance

●●● Strong

Strong precedent: derive community status from hive-* shape, not caller-controlled config.type, to
prevent owner-rule bypass.

PR-#1309

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
In tenants.ts, resolveAndValidateTenant explicitly treats a tenant as a community if either the
username matches COMMUNITY_NAME.test(username) or config.type === 'community', and even
documents that trusting caller-supplied config.type alone is unsafe because it allows omitting the
discriminator and skipping the community ownership check (a bypass previously accepted as a real
bug, e.g., PR #1309). In contrast, the new tools.ts route computes community status only from
config.type, then falls back to the blog branch when it’s missing and assigns owner = username,
which means a community-shaped username can be composed with the community account as owner; since
the SPA gates editor access on instanceConfiguration.owner, this results in an un-administerable
deployment.

apps/self-hosted/hosting/api/src/routes/tenants.ts[170-181]
apps/self-hosted/hosting/api/src/routes/tools.ts[104-129]
apps/self-hosted/src/features/auth/auth-provider.tsx[43-53]
PR-#1309

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
In `POST /v1/tools/compose-config`, community detection relies only on `body.config?.type === 'community'`, allowing callers to submit a community-shaped username (e.g., matching `hive-\d+`) without setting `config.type` (or setting it to `blog`) and thereby bypass the separate-owner rule for communities. This incorrectly routes the request through the personal-blog branch and sets `owner` to the community account itself, which the SPA uses for its ownership gate, producing a permanently locked-out deployment.
## Issue Context
The existing tenant creation/validation flow in `tenants.ts` already addresses this exact bypass by deriving `isCommunity` from `COMMUNITY_NAME.test(username) || body.config?.type === 'community'`, with an explicit comment explaining why trusting only the caller-supplied type is unsafe. `tools.ts` already imports `COMMUNITY_NAME` from `tenant-service.ts` but does not use it for this check; the compose endpoint should enforce the same semantics as tenant creation and include a regression test that covers an omitted discriminator.
## Fix Focus Areas
- apps/self-hosted/hosting/api/src/routes/tools.ts[104-129]
- apps/self-hosted/hosting/api/src/routes/tools.test.ts[90-116]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. Required tag breaks commands ✓ Resolved 🐞 Bug ☼ Reliability
Description
The required ${TAG:?...} interpolation is evaluated for every Compose command, but Quick Start
supplies TAG only to up and immediately shows bare docker compose logs -f. Users following
that primary path cannot run the documented logs, restart, exec, or down commands until they
separately persist or repeat the tag.
Code

apps/self-hosted/docker-compose.yml[20]

+    image: ecency/self-hosted:${TAG:?TAG environment variable is required}
Relevance

●●● Strong

Team previously pushed for explicit TAG-based image pinning; likely will fix docs to avoid missing
TAG.

PR-#702

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The Compose file makes TAG mandatory. The updated Quick Start sets it only for up, then invokes
logs without it; several later operational commands are also bare, while persistent .env setup
is presented only afterward as an optional path.

apps/self-hosted/docker-compose.yml[18-24]
apps/self-hosted/DEPLOYMENT.md[65-74]
apps/self-hosted/DEPLOYMENT.md[217-223]
apps/self-hosted/DEPLOYMENT.md[475-509]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Make the primary Compose workflow persist `TAG` before any commands are run, so all documented follow-up operations can parse the Compose file.
## Issue Context
An inline environment assignment applies to one process only. Prefer creating/updating `.env` during Quick Start and upgrades, or consistently prefix every Compose command with the selected tag.
## Fix Focus Areas
- apps/self-hosted/DEPLOYMENT.md[65-74]
- apps/self-hosted/DEPLOYMENT.md[207-223]
- apps/self-hosted/DEPLOYMENT.md[459-509]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Required tag breaks commands ✓ Resolved 🐞 Bug ☼ Reliability
Description
The required ${TAG:?...} interpolation is evaluated for every Compose command, but Quick Start
supplies TAG only to up and immediately shows bare docker compose logs -f. Users following
that primary path cannot run the documented logs, restart, exec, or down commands until they
separately persist or repeat the tag.
Code

apps/self-hosted/docker-compose.yml[20]

+    image: ecency/self-hosted:${TAG:?TAG environment variable is required}
Relevance

●●● Strong

Team previously pushed for explicit TAG-based image pinning; likely will fix docs to avoid missing
TAG.

PR-#702

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The Compose file makes TAG mandatory. The updated Quick Start sets it only for up, then invokes
logs without it; several later operational commands are also bare, while persistent .env setup
is presented only afterward as an optional path.

apps/self-hosted/docker-compose.yml[18-24]
apps/self-hosted/DEPLOYMENT.md[65-74]
apps/self-hosted/DEPLOYMENT.md[217-223]
apps/self-hosted/DEPLOYMENT.md[475-509]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Make the primary Compose workflow persist `TAG` before any commands are run, so all documented follow-up operations can parse the Compose file.
## Issue Context
An inline environment assignment applies to one process only. Prefer creating/updating `.env` during Quick Start and upgrades, or consistently prefix every Compose command with the selected tag.
## Fix Focus Areas
- apps/self-hosted/DEPLOYMENT.md[65-74]
- apps/self-hosted/DEPLOYMENT.md[207-223]
- apps/self-hosted/DEPLOYMENT.md[459-509]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Required tag breaks commands ✓ Resolved 🐞 Bug ☼ Reliability
Description
The required ${TAG:?...} interpolation is evaluated for every Compose command, but Quick Start
supplies TAG only to up and immediately shows bare docker compose logs -f. Users following
that primary path cannot run the documented logs, restart, exec, or down commands until they
separately persist or repeat the tag.
Code

apps/self-hosted/docker-compose.yml[20]

+    image: ecency/self-hosted:${TAG:?TAG environment variable is required}
Relevance

●●● Strong

Team previously pushed for explicit TAG-based image pinning; likely will fix docs to avoid missing
TAG.

PR-#702

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The Compose file makes TAG mandatory. The updated Quick Start sets it only for up, then invokes
logs without it; several later operational commands are also bare, while persistent .env setup
is presented only afterward as an optional path.

apps/self-hosted/docker-compose.yml[18-24]
apps/self-hosted/DEPLOYMENT.md[65-74]
apps/self-hosted/DEPLOYMENT.md[217-223]
apps/self-hosted/DEPLOYMENT.md[475-509]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Make the primary Compose workflow persist `TAG` before any commands are run, so all documented follow-up operations can parse the Compose file.
## Issue Context
An inline environment assignment applies to one process only. Prefer creating/updating `.env` during Quick Start and upgrades, or consistently prefix every Compose command with the selected tag.
## Fix Focus Areas
- apps/self-hosted/DEPLOYMENT.md[65-74]
- apps/self-hosted/DEPLOYMENT.md[207-223]
- apps/self-hosted/DEPLOYMENT.md[459-509]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View medium (6)
7. Response body cast to any ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The new test casts the JSON response to any, introducing an explicit any type and weakening
type-safety for this new code path. This violates the repository rule to avoid any in new/modified
TypeScript code.
Code

apps/self-hosted/hosting/api/src/routes/tools.test.ts[56]

+  return { res, body: (await res.json()) as any };
Relevance

●● Moderate

Mixed precedent: some PRs kept as any, others removed it to satisfy no-any guidance.

PR-#1459
PR-#1464

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2668119 disallows introducing new explicit any types in modified TypeScript. The
helper compose() returns { res, body: (await res.json()) as any }, which adds an explicit any
cast.

Rule 2668119: Disallow implicit and any types in new TypeScript code
apps/self-hosted/hosting/api/src/routes/tools.test.ts[50-57]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`apps/self-hosted/hosting/api/src/routes/tools.test.ts` casts `await res.json()` to `any`, introducing a new explicit `any`.
## Issue Context
The compliance rule disallows new implicit/explicit `any` in changed TypeScript code. Other tests in this package already model the expected response shape with explicit types instead of `any`.
## Fix Focus Areas
- apps/self-hosted/hosting/api/src/routes/tools.test.ts[50-57]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. TenantService.create mocked in test ✓ Resolved 📘 Rule violation ▣ Testability
Description
The new test overwrites TenantService.create with a vi.fn() mock, which is mocking an internal
application module rather than an external dependency. This violates the unit-test mocking rule and
can make refactors brittle by coupling tests to internal implementation details.
Code

apps/self-hosted/hosting/api/src/routes/tools.test.ts[R46-48]

+TenantService.create = mocks.create.mockRejectedValue(
+  new Error('compose-config must not create a tenant'),
+) as typeof TenantService.create;
Relevance

●● Moderate

Repo rejects mocking internal modules, but no close precedent on overwriting a service method
directly.

PR-#1456

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2668008 restricts Vitest mocking to external package dependencies, with only
limited exceptions for integration boundaries. The test directly overwrites the internal
TenantService.create method using a vi.fn() mock, which is an internal-module mock.

Rule 2668008: Mock only external package dependencies with vi.fn in unit tests
apps/self-hosted/hosting/api/src/routes/tools.test.ts[44-48]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The test reassigns `TenantService.create` to a `vi.fn()` mock. The compliance rule requires mocking only external package dependencies (with limited boundary exceptions), and `TenantService` is an internal module.
## Issue Context
This test already blocks persistence by mocking `../db/client` and `../services/config-service` as throwing proxies. Those boundary mocks should be sufficient to fail loudly if the route ever starts persisting.
## Fix Focus Areas
- apps/self-hosted/hosting/api/src/routes/tools.test.ts[44-49]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


9. TenantService.create mocked in test ✓ Resolved 📘 Rule violation ▣ Testability
Description
The new test overwrites TenantService.create with a vi.fn() mock, which is mocking an internal
application module rather than an external dependency. This violates the unit-test mocking rule and
can make refactors brittle by coupling tests to internal implementation details.
Code

apps/self-hosted/hosting/api/src/routes/tools.test.ts[R46-48]

+TenantService.create = mocks.create.mockRejectedValue(
+  new Error('compose-config must not create a tenant'),
+) as typeof TenantService.create;
Relevance

●● Moderate

Repo rejects mocking internal modules, but no close precedent on overwriting a service method
directly.

PR-#1456

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2668008 restricts Vitest mocking to external package dependencies, with only
limited exceptions for integration boundaries. The test directly overwrites the internal
TenantService.create method using a vi.fn() mock, which is an internal-module mock.

Rule 2668008: Mock only external package dependencies with vi.fn in unit tests
apps/self-hosted/hosting/api/src/routes/tools.test.ts[44-48]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The test reassigns `TenantService.create` to a `vi.fn()` mock. The compliance rule requires mocking only external package dependencies (with limited boundary exceptions), and `TenantService` is an internal module.
## Issue Context
This test already blocks persistence by mocking `../db/client` and `../services/config-service` as throwing proxies. Those boundary mocks should be sufficient to fail loudly if the route ever starts persisting.
## Fix Focus Areas
- apps/self-hosted/hosting/api/src/routes/tools.test.ts[44-49]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


10. Response body cast to any ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The new test casts the JSON response to any, introducing an explicit any type and weakening
type-safety for this new code path. This violates the repository rule to avoid any in new/modified
TypeScript code.
Code

apps/self-hosted/hosting/api/src/routes/tools.test.ts[56]

+  return { res, body: (await res.json()) as any };
Relevance

●● Moderate

Mixed precedent: some PRs kept as any, others removed it to satisfy no-any guidance.

PR-#1459
PR-#1464

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2668119 disallows introducing new explicit any types in modified TypeScript. The
helper compose() returns { res, body: (await res.json()) as any }, which adds an explicit any
cast.

Rule 2668119: Disallow implicit and any types in new TypeScript code
apps/self-hosted/hosting/api/src/routes/tools.test.ts[50-57]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`apps/self-hosted/hosting/api/src/routes/tools.test.ts` casts `await res.json()` to `any`, introducing a new explicit `any`.
## Issue Context
The compliance rule disallows new implicit/explicit `any` in changed TypeScript code. Other tests in this package already model the expected response shape with explicit types instead of `any`.
## Fix Focus Areas
- apps/self-hosted/hosting/api/src/routes/tools.test.ts[50-57]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


11. TenantService.create mocked in test ✓ Resolved 📘 Rule violation ▣ Testability
Description
The new test overwrites TenantService.create with a vi.fn() mock, which is mocking an internal
application module rather than an external dependency. This violates the unit-test mocking rule and
can make refactors brittle by coupling tests to internal implementation details.
Code

apps/self-hosted/hosting/api/src/routes/tools.test.ts[R46-48]

+TenantService.create = mocks.create.mockRejectedValue(
+  new Error('compose-config must not create a tenant'),
+) as typeof TenantService.create;
Relevance

●● Moderate

Repo rejects mocking internal modules, but no close precedent on overwriting a service method
directly.

PR-#1456

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2668008 restricts Vitest mocking to external package dependencies, with only
limited exceptions for integration boundaries. The test directly overwrites the internal
TenantService.create method using a vi.fn() mock, which is an internal-module mock.

Rule 2668008: Mock only external package dependencies with vi.fn in unit tests
apps/self-hosted/hosting/api/src/routes/tools.test.ts[44-48]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The test reassigns `TenantService.create` to a `vi.fn()` mock. The compliance rule requires mocking only external package dependencies (with limited boundary exceptions), and `TenantService` is an internal module.
## Issue Context
This test already blocks persistence by mocking `../db/client` and `../services/config-service` as throwing proxies. Those boundary mocks should be sufficient to fail loudly if the route ever starts persisting.
## Fix Focus Areas
- apps/self-hosted/hosting/api/src/routes/tools.test.ts[44-49]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


12. Response body cast to any ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The new test casts the JSON response to any, introducing an explicit any type and weakening
type-safety for this new code path. This violates the repository rule to avoid any in new/modified
TypeScript code.
Code

apps/self-hosted/hosting/api/src/routes/tools.test.ts[56]

+  return { res, body: (await res.json()) as any };
Relevance

●● Moderate

Mixed precedent: some PRs kept as any, others removed it to satisfy no-any guidance.

PR-#1459
PR-#1464

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2668119 disallows introducing new explicit any types in modified TypeScript. The
helper compose() returns { res, body: (await res.json()) as any }, which adds an explicit any
cast.

Rule 2668119: Disallow implicit and any types in new TypeScript code
apps/self-hosted/hosting/api/src/routes/tools.test.ts[50-57]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`apps/self-hosted/hosting/api/src/routes/tools.test.ts` casts `await res.json()` to `any`, introducing a new explicit `any`.
## Issue Context
The compliance rule disallows new implicit/explicit `any` in changed TypeScript code. Other tests in this package already model the expected response shape with explicit types instead of `any`.
## Fix Focus Areas
- apps/self-hosted/hosting/api/src/routes/tools.test.ts[50-57]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

13. Traefik middleware would deindex all tenants 🐞 Bug ⚙ Maintainability
Description
The unused hosting/traefik/dynamic/middlewares.yml sets X-Robots-Tag: noindex, nofollow on every
response via its security-headers middleware, which the new README warns would deindex every
tenant blog if ever wired in, yet the directory is kept in the repo with no enforcement preventing
it from being applied. The PR only adds a documentation warning rather than removing or fixing the
stale, contradictory config, leaving a live landmine for anyone who tries to enable Traefik later.
Code

apps/self-hosted/hosting/README.md[R107-110]

+- `traefik/` - **Unused.** Traefik is not in the stack and nothing loads
+  this. Note before wiring it in: `dynamic/middlewares.yml` sets
+  `X-Robots-Tag: noindex, nofollow` on every response, which would deindex
+  every tenant blog.
Relevance

●● Moderate

Removing/fixing unused Traefik config is subjective; no clear repo precedent found.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The README's new text explicitly states the traefik/ directory is unused and warns that its
middlewares.yml sets a global noindex header that would deindex every tenant blog if wired in; this
is confirmed by reading apps/self-hosted/hosting/traefik/dynamic/middlewares.yml which contains
X-Robots-Tag: "noindex, nofollow" under customResponseHeaders in a middleware with no scoping to
unclaimed-only hosts.

apps/self-hosted/hosting/traefik/dynamic/middlewares.yml[12-20]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The unused `hosting/traefik/` directory contains `dynamic/middlewares.yml` with a `security-headers` middleware that unconditionally sets `X-Robots-Tag: noindex, nofollow` on every response. The PR documents this as a landmine (deindexes every tenant blog if the middleware is ever wired in) but leaves the misconfigured file in place.
## Issue Context
The file is not loaded by the current stack (docker-compose.yml does not reference Traefik), so today it's inert, but the PR's own README addition treats it as risky legacy content worth calling out explicitly.
## Fix Focus Areas
- apps/self-hosted/hosting/traefik/dynamic/middlewares.yml[12-20]
- apps/self-hosted/hosting/README.md[107-110]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


14. Traefik middleware would deindex all tenants ✗ Dismissed 🐞 Bug ⚙ Maintainability
Description
The unused hosting/traefik/dynamic/middlewares.yml sets X-Robots-Tag: noindex, nofollow on every
response via its security-headers middleware, which the new README warns would deindex every
tenant blog if ever wired in, yet the directory is kept in the repo with no enforcement preventing
it from being applied. The PR only adds a documentation warning rather than removing or fixing the
stale, contradictory config, leaving a live landmine for anyone who tries to enable Traefik later.
Code

apps/self-hosted/hosting/README.md[R107-110]

+- `traefik/` - **Unused.** Traefik is not in the stack and nothing loads
+  this. Note before wiring it in: `dynamic/middlewares.yml` sets
+  `X-Robots-Tag: noindex, nofollow` on every response, which would deindex
+  every tenant blog.
Relevance

●● Moderate

Removing/fixing unused Traefik config is subjective; no clear repo precedent found.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The README's new text explicitly states the traefik/ directory is unused and warns that its
middlewares.yml sets a global noindex header that would deindex every tenant blog if wired in; this
is confirmed by reading apps/self-hosted/hosting/traefik/dynamic/middlewares.yml which contains
X-Robots-Tag: "noindex, nofollow" under customResponseHeaders in a middleware with no scoping to
unclaimed-only hosts.

apps/self-hosted/hosting/traefik/dynamic/middlewares.yml[12-20]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The unused `hosting/traefik/` directory contains `dynamic/middlewares.yml` with a `security-headers` middleware that unconditionally sets `X-Robots-Tag: noindex, nofollow` on every response. The PR documents this as a landmine (deindexes every tenant blog if the middleware is ever wired in) but leaves the misconfigured file in place.
## Issue Context
The file is not loaded by the current stack (docker-compose.yml does not reference Traefik), so today it's inert, but the PR's own README addition treats it as risky legacy content worth calling out explicitly.
## Fix Focus Areas
- apps/self-hosted/hosting/traefik/dynamic/middlewares.yml[12-20]
- apps/self-hosted/hosting/README.md[107-110]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


15. Traefik middleware would deindex all tenants ✗ Dismissed 🐞 Bug ⚙ Maintainability
Description
The unused hosting/traefik/dynamic/middlewares.yml sets X-Robots-Tag: noindex, nofollow on every
response via its security-headers middleware, which the new README warns would deindex every
tenant blog if ever wired in, yet the directory is kept in the repo with no enforcement preventing
it from being applied. The PR only adds a documentation warning rather than removing or fixing the
stale, contradictory config, leaving a live landmine for anyone who tries to enable Traefik later.
Code

apps/self-hosted/hosting/README.md[R107-110]

+- `traefik/` - **Unused.** Traefik is not in the stack and nothing loads
+  this. Note before wiring it in: `dynamic/middlewares.yml` sets
+  `X-Robots-Tag: noindex, nofollow` on every response, which would deindex
+  every tenant blog.
Relevance

●● Moderate

Removing/fixing unused Traefik config is subjective; no clear repo precedent found.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The README's new text explicitly states the traefik/ directory is unused and warns that its
middlewares.yml sets a global noindex header that would deindex every tenant blog if wired in; this
is confirmed by reading apps/self-hosted/hosting/traefik/dynamic/middlewares.yml which contains
X-Robots-Tag: "noindex, nofollow" under customResponseHeaders in a middleware with no scoping to
unclaimed-only hosts.

apps/self-hosted/hosting/traefik/dynamic/middlewares.yml[12-20]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The unused `hosting/traefik/` directory contains `dynamic/middlewares.yml` with a `security-headers` middleware that unconditionally sets `X-Robots-Tag: noindex, nofollow` on every response. The PR documents this as a landmine (deindexes every tenant blog if the middleware is ever wired in) but leaves the misconfigured file in place.
## Issue Context
The file is not loaded by the current stack (docker-compose.yml does not reference Traefik), so today it's inert, but the PR's own README addition treats it as risky legacy content worth calling out explicitly.
## Fix Focus Areas
- apps/self-hosted/hosting/traefik/dynamic/middlewares.yml[12-20]
- apps/self-hosted/hosting/README.md[107-110]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can type 'qodo, fix this' on a finding and the fix lands right on your PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: cc0a156a-0ca1-4645-b8db-d8b6b0ca4353

📥 Commits

Reviewing files that changed from the base of the PR and between 9cfc666 and b2812b1.

📒 Files selected for processing (5)
  • apps/self-hosted/DEPLOYMENT.md
  • apps/self-hosted/docker-compose.yml
  • apps/self-hosted/hosting/README.md
  • apps/self-hosted/hosting/api/src/routes/tools.test.ts
  • apps/self-hosted/hosting/api/src/routes/tools.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/self-hosted/hosting/README.md
  • apps/self-hosted/DEPLOYMENT.md

📝 Walkthrough

Walkthrough

The PR updates self-hosted deployment instructions and Compose configuration, replaces built-in Traefik setup with external proxy guidance, documents hosting files, and adds a rate-limited API route that composes validated self-hosted configuration without creating tenant state.

Changes

Self-hosted deployment and configuration tooling

Layer / File(s) Summary
Tagged image and Compose configuration
apps/self-hosted/DEPLOYMENT.md, apps/self-hosted/docker-compose.yml
Deployment now requires TAG, uses published images, and documents configuration and SEO mount prerequisites.
Deployment operations and hosting layout
apps/self-hosted/DEPLOYMENT.md, apps/self-hosted/docker-compose.yml, apps/self-hosted/hosting/README.md
Documentation covers source builds, external HTTPS proxies, upgrades, troubleshooting, theme modes, managed-platform boundaries, pricing guidance, and current hosting files.
Configuration composition API
apps/self-hosted/hosting/api/src/routes/tools.ts, apps/self-hosted/hosting/api/src/index.ts
The API validates composition input, derives community mode, builds configuration through TenantService, removes managed-only markers, applies rate limits, and mounts /v1/tools.
Composition route validation
apps/self-hosted/hosting/api/src/routes/tools.test.ts
Tests cover ownership inputs, configuration rosters, marker removal, mocked side effects, and input immutability.

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

Mergeability Score: 🟡 Moderate · up to b2812

The PR adds the compose-config endpoint and changes self-hosted deployment behavior and documentation, but unresolved ownership validation, image pinning, host exposure, test isolation, and conflicting deployment guidance could cause invalid tenant ownership or unsafe and non-reproducible deployments. Merge should wait until these bounded issues are fixed or explicitly accepted by the responsible owners.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant toolsRoutes
  participant TenantService
  Client->>toolsRoutes: POST /v1/tools/compose-config
  toolsRoutes->>toolsRoutes: Validate input and enforce rate limits
  toolsRoutes->>TenantService: Build tenant configuration
  TenantService-->>toolsRoutes: Composed configuration
  toolsRoutes-->>Client: Configuration without managed-only markers
Loading

Possibly related PRs

Poem

A rabbit pins the TAG just right,
Then checks the proxy through the night.
Config blooms without tenant state,
Clean markers pass beyond the gate.
Tests guard each hopping route.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main changes: the compose-config endpoint and independent deployment documentation.
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.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/self-hosted-independent-deploy

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


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.

@qodo-code-review

qodo-code-review Bot commented Aug 12, 2026

Copy link
Copy Markdown

PR Summary by Qodo

Add compose-config tool endpoint and fix self-hosted deployment drift

✨ Enhancement 📝 Documentation ⚙️ Configuration changes 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Add anonymous POST /v1/tools/compose-config to compose configs without creating tenants.
• Strip managed-only markers so self-hosted instances don’t misbehave on other domains.
• Fix Docker compose/tag pinning and update deployment docs with accurate ports/HTTPS guidance.
Diagram

graph TD
  A((Self-hoster / Web client)) --> B["Rate limit (general + tools-compose)"] --> C["POST /v1/tools/compose-config"] --> D["TenantService.buildConfig"] --> E["Hive RPC (community title)"]
  D --> F["Strip managed-only markers"] --> G["Return composed config"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Add a dry-run mode to existing tenant-create flow
  • ➕ Avoids introducing a new surface area under /v1/tools
  • ➕ Maximizes reuse of existing validation/payload shape and future schema drift protection
  • ➖ Higher risk of accidentally reintroducing persistence paths (DB rows/files) into a ‘dry-run’
  • ➖ Harder to guarantee “creates nothing” compared to a separate, minimal route + negative tests
2. Move “served-only marker stripping” into TenantService.buildConfig via an option
  • ➕ Centralizes config sanitization logic and makes it reusable by other callers
  • ➕ Reduces risk of future callers forgetting to strip managed-only markers
  • ➖ TenantService.buildConfig is also used for managed flows; adding options increases coupling and testing matrix
  • ➖ Some markers are explicitly “serve-time injected” (e.g., managed) and are clearer to handle at the tools boundary
3. Cache community title lookups for the tools endpoint
  • ➕ Reduces repeated chain RPC costs for popular communities under anonymous usage
  • ➕ Improves latency under bursty compose requests
  • ➖ Adds caching complexity/invalidations for a tool meant to stay pure and minimal
  • ➖ Rate limiting may already be sufficient for expected usage

Recommendation: Current approach (separate /v1/tools/compose-config endpoint) is the safest way to guarantee ‘creates nothing’, especially with the defensive tests that fail if DB/config publishing is accidentally introduced. Consider extracting the marker-stripping helper into a shared utility if another independent-deploy surface is added later, but keeping it out of the managed create path is a reasonable separation.

Files changed (6) +509 / -129

Enhancement (2) +140 / -0
index.tsRegister /v1/tools routes and apply general rate limiting +7/-0

Register /v1/tools routes and apply general rate limiting

• Adds toolsRoutes to the main Hono app and mounts it under /v1/tools. Ensures the tools endpoints are behind the existing general rate limit in addition to any route-specific limits, reflecting the anonymous and potentially RPC-expensive nature of compose-config.

apps/self-hosted/hosting/api/src/index.ts

tools.tsAdd POST /v1/tools/compose-config (no persistence) with marker stripping +133/-0

Add POST /v1/tools/compose-config (no persistence) with marker stripping

• Implements an anonymous compose-config endpoint that validates the same roster-backed payload shape as tenant creation, enforces the community-owner rule, and calls TenantService.buildConfig. Adds a dedicated tools-compose rate limit budget and removes managed-only markers (managed/template/claimPreview and Ecency-owned hivesigner clientId) before returning the composed document.

apps/self-hosted/hosting/api/src/routes/tools.ts

Tests (1) +182 / -0
tools.test.tsAdd negative tests to prove compose-config does not touch DB or publish files +182/-0

Add negative tests to prove compose-config does not touch DB or publish files

• Introduces Vitest coverage asserting successful composition, managed-marker stripping, and community-owner validation. Mocks the DB client and ConfigService as throwing proxies and forces TenantService.create to reject, so any future persistence introduced into this route fails loudly. Adds unit tests for withoutServedOnlyMarkers to ensure it is non-mutating and prunes empty hivesigner blocks.

apps/self-hosted/hosting/api/src/routes/tools.test.ts

Documentation (2) +164 / -83
DEPLOYMENT.mdCorrect self-hosted deployment guidance (tags, ports, HTTPS, upgrades) +150/-81

Correct self-hosted deployment guidance (tags, ports, HTTPS, upgrades)

• Adds an explicit image-tag selection section and makes TAG required throughout examples. Fixes port references to 3000, clarifies compose vs docker run vs source builds, recommends Caddy with a minimal working config, and rewrites upgrade/rollback guidance around pinned tags. Cleans up SEO generator instructions and replaces stale pricing with a link to the live hosting page.

apps/self-hosted/DEPLOYMENT.md

README.mdFix hosting directory inventory and warn about unused Traefik noindex header +14/-2

Fix hosting directory inventory and warn about unused Traefik noindex header

• Corrects the file/directory list (including nginx-multi-tenant.conf being a file) and documents additional directories (db, origin). Notes that traefik/ is unused and contains a global X-Robots-Tag: noindex, nofollow middleware that would deindex tenants if enabled. Clarifies what CI actually deploys to hosts.

apps/self-hosted/hosting/README.md

Other (1) +23 / -46
docker-compose.ymlRequire pinned image tag and remove implicit local builds +23/-46

Require pinned image tag and remove implicit local builds

• Switches the blog service to use ecency/self-hosted:${TAG} with a required TAG guard, removing the build stanza that could override pinning. Improves comments around config/SEO mounts to prevent Docker creating directories for missing files and causing demo-config fallback. Replaces the old Traefik-oriented commented section with guidance to use a reverse proxy per DEPLOYMENT.md.

apps/self-hosted/docker-compose.yml

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ea574a23d0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/self-hosted/DEPLOYMENT.md Outdated
# Build and start
docker compose up -d
# Pull and start
TAG=sha-abc1234 docker compose up -d

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Persist TAG for subsequent Compose commands

With the Quick Start exactly as written, TAG=... exists only for the docker compose up process; the immediately following docker compose logs -f loads docker-compose.yml again without TAG and fails on the required ${TAG:?...} interpolation. Export TAG, create .env before the first command, or prefix every Compose invocation so the documented sequence works.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Valid, fixed in 9cfc666. Quick Start now writes .env before the first compose command, so docker compose logs -f (and restart, down, exec) keep working; the Options and Updating sections were reworked the same way, and no bare TAG=... docker compose invocation is left in the file.

Verified by walking the corrected Quick Start exactly as written against the real image: write .env, docker compose up -d, docker compose logs (the command that failed before), site answers 200 and serves the mounted config.json, docker compose down clean.

toolsRoutes.post('/compose-config', composeLimit, zValidator('json', composeConfigSchema), async (c) => {
const body = c.req.valid('json');
const username = body.username.toLowerCase();
const isCommunity = body.config?.type === 'community';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Derive community mode from hive- usernames*

When a compose request uses a hive-NNNN username but omits config.type (or explicitly sends blog), this branch treats it as a personal blog, ignores the supplied owner, and returns a config owned by the keyless community account, permanently hiding the editor from the real administrator. The managed creation path already derives community status from COMMUNITY_NAME.test(username) || config.type === 'community'; this endpoint should apply the same rule before composing the independent config.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Valid, fixed in 9cfc666. The endpoint now derives it the same way the create path does: COMMUNITY_NAME.test(username) || body.config?.type === 'community', so a hive-NNNN name is a community whatever the body claims, and the separate-owner rule applies to it.

Fixing the derivation surfaced a second half of the same bug: with the type only inferred, buildConfig still composed a BLOG-typed document for that name, so the deployment would have run in blog mode reading a keyless account and shown an empty feed forever. The inferred case now passes type: 'community' and the resolved communityId into the builder, so the ownership rule and the composed mode agree. Regression test covers both the omitted-type and the explicit-blog request.

@qodo-code-review

qodo-code-review Bot commented Aug 12, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Community-shaped username bypasses owner rule ✓ Resolved 🐞 Bug ≡ Correctness
Description
In tools.ts, the new compose-config route derives isCommunity only from `body.config?.type ===
'community'`, unlike tenants.ts where community status is also inferred from the username pattern to
prevent callers from omitting type and bypassing the separate-owner requirement. As a result, a
POST like { username: 'hive-125125' } with no config.type takes the personal-blog branch and
returns a composed config with owner === 'hive-125125', creating a community deployment that is
permanently locked out of its own editor because no user holds that account’s keys.
Code

apps/self-hosted/hosting/api/src/routes/tools.ts[R104-107]

+toolsRoutes.post('/compose-config', composeLimit, zValidator('json', composeConfigSchema), async (c) => {
+  const body = c.req.valid('json');
+  const username = body.username.toLowerCase();
+  const isCommunity = body.config?.type === 'community';
Relevance

●●● Strong

Strong precedent: derive community status from hive-* shape, not caller-controlled config.type, to
prevent owner-rule bypass.

PR-#1309

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
In tenants.ts, resolveAndValidateTenant explicitly treats a tenant as a community if either the
username matches COMMUNITY_NAME.test(username) or config.type === 'community', and even
documents that trusting caller-supplied config.type alone is unsafe because it allows omitting the
discriminator and skipping the community ownership check (a bypass previously accepted as a real
bug, e.g., PR #1309). In contrast, the new tools.ts route computes community status only from
config.type, then falls back to the blog branch when it’s missing and assigns owner = username,
which means a community-shaped username can be composed with the community account as owner; since
the SPA gates editor access on instanceConfiguration.owner, this results in an un-administerable
deployment.

apps/self-hosted/hosting/api/src/routes/tenants.ts[170-181]
apps/self-hosted/hosting/api/src/routes/tools.ts[104-129]
apps/self-hosted/src/features/auth/auth-provider.tsx[43-53]
PR-#1309

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
In `POST /v1/tools/compose-config`, community detection relies only on `body.config?.type === 'community'`, allowing callers to submit a community-shaped username (e.g., matching `hive-\d+`) without setting `config.type` (or setting it to `blog`) and thereby bypass the separate-owner rule for communities. This incorrectly routes the request through the personal-blog branch and sets `owner` to the community account itself, which the SPA uses for its ownership gate, producing a permanently locked-out deployment.

## Issue Context
The existing tenant creation/validation flow in `tenants.ts` already addresses this exact bypass by deriving `isCommunity` from `COMMUNITY_NAME.test(username) || body.config?.type === 'community'`, with an explicit comment explaining why trusting only the caller-supplied type is unsafe. `tools.ts` already imports `COMMUNITY_NAME` from `tenant-service.ts` but does not use it for this check; the compose endpoint should enforce the same semantics as tenant creation and include a regression test that covers an omitted discriminator.

## Fix Focus Areas
- apps/self-hosted/hosting/api/src/routes/tools.ts[104-129]
- apps/self-hosted/hosting/api/src/routes/tools.test.ts[90-116]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Required tag breaks commands ✓ Resolved 🐞 Bug ☼ Reliability
Description
The required ${TAG:?...} interpolation is evaluated for every Compose command, but Quick Start
supplies TAG only to up and immediately shows bare docker compose logs -f. Users following
that primary path cannot run the documented logs, restart, exec, or down commands until they
separately persist or repeat the tag.
Code

apps/self-hosted/docker-compose.yml[20]

+    image: ecency/self-hosted:${TAG:?TAG environment variable is required}
Relevance

●●● Strong

Team previously pushed for explicit TAG-based image pinning; likely will fix docs to avoid missing
TAG.

PR-#702

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The Compose file makes TAG mandatory. The updated Quick Start sets it only for up, then invokes
logs without it; several later operational commands are also bare, while persistent .env setup
is presented only afterward as an optional path.

apps/self-hosted/docker-compose.yml[18-24]
apps/self-hosted/DEPLOYMENT.md[65-74]
apps/self-hosted/DEPLOYMENT.md[217-223]
apps/self-hosted/DEPLOYMENT.md[475-509]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Make the primary Compose workflow persist `TAG` before any commands are run, so all documented follow-up operations can parse the Compose file.

## Issue Context
An inline environment assignment applies to one process only. Prefer creating/updating `.env` during Quick Start and upgrades, or consistently prefix every Compose command with the selected tag.

## Fix Focus Areas
- apps/self-hosted/DEPLOYMENT.md[65-74]
- apps/self-hosted/DEPLOYMENT.md[207-223]
- apps/self-hosted/DEPLOYMENT.md[459-509]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. TenantService.create mocked in test ✓ Resolved 📘 Rule violation ▣ Testability
Description
The new test overwrites TenantService.create with a vi.fn() mock, which is mocking an internal
application module rather than an external dependency. This violates the unit-test mocking rule and
can make refactors brittle by coupling tests to internal implementation details.
Code

apps/self-hosted/hosting/api/src/routes/tools.test.ts[R46-48]

+TenantService.create = mocks.create.mockRejectedValue(
+  new Error('compose-config must not create a tenant'),
+) as typeof TenantService.create;
Relevance

●● Moderate

Repo rejects mocking internal modules, but no close precedent on overwriting a service method
directly.

PR-#1456

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2668008 restricts Vitest mocking to external package dependencies, with only
limited exceptions for integration boundaries. The test directly overwrites the internal
TenantService.create method using a vi.fn() mock, which is an internal-module mock.

Rule 2668008: Mock only external package dependencies with vi.fn in unit tests
apps/self-hosted/hosting/api/src/routes/tools.test.ts[44-48]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The test reassigns `TenantService.create` to a `vi.fn()` mock. The compliance rule requires mocking only external package dependencies (with limited boundary exceptions), and `TenantService` is an internal module.

## Issue Context
This test already blocks persistence by mocking `../db/client` and `../services/config-service` as throwing proxies. Those boundary mocks should be sufficient to fail loudly if the route ever starts persisting.

## Fix Focus Areas
- apps/self-hosted/hosting/api/src/routes/tools.test.ts[44-49]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Response body cast to any ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The new test casts the JSON response to any, introducing an explicit any type and weakening
type-safety for this new code path. This violates the repository rule to avoid any in new/modified
TypeScript code.
Code

apps/self-hosted/hosting/api/src/routes/tools.test.ts[56]

+  return { res, body: (await res.json()) as any };
Relevance

●● Moderate

Mixed precedent: some PRs kept as any, others removed it to satisfy no-any guidance.

PR-#1459
PR-#1464

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2668119 disallows introducing new explicit any types in modified TypeScript. The
helper compose() returns { res, body: (await res.json()) as any }, which adds an explicit any
cast.

Rule 2668119: Disallow implicit and any types in new TypeScript code
apps/self-hosted/hosting/api/src/routes/tools.test.ts[50-57]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`apps/self-hosted/hosting/api/src/routes/tools.test.ts` casts `await res.json()` to `any`, introducing a new explicit `any`.

## Issue Context
The compliance rule disallows new implicit/explicit `any` in changed TypeScript code. Other tests in this package already model the expected response shape with explicit types instead of `any`.

## Fix Focus Areas
- apps/self-hosted/hosting/api/src/routes/tools.test.ts[50-57]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

5. Traefik middleware would deindex all tenants ✗ Dismissed 🐞 Bug ⚙ Maintainability
Description
The unused hosting/traefik/dynamic/middlewares.yml sets X-Robots-Tag: noindex, nofollow on every
response via its security-headers middleware, which the new README warns would deindex every
tenant blog if ever wired in, yet the directory is kept in the repo with no enforcement preventing
it from being applied. The PR only adds a documentation warning rather than removing or fixing the
stale, contradictory config, leaving a live landmine for anyone who tries to enable Traefik later.
Code

apps/self-hosted/hosting/README.md[R107-110]

+- `traefik/` - **Unused.** Traefik is not in the stack and nothing loads
+  this. Note before wiring it in: `dynamic/middlewares.yml` sets
+  `X-Robots-Tag: noindex, nofollow` on every response, which would deindex
+  every tenant blog.
Relevance

●● Moderate

Removing/fixing unused Traefik config is subjective; no clear repo precedent found.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The README's new text explicitly states the traefik/ directory is unused and warns that its
middlewares.yml sets a global noindex header that would deindex every tenant blog if wired in; this
is confirmed by reading apps/self-hosted/hosting/traefik/dynamic/middlewares.yml which contains
X-Robots-Tag: "noindex, nofollow" under customResponseHeaders in a middleware with no scoping to
unclaimed-only hosts.

apps/self-hosted/hosting/traefik/dynamic/middlewares.yml[12-20]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The unused `hosting/traefik/` directory contains `dynamic/middlewares.yml` with a `security-headers` middleware that unconditionally sets `X-Robots-Tag: noindex, nofollow` on every response. The PR documents this as a landmine (deindexes every tenant blog if the middleware is ever wired in) but leaves the misconfigured file in place.

## Issue Context
The file is not loaded by the current stack (docker-compose.yml does not reference Traefik), so today it's inert, but the PR's own README addition treats it as risky legacy content worth calling out explicitly.

## Fix Focus Areas
- apps/self-hosted/hosting/traefik/dynamic/middlewares.yml[12-20]
- apps/self-hosted/hosting/README.md[107-110]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context
✅ Compliance rules (platform): 75 rules
✅ Skills: 6 invoked
  add-feature
  add-query
  add-sdk-mutation
  add-test
  code-review
  debug
Review mode: 🧠 Deep: This adds a new anonymous API path with validation, rate limiting, config sanitization, service integration, and deployment behavior changes across multiple files, creating several independent opportunities for subtle defects.

Grey Divider

Tip of the day
💡 Did you know, you can type 'qodo, fix this' on a finding and the fix lands right on your PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread apps/self-hosted/hosting/api/src/routes/tools.test.ts Outdated
Comment thread apps/self-hosted/hosting/api/src/routes/tools.test.ts Outdated
Comment thread apps/self-hosted/docker-compose.yml
Comment thread apps/self-hosted/hosting/api/src/routes/tools.ts Outdated
Comment thread apps/self-hosted/hosting/README.md

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🧹 Nitpick comments (2)
apps/self-hosted/hosting/api/src/index.ts (1)

80-82: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Note the fail-open behavior for this anonymous endpoint.

rateLimit skips limiting when no trusted proxy IP is present, and it also fails open when Redis is unavailable (see apps/self-hosted/hosting/api/src/middleware/rate-limit.ts lines 49-82). In both cases /v1/tools/* accepts unlimited anonymous requests, and each request can spend a Hive RPC lookup. Consider an alert on Redis unavailability, or a small in-process fallback limit for this route group.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/self-hosted/hosting/api/src/index.ts` around lines 80 - 82, Update the
/v1/tools/* protection around generalLimit to retain an effective request cap
when the trusted proxy IP is missing or Redis is unavailable, using a small
in-process fallback limiter for this anonymous route group or the established
alerting mechanism for Redis failures. Preserve the existing general budget and
route-specific limiting behavior.
apps/self-hosted/hosting/api/src/routes/tools.ts (1)

141-141: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider handling a failed community title lookup.

TenantService.buildConfig performs a chain lookup for a community without a title. This route has no error handling, so a failing or slow RPC turns into a 500 for an anonymous caller. A fallback to the derived default title keeps the tool usable when the RPC is degraded.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/self-hosted/hosting/api/src/routes/tools.ts` at line 141, Update the
route flow around TenantService.buildConfig to handle a failed or slow community
title lookup by falling back to the derived default title, especially for
anonymous callers. Preserve the existing configured-title behavior when the
lookup succeeds, and ensure RPC failures do not become an unhandled 500.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/self-hosted/DEPLOYMENT.md`:
- Around line 228-231: Update the operational examples in the deployment
documentation so proxy configurations reference the configured PORT instead of
hardcoded 3000, including the Caddy and Nginx examples. Replace the
troubleshooting diagnostic command with docker compose port blog 80, and state
that proxy examples must use the configured PORT.
- Line 286: Update the fenced code block in DEPLOYMENT.md to include a language
identifier after its opening fence, using caddyfile or text to satisfy
Markdownlint MD040.
- Around line 74-75: Mark sha-abc1234 as a placeholder in every runnable
example: update apps/self-hosted/DEPLOYMENT.md lines 74-75, 220-223, and
236-240, plus apps/self-hosted/docker-compose.yml line 7, to instruct users to
replace it with an existing Docker Hub image tag before running the commands.
- Around line 209-212: Update the release-tag guidance in the deployment
documentation to make the choice conditional: use the immutable vX.Y.Z tag when
Docker Hub lists it for both images; otherwise, pin an available sha-<7> tag.
Treat Docker Hub image tags as the authoritative source and remove any
implication that the managed platform health endpoint determines tag
availability.
- Around line 200-205: Update the deployment instructions around the published
tags table to require pinning ecency/hosting-api to the selected immutable tag,
verifying that the tag exists in both repositories before running the SEO
generator, and choosing another valid tag if it does not; explicitly prevent the
command from falling back to latest.

In `@apps/self-hosted/docker-compose.yml`:
- Around line 23-24: Update the ports mapping in the Docker Compose service to
bind the published port to loopback rather than all host interfaces, preserving
the existing PORT default and container port 80. If intentional external
exposure must remain supported, document the explicit host-bind or firewall
override alongside this configuration.

In `@apps/self-hosted/hosting/api/src/routes/tools.test.ts`:
- Around line 36-48: Remove the unused mock declarations around the
`@ecency/sdk/hive` mock and `TenantService.create` override in the test setup,
while preserving the Hive mock needed by `TenantService` for community title
lookup.

In `@apps/self-hosted/hosting/api/src/routes/tools.ts`:
- Around line 118-139: Update the community-owner validation in the community
branch so it also rejects any owner whose normalized name matches the
COMMUNITY_NAME pattern, not only an owner equal to communityId. Preserve the
existing requirement that owner is present and distinct from the communityId,
and continue assigning the normalized valid owner afterward.

In `@apps/self-hosted/hosting/README.md`:
- Around line 107-110: Resolve the contradiction between the Traefik “Unused”
statement and the architecture diagram and Components section by either labeling
those Traefik references as historical/conceptual or revising them to describe
the deployed Nginx layout. Keep all README sections consistent about the active
load balancer.

---

Nitpick comments:
In `@apps/self-hosted/hosting/api/src/index.ts`:
- Around line 80-82: Update the /v1/tools/* protection around generalLimit to
retain an effective request cap when the trusted proxy IP is missing or Redis is
unavailable, using a small in-process fallback limiter for this anonymous route
group or the established alerting mechanism for Redis failures. Preserve the
existing general budget and route-specific limiting behavior.

In `@apps/self-hosted/hosting/api/src/routes/tools.ts`:
- Line 141: Update the route flow around TenantService.buildConfig to handle a
failed or slow community title lookup by falling back to the derived default
title, especially for anonymous callers. Preserve the existing configured-title
behavior when the lookup succeeds, and ensure RPC failures do not become an
unhandled 500.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7ff95a75-8254-4f82-b535-c689b5bab405

📥 Commits

Reviewing files that changed from the base of the PR and between 33e2760 and 9cfc666.

📒 Files selected for processing (6)
  • apps/self-hosted/DEPLOYMENT.md
  • apps/self-hosted/docker-compose.yml
  • apps/self-hosted/hosting/README.md
  • apps/self-hosted/hosting/api/src/index.ts
  • apps/self-hosted/hosting/api/src/routes/tools.test.ts
  • apps/self-hosted/hosting/api/src/routes/tools.ts

Comment thread apps/self-hosted/DEPLOYMENT.md Outdated
Comment thread apps/self-hosted/DEPLOYMENT.md
Comment thread apps/self-hosted/DEPLOYMENT.md Outdated
Comment thread apps/self-hosted/DEPLOYMENT.md
Comment thread apps/self-hosted/DEPLOYMENT.md Outdated
Comment thread apps/self-hosted/docker-compose.yml Outdated
Comment thread apps/self-hosted/hosting/api/src/routes/tools.test.ts Outdated
Comment thread apps/self-hosted/hosting/api/src/routes/tools.ts
Comment thread apps/self-hosted/hosting/README.md
@feruzm

feruzm commented Aug 13, 2026

Copy link
Copy Markdown
Member Author

Valid, fixed at b2812b1. tools.ts now carries the same equality check as the create path:

if (username !== communityId) {
  return c.json({ error: 'Subdomain must equal the community id' }, 400);
}

You are right that this contradicted the PR's own claim. The composed document pins both names, so accepting username: hive-125125 with communityId: hive-99999 would have produced a site called one thing that serves the other's feed, and an owner reading their own config.json would find two identities in it with no way to tell which the app honours. Regression test asserts the 400 with that exact pair.

That completes the round: the earlier Qodo findings on this PR now show resolved, and its Traefik note was dismissed after I explained why the documentation fix is the right scope here rather than deleting a directory I did not write. 465 API tests and npm run build (tsc) pass.

@feruzm
feruzm merged commit 568ce19 into develop Aug 13, 2026
12 checks passed
@feruzm
feruzm deleted the feature/self-hosted-independent-deploy branch August 13, 2026 07:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant