Independent deployment: compose-config endpoint and deployment docs - #1465
Conversation
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.
Code Review by Qodo
1.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (5)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughThe 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. ChangesSelf-hosted deployment and configuration tooling
Estimated code review effort: 4 (Complex) | ~45 minutes Mergeability Score: 🟡 Moderate · up to 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
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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
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. Comment |
PR Summary by QodoAdd compose-config tool endpoint and fix self-hosted deployment drift
AI Description
Diagram
High-Level Assessment
Files changed (6)
|
There was a problem hiding this comment.
💡 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".
| # Build and start | ||
| docker compose up -d | ||
| # Pull and start | ||
| TAG=sha-abc1234 docker compose up -d |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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.
Code Review by Qodo
1.
|
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (2)
apps/self-hosted/hosting/api/src/index.ts (1)
80-82: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low valueNote the fail-open behavior for this anonymous endpoint.
rateLimitskips limiting when no trusted proxy IP is present, and it also fails open when Redis is unavailable (seeapps/self-hosted/hosting/api/src/middleware/rate-limit.tslines 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 winConsider handling a failed community title lookup.
TenantService.buildConfigperforms 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
📒 Files selected for processing (6)
apps/self-hosted/DEPLOYMENT.mdapps/self-hosted/docker-compose.ymlapps/self-hosted/hosting/README.mdapps/self-hosted/hosting/api/src/index.tsapps/self-hosted/hosting/api/src/routes/tools.test.tsapps/self-hosted/hosting/api/src/routes/tools.ts
|
Valid, fixed at b2812b1. 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 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 |
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.buildConfigon 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 ascreateTenantSchemaand 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:
managedis 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.templatereplaces the whole site with the claim landing page.claimPreviewmarks the read-only preview of an unclaimed subdomain.hivesigner.clientIdmay 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.ymldocumentedTAGwhile hardcodingimage: ecency/self-hosted:latest, so every deployment following the guide silently ranlatest(last published 2026-08-12 08:21, before the release-tag change) rather than the pinned build. It now requiresTAGthe way the managed stack does, and no longer carries abuild: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.Ztag exists yet, sosha-<7>is what to pin today);journalandreaderadded 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:3100ports 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.mdlisted anginx/directory that does not exist and omitted four that do. It now also notes that the unusedtraefik/config setsX-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 configvalidates with a TAG and fails loudly without one.Summary by CodeRabbit
New Features
.env-based Compose configuration.Documentation
Bug Fixes