Skip to content

Hosting signup: free self-host branch with a downloadable deployment bundle - #1466

Merged
feruzm merged 3 commits into
developfrom
feature/self-hosted-selfhost-bundle
Aug 13, 2026
Merged

Hosting signup: free self-host branch with a downloadable deployment bundle#1466
feruzm merged 3 commits into
developfrom
feature/self-hosted-selfhost-bundle

Conversation

@feruzm

@feruzm feruzm commented Aug 12, 2026

Copy link
Copy Markdown
Member

Second half of #1453. Depends on #1465 (the /v1/tools/compose-config endpoint) being merged AND deployed: until then this branch's request 404s and the user sees "Not found".

The fork

After customizing, the signup asks where the blog will live. Managed is the default and its flow is untouched, so the paid path costs no extra click and every existing spec passes unchanged. Choosing to self-host swaps Continue for a download button.

The self-host branch calls composeConfig and nothing else. It never calls createTenant, never fetches payment instructions, and deliberately leaves createdForRef alone, so a user who tries self-hosting and then switches back to managed still gets a proper reservation. Tests assert those absences directly, since they are the whole point of the branch being free.

The bundle

A zip containing config.json (the composed document, already stripped of managed-only markers server-side), docker-compose.yml, .env, Caddyfile and a README covering run, HTTPS, config edits, upgrade and rollback, the SEO generator and Hivesigner setup.

The image tag is read from the platform's own /health rather than composed in the client, so a bundle pins a build that demonstrably exists; both images come from one commit in one CI run, so the tag resolves for the blog and the hosting API alike. If /health cannot be reached the download is refused with a message rather than shipping a bundle pinned to nothing.

There is no zip dependency anywhere in this workspace, and adding one costs an allowBuilds review plus the minimum-release-age wait, so the archive is written by hand in the STORE format (no compression to get wrong on small text files). Rather than trust that arithmetic, it is verified three ways:

  • the spec reads the archive back through an independent parser and checks every entry's CRC, including a multi-byte-character case where byte length differs from string length;
  • a generated bundle passes Python's zipfile.testzip();
  • that bundle was extracted and actually run: docker compose up reported healthy, the site answered 200, and it served the bundle's own mounted config.json.

A signup-level test decodes the real archive out of the Blob handed to the browser and asserts the pinned tag matches the running build.

Verified: 2657 web tests (58 in hosting-signup, 4 new plus 10 for the bundle), typecheck, both icon audits and lint clean.

…bundle

After customizing, the signup asks where the blog will live. Managed stays
the default and its flow is untouched; choosing to self-host composes the
same config document through the hosting API, wraps it with the files a
deployment needs and hands over a zip. That branch never calls createTenant,
so no name is reserved and no payment clock starts.

The archive is written in the store format by hand: there is no zip
dependency in this workspace, and the files are small text.
@feruzm

feruzm commented Aug 12, 2026

Copy link
Copy Markdown
Member Author

Merge order: this needs #1465 merged and deployed first. The endpoint it calls only reaches production on a merge to develop, so merging this one first would ship a download button that 404s.

@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 (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (1)

Grey Divider


Action required

1. useCallback missing dependencies 📜 Skill insight ≡ Correctness
Description
useDownloadSelfHostBundle() defines download with useCallback(..., []) while closing over
multiple values, risking stale references and violating the hook dependency requirement. This can
cause hard-to-debug behavior changes when surrounding code evolves.
Code

apps/web/src/features/hosting-signup/use-download-self-host-bundle.ts[R63-66]

+    } finally {
+      setBusy(false);
+    }
+  }, []);
Relevance

●●● Strong

Exhaustive-deps/stale-closure fixes are commonly accepted; empty deps on closure values is a typical
change request.

PR-#737
PR-#666

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Compliance ID 2668377 requires hook dependency arrays to be complete. The new callback ends with an
empty dependency array ([]) despite referencing multiple values from the module/hook scope.

apps/web/src/features/hosting-signup/use-download-self-host-bundle.ts[63-66]
Skill: code-review: Skill: code-review: Skill: code-review: Skill: code-review: Skill: code-review: Skill: code-review: Skill: code-review: Skill: code-review

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

## Issue description
A `useCallback` is declared with an empty dependency array while referencing values from the enclosing scope, violating the hook dependency completeness rule and risking stale closures.
## Issue Context
In `useDownloadSelfHostBundle`, the `download` callback uses `hostingApi`, `buildSelfHostZip`, `i18next`, `setBusy`, and `setError` but ends with `}, []);`.
## Fix Focus Areas
- apps/web/src/features/hosting-signup/use-download-self-host-bundle.ts[22-66]

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


2. useCallback missing dependencies ✗ Dismissed 📜 Skill insight ≡ Correctness
Description
useDownloadSelfHostBundle() defines download with useCallback(..., []) while closing over
multiple values, risking stale references and violating the hook dependency requirement. This can
cause hard-to-debug behavior changes when surrounding code evolves.
Code

apps/web/src/features/hosting-signup/use-download-self-host-bundle.ts[R63-66]

+    } finally {
+      setBusy(false);
+    }
+  }, []);
Relevance

●●● Strong

Exhaustive-deps/stale-closure fixes are commonly accepted; empty deps on closure values is a typical
change request.

PR-#737
PR-#666

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Compliance ID 2668377 requires hook dependency arrays to be complete. The new callback ends with an
empty dependency array ([]) despite referencing multiple values from the module/hook scope.

apps/web/src/features/hosting-signup/use-download-self-host-bundle.ts[63-66]
Skill: code-review: Skill: code-review: Skill: code-review: Skill: code-review

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

## Issue description
A `useCallback` is declared with an empty dependency array while referencing values from the enclosing scope, violating the hook dependency completeness rule and risking stale closures.
## Issue Context
In `useDownloadSelfHostBundle`, the `download` callback uses `hostingApi`, `buildSelfHostZip`, `i18next`, `setBusy`, and `setError` but ends with `}, []);`.
## Fix Focus Areas
- apps/web/src/features/hosting-signup/use-download-self-host-bundle.ts[22-66]

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


3. useCallback missing dependencies ✗ Dismissed 📜 Skill insight ≡ Correctness
Description
useDownloadSelfHostBundle() defines download with useCallback(..., []) while closing over
multiple values, risking stale references and violating the hook dependency requirement. This can
cause hard-to-debug behavior changes when surrounding code evolves.
Code

apps/web/src/features/hosting-signup/use-download-self-host-bundle.ts[R63-66]

+    } finally {
+      setBusy(false);
+    }
+  }, []);
Relevance

●●● Strong

Exhaustive-deps/stale-closure fixes are commonly accepted; empty deps on closure values is a typical
change request.

PR-#737
PR-#666

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Compliance ID 2668377 requires hook dependency arrays to be complete. The new callback ends with an
empty dependency array ([]) despite referencing multiple values from the module/hook scope.

apps/web/src/features/hosting-signup/use-download-self-host-bundle.ts[63-66]
Skill: code-review: Skill: code-review

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

## Issue description
A `useCallback` is declared with an empty dependency array while referencing values from the enclosing scope, violating the hook dependency completeness rule and risking stale closures.
## Issue Context
In `useDownloadSelfHostBundle`, the `download` callback uses `hostingApi`, `buildSelfHostZip`, `i18next`, `setBusy`, and `setError` but ends with `}, []);`.
## Fix Focus Areas
- apps/web/src/features/hosting-signup/use-download-self-host-bundle.ts[22-66]

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



Remediation recommended

4. e: any in onChange ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
DestinationPicker introduces an explicit any type in the FormControl onChange handler,
weakening type safety and violating the no-any rule for new TypeScript code. This can hide real
event typing issues and runtime errors.
Code

apps/web/src/features/hosting-signup/destination-picker.tsx[R83-86]

+            disabled={disabled}
+            onChange={(e: any) => onDomainChange(e.target.value)}
+            placeholder="blog.example.com"
+          />
Relevance

●●● Strong

Team often removes newly introduced (e: any) in FormControl onChange; multiple recent acceptances
despite one rejection.

PR-#1457
PR-#1438
PR-#1456

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Compliance ID 2668119 disallows introducing any in new/modified TypeScript. The new handler
explicitly types the event parameter as any.

Rule 2668119: Disallow implicit and any types in new TypeScript code
apps/web/src/features/hosting-signup/destination-picker.tsx[83-86]

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

## Issue description
New TypeScript code introduces an explicit `any` type in an event handler.
## Issue Context
`DestinationPicker` uses `onChange={(e: any) => ...}` for a `FormControl` change handler.
## Fix Focus Areas
- apps/web/src/features/hosting-signup/destination-picker.tsx[83-86]

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


5. Unsanitized domain in bundle ✓ Resolved 🐞 Bug ≡ Correctness
Description
The self-host domain is accepted as free-form text and then embedded verbatim into generated files
(notably Caddyfile and .env) with only .trim(), so inputs containing
schemes/paths/whitespace/newlines can produce invalid configs or unintended extra Caddy directives
in the downloaded bundle.
Code

apps/web/src/features/hosting-signup/self-host-bundle.ts[R86-88]

+${domain} {
+    reverse_proxy 127.0.0.1:3000
+}
Relevance

●●● Strong

They regularly sanitize user-controlled strings to prevent newline/log/config injection; likely to
add domain validation/sanitization.

PR-#947
PR-#916

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The domain comes from an unrestricted text input and is passed to the bundle builder; the builder
only trims it and interpolates it into the generated .env and Caddyfile contents.

apps/web/src/features/hosting-signup/destination-picker.tsx[74-86]
apps/web/src/features/hosting-signup/hosting-signup.tsx[347-362]
apps/web/src/features/hosting-signup/self-host-bundle.ts[205-214]
apps/web/src/features/hosting-signup/self-host-bundle.ts[68-78]
apps/web/src/features/hosting-signup/self-host-bundle.ts[81-89]

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 optional self-host domain is currently only trimmed and then interpolated into multiple generated files (`Caddyfile`, `.env`, README). This can yield broken deployment files when users paste common formats (e.g., `https://example.com`) or include whitespace/control characters, and newline/control characters can change the structure of the generated Caddyfile.
### Issue Context
- Domain is collected via a plain text input and passed through to the bundle generator.
- The bundle generator uses the domain string directly in `Caddyfile` (`${domain} { ... }`) and `.env` (`DOMAIN=${domain}`).
### Fix Focus Areas
- apps/web/src/features/hosting-signup/destination-picker.tsx[74-86]
- apps/web/src/features/hosting-signup/hosting-signup.tsx[347-362]
- apps/web/src/features/hosting-signup/self-host-bundle.ts[205-214]
- apps/web/src/features/hosting-signup/self-host-bundle.ts[81-89]
### Suggested remediation
1. Add a small sanitizer/validator for the domain:
- Reject any value containing whitespace/control characters (including `\r`/`\n`).
- Reject values containing a scheme (`://`), path (`/`), query (`?`), or fragment (`#`).
- Optionally validate against a hostname pattern (DNS hostname) and allow `:port` only if you intend to support it.
2. Enforce this in `buildSelfHostBundle()` (not only in the UI) so other callers can’t generate malformed bundles.
3. If invalid, either:
- treat it as “no domain provided” and fall back to `blog.example.com`, or
- surface a user-visible error and prevent download.

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


6. Unsanitized domain in bundle ✓ Resolved 🐞 Bug ≡ Correctness
Description
The self-host domain is accepted as free-form text and then embedded verbatim into generated files
(notably Caddyfile and .env) with only .trim(), so inputs containing
schemes/paths/whitespace/newlines can produce invalid configs or unintended extra Caddy directives
in the downloaded bundle.
Code

apps/web/src/features/hosting-signup/self-host-bundle.ts[R86-88]

+${domain} {
+    reverse_proxy 127.0.0.1:3000
+}
Relevance

●●● Strong

They regularly sanitize user-controlled strings to prevent newline/log/config injection; likely to
add domain validation/sanitization.

PR-#947
PR-#916

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The domain comes from an unrestricted text input and is passed to the bundle builder; the builder
only trims it and interpolates it into the generated .env and Caddyfile contents.

apps/web/src/features/hosting-signup/destination-picker.tsx[74-86]
apps/web/src/features/hosting-signup/hosting-signup.tsx[347-362]
apps/web/src/features/hosting-signup/self-host-bundle.ts[205-214]
apps/web/src/features/hosting-signup/self-host-bundle.ts[68-78]
apps/web/src/features/hosting-signup/self-host-bundle.ts[81-89]

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 optional self-host domain is currently only trimmed and then interpolated into multiple generated files (`Caddyfile`, `.env`, README). This can yield broken deployment files when users paste common formats (e.g., `https://example.com`) or include whitespace/control characters, and newline/control characters can change the structure of the generated Caddyfile.
### Issue Context
- Domain is collected via a plain text input and passed through to the bundle generator.
- The bundle generator uses the domain string directly in `Caddyfile` (`${domain} { ... }`) and `.env` (`DOMAIN=${domain}`).
### Fix Focus Areas
- apps/web/src/features/hosting-signup/destination-picker.tsx[74-86]
- apps/web/src/features/hosting-signup/hosting-signup.tsx[347-362]
- apps/web/src/features/hosting-signup/self-host-bundle.ts[205-214]
- apps/web/src/features/hosting-signup/self-host-bundle.ts[81-89]
### Suggested remediation
1. Add a small sanitizer/validator for the domain:
- Reject any value containing whitespace/control characters (including `\r`/`\n`).
- Reject values containing a scheme (`://`), path (`/`), query (`?`), or fragment (`#`).
- Optionally validate against a hostname pattern (DNS hostname) and allow `:port` only if you intend to support it.
2. Enforce this in `buildSelfHostBundle()` (not only in the UI) so other callers can’t generate malformed bundles.
3. If invalid, either:
- treat it as “no domain provided” and fall back to `blog.example.com`, or
- surface a user-visible error and prevent download.

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


View medium (6)
7. e: any in onChange ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
DestinationPicker introduces an explicit any type in the FormControl onChange handler,
weakening type safety and violating the no-any rule for new TypeScript code. This can hide real
event typing issues and runtime errors.
Code

apps/web/src/features/hosting-signup/destination-picker.tsx[R83-86]

+            disabled={disabled}
+            onChange={(e: any) => onDomainChange(e.target.value)}
+            placeholder="blog.example.com"
+          />
Relevance

●●● Strong

Team often removes newly introduced (e: any) in FormControl onChange; multiple recent acceptances
despite one rejection.

PR-#1457
PR-#1438
PR-#1456

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Compliance ID 2668119 disallows introducing any in new/modified TypeScript. The new handler
explicitly types the event parameter as any.

Rule 2668119: Disallow implicit and any types in new TypeScript code
apps/web/src/features/hosting-signup/destination-picker.tsx[83-86]

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

## Issue description
New TypeScript code introduces an explicit `any` type in an event handler.
## Issue Context
`DestinationPicker` uses `onChange={(e: any) => ...}` for a `FormControl` change handler.
## Fix Focus Areas
- apps/web/src/features/hosting-signup/destination-picker.tsx[83-86]

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


8. Unsanitized domain in bundle ✓ Resolved 🐞 Bug ≡ Correctness
Description
The self-host domain is accepted as free-form text and then embedded verbatim into generated files
(notably Caddyfile and .env) with only .trim(), so inputs containing
schemes/paths/whitespace/newlines can produce invalid configs or unintended extra Caddy directives
in the downloaded bundle.
Code

apps/web/src/features/hosting-signup/self-host-bundle.ts[R86-88]

+${domain} {
+    reverse_proxy 127.0.0.1:3000
+}
Relevance

●●● Strong

They regularly sanitize user-controlled strings to prevent newline/log/config injection; likely to
add domain validation/sanitization.

PR-#947
PR-#916

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The domain comes from an unrestricted text input and is passed to the bundle builder; the builder
only trims it and interpolates it into the generated .env and Caddyfile contents.

apps/web/src/features/hosting-signup/destination-picker.tsx[74-86]
apps/web/src/features/hosting-signup/hosting-signup.tsx[347-362]
apps/web/src/features/hosting-signup/self-host-bundle.ts[205-214]
apps/web/src/features/hosting-signup/self-host-bundle.ts[68-78]
apps/web/src/features/hosting-signup/self-host-bundle.ts[81-89]

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 optional self-host domain is currently only trimmed and then interpolated into multiple generated files (`Caddyfile`, `.env`, README). This can yield broken deployment files when users paste common formats (e.g., `https://example.com`) or include whitespace/control characters, and newline/control characters can change the structure of the generated Caddyfile.
### Issue Context
- Domain is collected via a plain text input and passed through to the bundle generator.
- The bundle generator uses the domain string directly in `Caddyfile` (`${domain} { ... }`) and `.env` (`DOMAIN=${domain}`).
### Fix Focus Areas
- apps/web/src/features/hosting-signup/destination-picker.tsx[74-86]
- apps/web/src/features/hosting-signup/hosting-signup.tsx[347-362]
- apps/web/src/features/hosting-signup/self-host-bundle.ts[205-214]
- apps/web/src/features/hosting-signup/self-host-bundle.ts[81-89]
### Suggested remediation
1. Add a small sanitizer/validator for the domain:
 - Reject any value containing whitespace/control characters (including `\r`/`\n`).
 - Reject values containing a scheme (`://`), path (`/`), query (`?`), or fragment (`#`).
 - Optionally validate against a hostname pattern (DNS hostname) and allow `:port` only if you intend to support it.
2. Enforce this in `buildSelfHostBundle()` (not only in the UI) so other callers can’t generate malformed bundles.
3. If invalid, either:
 - treat it as “no domain provided” and fall back to `blog.example.com`, or
 - surface a user-visible error and prevent download.

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


9. e: any in onChange ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
DestinationPicker introduces an explicit any type in the FormControl onChange handler,
weakening type safety and violating the no-any rule for new TypeScript code. This can hide real
event typing issues and runtime errors.
Code

apps/web/src/features/hosting-signup/destination-picker.tsx[R83-86]

+            disabled={disabled}
+            onChange={(e: any) => onDomainChange(e.target.value)}
+            placeholder="blog.example.com"
+          />
Relevance

●●● Strong

Team often removes newly introduced (e: any) in FormControl onChange; multiple recent acceptances
despite one rejection.

PR-#1457
PR-#1438
PR-#1456

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Compliance ID 2668119 disallows introducing any in new/modified TypeScript. The new handler
explicitly types the event parameter as any.

Rule 2668119: Disallow implicit and any types in new TypeScript code
apps/web/src/features/hosting-signup/destination-picker.tsx[83-86]

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

## Issue description
New TypeScript code introduces an explicit `any` type in an event handler.
## Issue Context
`DestinationPicker` uses `onChange={(e: any) => ...}` for a `FormControl` change handler.
## Fix Focus Areas
- apps/web/src/features/hosting-signup/destination-picker.tsx[83-86]

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


10. Hardcoded placeholder blog.example.com ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The new input placeholder uses a hard-coded user-facing string instead of an i18next key, which
breaks translation consistency. This violates the requirement to use i18next for all user-visible
copy.
Code

apps/web/src/features/hosting-signup/destination-picker.tsx[R84-86]

+            onChange={(e: any) => onDomainChange(e.target.value)}
+            placeholder="blog.example.com"
+          />
Relevance

●● Moderate

Repo enforces i18n for user-facing strings, but similar hardcoded attribute strings have been both
accepted and rejected.

PR-#635
PR-#701

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Compliance ID 2668127 requires all user-facing text to come from i18next. The new placeholder is a
literal string rather than i18next.t('...').

Rule 2668127: Use i18next for all user-facing text translations
apps/web/src/features/hosting-signup/destination-picker.tsx[84-86]

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

## Issue description
A new user-facing placeholder string is hard-coded instead of being provided via i18next.
## Issue Context
The `FormControl` placeholder is set to the literal `blog.example.com`.
## Fix Focus Areas
- apps/web/src/features/hosting-signup/destination-picker.tsx[84-86]
- apps/web/src/features/i18n/locales/en-US.json[36-44]

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


11. Hardcoded placeholder blog.example.com ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The new input placeholder uses a hard-coded user-facing string instead of an i18next key, which
breaks translation consistency. This violates the requirement to use i18next for all user-visible
copy.
Code

apps/web/src/features/hosting-signup/destination-picker.tsx[R84-86]

+            onChange={(e: any) => onDomainChange(e.target.value)}
+            placeholder="blog.example.com"
+          />
Relevance

●● Moderate

Repo enforces i18n for user-facing strings, but similar hardcoded attribute strings have been both
accepted and rejected.

PR-#635
PR-#701

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Compliance ID 2668127 requires all user-facing text to come from i18next. The new placeholder is a
literal string rather than i18next.t('...').

Rule 2668127: Use i18next for all user-facing text translations
apps/web/src/features/hosting-signup/destination-picker.tsx[84-86]

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

## Issue description
A new user-facing placeholder string is hard-coded instead of being provided via i18next.
## Issue Context
The `FormControl` placeholder is set to the literal `blog.example.com`.
## Fix Focus Areas
- apps/web/src/features/hosting-signup/destination-picker.tsx[84-86]
- apps/web/src/features/i18n/locales/en-US.json[36-44]

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


12. Hardcoded placeholder blog.example.com ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The new input placeholder uses a hard-coded user-facing string instead of an i18next key, which
breaks translation consistency. This violates the requirement to use i18next for all user-visible
copy.
Code

apps/web/src/features/hosting-signup/destination-picker.tsx[R84-86]

+            onChange={(e: any) => onDomainChange(e.target.value)}
+            placeholder="blog.example.com"
+          />
Relevance

●● Moderate

Repo enforces i18n for user-facing strings, but similar hardcoded attribute strings have been both
accepted and rejected.

PR-#635
PR-#701

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Compliance ID 2668127 requires all user-facing text to come from i18next. The new placeholder is a
literal string rather than i18next.t('...').

Rule 2668127: Use i18next for all user-facing text translations
apps/web/src/features/hosting-signup/destination-picker.tsx[84-86]

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

## Issue description
A new user-facing placeholder string is hard-coded instead of being provided via i18next.
## Issue Context
The `FormControl` placeholder is set to the literal `blog.example.com`.
## Fix Focus Areas
- apps/web/src/features/hosting-signup/destination-picker.tsx[84-86]
- apps/web/src/features/i18n/locales/en-US.json[36-44]

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



Informational

13. Unused DOMAIN env var ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
The generated .env includes DOMAIN and comments that it is used by the Caddyfile, but neither
docker-compose.yml nor Caddyfile references it, which can mislead users into thinking updating
.env updates the HTTPS config.
Code

apps/web/src/features/hosting-signup/self-host-bundle.ts[R76-77]

+# Used by the Caddyfile.
+DOMAIN=${domain}
Relevance

●● Moderate

Unused/misleading env vars are sometimes cleaned up, but no close precedent for generated bundle
.env variables.

PR-#702

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The bundle generator writes DOMAIN into .env, but composeYml() only references TAG and PORT and
caddyfile() inlines the domain string; nothing reads the .env DOMAIN value.

apps/web/src/features/hosting-signup/self-host-bundle.ts[34-66]
apps/web/src/features/hosting-signup/self-host-bundle.ts[68-78]
apps/web/src/features/hosting-signup/self-host-bundle.ts[81-89]

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 self-host bundle emits a `DOMAIN` entry in `.env` and labels it “Used by the Caddyfile”, but the generated `docker-compose.yml` only uses `TAG` and `PORT`, and the generated `Caddyfile` hardcodes the domain at bundle generation time. This makes the bundle’s configuration/docs inconsistent and can confuse users.
### Issue Context
- `.env` contains TAG, PORT, and DOMAIN.
- `docker-compose.yml` references TAG/PORT, not DOMAIN.
- `Caddyfile` embeds the domain literal.
### Fix Focus Areas
- apps/web/src/features/hosting-signup/self-host-bundle.ts[34-66]
- apps/web/src/features/hosting-signup/self-host-bundle.ts[68-78]
- apps/web/src/features/hosting-signup/self-host-bundle.ts[81-89]
### Suggested remediation (pick one)
1. **Simplest:** remove `DOMAIN` from `.env` and delete/adjust the misleading comment.
2. **If you want one source of truth:** redesign the bundle so DOMAIN is actually consumed (e.g., regenerate the Caddyfile from the env value as a documented step), but avoid implying Caddy reads `.env` automatically unless you also document/export it.

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


14. Unused DOMAIN env var ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
The generated .env includes DOMAIN and comments that it is used by the Caddyfile, but neither
docker-compose.yml nor Caddyfile references it, which can mislead users into thinking updating
.env updates the HTTPS config.
Code

apps/web/src/features/hosting-signup/self-host-bundle.ts[R76-77]

+# Used by the Caddyfile.
+DOMAIN=${domain}
Relevance

●● Moderate

Unused/misleading env vars are sometimes cleaned up, but no close precedent for generated bundle
.env variables.

PR-#702

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The bundle generator writes DOMAIN into .env, but composeYml() only references TAG and PORT and
caddyfile() inlines the domain string; nothing reads the .env DOMAIN value.

apps/web/src/features/hosting-signup/self-host-bundle.ts[34-66]
apps/web/src/features/hosting-signup/self-host-bundle.ts[68-78]
apps/web/src/features/hosting-signup/self-host-bundle.ts[81-89]

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 self-host bundle emits a `DOMAIN` entry in `.env` and labels it “Used by the Caddyfile”, but the generated `docker-compose.yml` only uses `TAG` and `PORT`, and the generated `Caddyfile` hardcodes the domain at bundle generation time. This makes the bundle’s configuration/docs inconsistent and can confuse users.
### Issue Context
- `.env` contains TAG, PORT, and DOMAIN.
- `docker-compose.yml` references TAG/PORT, not DOMAIN.
- `Caddyfile` embeds the domain literal.
### Fix Focus Areas
- apps/web/src/features/hosting-signup/self-host-bundle.ts[34-66]
- apps/web/src/features/hosting-signup/self-host-bundle.ts[68-78]
- apps/web/src/features/hosting-signup/self-host-bundle.ts[81-89]
### Suggested remediation (pick one)
1. **Simplest:** remove `DOMAIN` from `.env` and delete/adjust the misleading comment.
2. **If you want one source of truth:** redesign the bundle so DOMAIN is actually consumed (e.g., regenerate the Caddyfile from the env value as a documented step), but avoid implying Caddy reads `.env` automatically unless you also document/export it.

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


15. Unused DOMAIN env var ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
The generated .env includes DOMAIN and comments that it is used by the Caddyfile, but neither
docker-compose.yml nor Caddyfile references it, which can mislead users into thinking updating
.env updates the HTTPS config.
Code

apps/web/src/features/hosting-signup/self-host-bundle.ts[R76-77]

+# Used by the Caddyfile.
+DOMAIN=${domain}
Relevance

●● Moderate

Unused/misleading env vars are sometimes cleaned up, but no close precedent for generated bundle
.env variables.

PR-#702

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The bundle generator writes DOMAIN into .env, but composeYml() only references TAG and PORT and
caddyfile() inlines the domain string; nothing reads the .env DOMAIN value.

apps/web/src/features/hosting-signup/self-host-bundle.ts[34-66]
apps/web/src/features/hosting-signup/self-host-bundle.ts[68-78]
apps/web/src/features/hosting-signup/self-host-bundle.ts[81-89]

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 self-host bundle emits a `DOMAIN` entry in `.env` and labels it “Used by the Caddyfile”, but the generated `docker-compose.yml` only uses `TAG` and `PORT`, and the generated `Caddyfile` hardcodes the domain at bundle generation time. This makes the bundle’s configuration/docs inconsistent and can confuse users.
### Issue Context
- `.env` contains TAG, PORT, and DOMAIN.
- `docker-compose.yml` references TAG/PORT, not DOMAIN.
- `Caddyfile` embeds the domain literal.
### Fix Focus Areas
- apps/web/src/features/hosting-signup/self-host-bundle.ts[34-66]
- apps/web/src/features/hosting-signup/self-host-bundle.ts[68-78]
- apps/web/src/features/hosting-signup/self-host-bundle.ts[81-89]
### Suggested remediation (pick one)
1. **Simplest:** remove `DOMAIN` from `.env` and delete/adjust the misleading comment.
2. **If you want one source of truth:** redesign the bundle so DOMAIN is actually consumed (e.g., regenerate the Caddyfile from the env value as a documented step), but avoid implying Caddy reads `.env` automatically unless you also document/export it.

ⓘ 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

Warning

Review limit reached

@feruzm, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 97 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: bb8c280c-cbb2-4664-9788-51fe5e45bb13

📥 Commits

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

📒 Files selected for processing (8)
  • apps/web/src/features/hosting-signup/destination-picker.tsx
  • apps/web/src/features/hosting-signup/hosting-api.ts
  • apps/web/src/features/hosting-signup/hosting-signup.tsx
  • apps/web/src/features/hosting-signup/self-host-bundle.ts
  • apps/web/src/features/hosting-signup/use-download-self-host-bundle.ts
  • apps/web/src/features/i18n/locales/en-US.json
  • apps/web/src/specs/features/hosting-signup/hosting-signup.spec.tsx
  • apps/web/src/specs/features/hosting-signup/self-host-bundle.spec.ts

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-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Hosting signup: add free self-host option that downloads a pinned deployment bundle

✨ Enhancement 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Add a destination choice to hosting signup: managed (existing) or self-host (free).
• For self-host, compose config and download a runnable Docker/Caddy bundle ZIP.
• Pin bundle images to the platform’s running build SHA, and test ZIP correctness end-to-end.
Diagram

graph TD
  U["User in signup"] --> D{"Destination?"} --> M["Managed flow"] --> T["createTenant + payment"]
  D --> S["Self-host flow"] --> C["POST /v1/tools/compose-config"] --> H["GET /health (sha)"] --> Z["Build ZIP + download"]

  subgraph Legend
    direction LR
    _user["UI"] ~~~ _dec{"Decision"} ~~~ _api["API call"]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Use a ZIP library (e.g., JSZip/fflate)
  • ➕ Less custom binary-format code to maintain
  • ➕ Potentially supports compression/encryption if needed later
  • ➖ Introduces a new dependency in a workspace where adding deps is costly/slow
  • ➖ Still requires careful tests for browser compatibility and bundle determinism
2. Generate the bundle server-side
  • ➕ Keeps binary archive logic out of the browser
  • ➕ Can reuse backend tooling and potentially stream downloads
  • ➖ Moves work to hosting-api deployment and capacity
  • ➖ Requires new API surface, auth/abuse considerations, and rollout coordination
3. Ship a tarball instead of ZIP
  • ➕ Simpler archive format to implement by hand
  • ➕ Common in Linux-centric deployment workflows
  • ➖ Worse UX on Windows/macOS where ZIP is the default expectation
  • ➖ Less friendly for a general audience downloading from a browser

Recommendation: Current approach is reasonable given the explicit constraint of avoiding new workspace dependencies and the small, text-only payload. Keeping the self-host flow strictly non-reserving (no createTenant/payment) is the correct product boundary and is well-asserted in tests. One follow-up worth considering is a more explicit, user-friendly message for the known rollout dependency: if /v1/tools/compose-config returns 404 (endpoint not deployed yet), translate it into a targeted “feature not available yet” error instead of surfacing a generic Not found.

Files changed (8) +893 / -4

Enhancement (5) +589 / -3
destination-picker.tsxAdd managed vs self-host destination radio picker with optional domain +92/-0

Add managed vs self-host destination radio picker with optional domain

• Introduces a new UI control to choose where a customized blog will be hosted. When self-host is selected, it reveals an optional domain input used to tailor the downloaded setup files.

apps/web/src/features/hosting-signup/destination-picker.tsx

hosting-api.tsExtend hosting API client with /health and compose-config endpoints +17/-0

Extend hosting API client with /health and compose-config endpoints

• Adds a lightweight health call returning build identity (version/sha). Adds composeConfig to request a standalone composed config document for self-hosting without creating a tenant.

apps/web/src/features/hosting-signup/hosting-api.ts

hosting-signup.tsxBranch customize step to allow self-host bundle download +80/-3

Branch customize step to allow self-host bundle download

• Adds destination state (managed default) and wires a self-host download action into the customize step UI. Self-host swaps the Continue button for a Download button and surfaces bundle errors without advancing to payment.

apps/web/src/features/hosting-signup/hosting-signup.tsx

self-host-bundle.tsGenerate self-host deployment bundle files and write a STORE ZIP by hand +331/-0

Generate self-host deployment bundle files and write a STORE ZIP by hand

• Creates README/config/compose/env/Caddyfile content for a runnable self-hosted deployment and pins images via a required TAG. Implements CRC32, DOS timestamps, central directory writing, and a deterministic STORE-only ZIP generator.

apps/web/src/features/hosting-signup/self-host-bundle.ts

use-download-self-host-bundle.tsAdd hook to compose config, pin build SHA, and trigger bundle download +69/-0

Add hook to compose config, pin build SHA, and trigger bundle download

• Implements the self-host download flow: calls composeConfig, reads /health for the running SHA, builds a pinned ZIP, and downloads via a temporary blob URL anchor. Refuses to download when build SHA cannot be confirmed and exposes an i18n error message.

apps/web/src/features/hosting-signup/use-download-self-host-bundle.ts

Tests (2) +295 / -1
hosting-signup.spec.tsxAdd signup tests for self-host branch behavior and tag pinning +115/-1

Add signup tests for self-host branch behavior and tag pinning

• Extends hosting API mocks to include composeConfig and health. Adds tests verifying managed remains the default, self-host downloads without createTenant/payment calls, bundle contents include the pinned sha tag, and failures (e.g., /health offline) surface a user error.

apps/web/src/specs/features/hosting-signup/hosting-signup.spec.tsx

self-host-bundle.spec.tsAdd independent ZIP parser tests validating CRCs and byte-accurate sizes +180/-0

Add independent ZIP parser tests validating CRCs and byte-accurate sizes

• Adds tests that parse the generated ZIP via central directory offsets and validate every entry via CRC. Covers unicode/multibyte content sizing, determinism for a fixed timestamp, and correct handling of empty archives.

apps/web/src/specs/features/hosting-signup/self-host-bundle.spec.ts

Documentation (1) +9 / -0
en-US.jsonAdd hosting signup strings for destination choice and self-host errors +9/-0

Add hosting signup strings for destination choice and self-host errors

• Introduces new English strings for the managed/self-host destination picker, optional domain field, download CTA, and “cannot confirm release” error.

apps/web/src/features/i18n/locales/en-US.json

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Hosting signup: add free self-host download bundle path

✨ Enhancement 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Add a managed-vs-self-host destination choice inside the customize step.
• Self-host path composes config and downloads a pinned Docker deployment bundle zip.
• Add ZIP writer + specs ensuring no tenant reservation or payment calls occur.
Diagram

graph TD
  U(["User"]) --> UI["Hosting signup UI"] --> D["Destination picker"]
  D --> M["Managed flow"] --> API{{"Hosting API"}} --> T["createTenant + payment"]
  D --> S["Self-host flow"] --> API{{"Hosting API"}} --> C["compose-config + /health"] --> Z["Build ZIP bundle"] --> DL["Download .zip"]
  subgraph Legend
    direction LR
    _u(["User"]) ~~~ _c["Web component"] ~~~ _api{{"External API"}}
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Use a ZIP library dependency
  • ➕ Simplifies correctness concerns (headers/CRC/encoding handled by library)
  • ➕ Reduces long-term maintenance of binary-format code
  • ➖ Adds a new dependency and associated workspace/release gating constraints noted in the PR
  • ➖ May increase bundle size and still requires test coverage for generated output
2. Generate the bundle server-side
  • ➕ Avoids shipping ZIP-writing logic to the browser
  • ➕ Server can embed authoritative build metadata and potentially stream large bundles
  • ➖ Requires new backend endpoint and deployment coordination
  • ➖ Creates server load and introduces persistence/streaming concerns for downloads
3. Pin tag via config endpoint response (single call)
  • ➕ Removes a second network call to /health
  • ➕ Keeps bundle pinning coupled to the composing service
  • ➖ Requires backend changes and versioning for returned metadata
  • ➖ Health remains a useful independent availability check before download

Recommendation: Given the explicit workspace constraint against adding a ZIP dependency and the goal to keep self-hosting entirely client-driven (no reservation/payment side effects), the current browser-only STORE ZIP approach is reasonable—especially with the strong CRC/round-trip tests. If the bundle grows in size/complexity or dependency constraints change, revisiting a standard ZIP library or server-side generation would reduce maintenance risk.

Files changed (8) +893 / -4

Enhancement (5) +589 / -3
destination-picker.tsxAdd managed vs self-host destination selector with optional domain input +92/-0

Add managed vs self-host destination selector with optional domain input

• Introduces a new DestinationPicker component that renders a radio-style choice between managed hosting and self-hosting. When self-host is selected, it reveals an optional domain input used for bundle instructions/config templates.

apps/web/src/features/hosting-signup/destination-picker.tsx

hosting-api.tsAdd /health and compose-config hosting API helpers +17/-0

Add /health and compose-config hosting API helpers

• Extends the hostingApi client with a health() call to fetch the running platform SHA/version and a composeConfig() call to build an independent deployment config without creating a tenant. These are used exclusively by the free self-host branch.

apps/web/src/features/hosting-signup/hosting-api.ts

hosting-signup.tsxWire self-host branch into signup customize step with download action +80/-3

Wire self-host branch into signup customize step with download action

• Adds destination state (managed default) and an optional self-host domain field to the customize step. When self-host is selected, replaces the Continue button with a download button that generates the deployment bundle and does not advance to payment.

apps/web/src/features/hosting-signup/hosting-signup.tsx

self-host-bundle.tsImplement browser-only self-host bundle generation and STORE ZIP writer +331/-0

Implement browser-only self-host bundle generation and STORE ZIP writer

• Adds pure functions that build the deployment bundle files (README, config.json, docker-compose.yml, .env, Caddyfile) and a hand-rolled ZIP(Store) implementation with CRC32 and deterministic timestamps. Produces a Uint8Array suitable for browser download without external zip dependencies.

apps/web/src/features/hosting-signup/self-host-bundle.ts

use-download-self-host-bundle.tsAdd hook to compose config, pin image tag, and trigger bundle download +69/-0

Add hook to compose config, pin image tag, and trigger bundle download

• Implements a React hook that calls compose-config, fetches the running build SHA from /health to pin the Docker tag, builds the ZIP bundle, and downloads it via Blob URL/anchor click. Surfaces an error instead of downloading an unpinned bundle when /health is unavailable.

apps/web/src/features/hosting-signup/use-download-self-host-bundle.ts

Tests (2) +295 / -1
hosting-signup.spec.tsxAdd end-to-end-ish tests for self-host signup branch behavior +115/-1

Add end-to-end-ish tests for self-host signup branch behavior

• Extends hostingApi mocks to include composeConfig and health. Adds tests asserting the default remains managed, self-host download composes config and downloads a blob without calling createTenant/paymentInstructions, the tag is pinned from /health, and failures are surfaced instead of downloading an unpinned bundle.

apps/web/src/specs/features/hosting-signup/hosting-signup.spec.tsx

self-host-bundle.spec.tsAdd unit tests that parse ZIP output and verify CRC/determinism +180/-0

Add unit tests that parse ZIP output and verify CRC/determinism

• Adds independent ZIP parsing and CRC verification to validate the hand-written ZIP(Store) output. Covers expected bundle contents, tag pinning rules, domain placeholder behavior, unicode byte-length correctness, determinism under fixed timestamps, and empty-archive behavior.

apps/web/src/specs/features/hosting-signup/self-host-bundle.spec.ts

Documentation (1) +9 / -0
en-US.jsonAdd copy for destination picker and self-host download errors +9/-0

Add copy for destination picker and self-host download errors

• Introduces translation strings for the destination selection UI, self-host domain hinting, the download button label, and a user-facing error when the running build SHA cannot be confirmed.

apps/web/src/features/i18n/locales/en-US.json

@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: bfe8cd73dd

ℹ️ 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 on lines +206 to +207
const hasDomain = !!input.domain?.trim();
const domain = hasDomain ? input.domain!.trim() : EXAMPLE_DOMAIN;

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 Validate the optional domain before generating configs

When a user enters a common URL-shaped value such as https://blog.example.com, this raw value is inserted everywhere as a hostname, producing README URLs such as https://https://blog.example.com/rss.xml; whitespace or newline-containing input can also corrupt the generated .env and Caddyfile. Validate and normalize this field to a hostname before building the bundle, or reject it with a form error.

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, and the injection half of it was the serious part: fixed in 1aed805. The field now goes through normalizeDomain(), which parses the value (adding a scheme when absent), takes the hostname, lowercases it and checks it against a DNS-label pattern; anything else returns null and the bundle falls back to the placeholder rather than writing the paste into a config file. So https://blog.example.com/ becomes blog.example.com instead of producing https://https://..., and a value carrying a newline can no longer add lines to the generated Caddyfile, where every line is a directive. The picker shows an inline message when a typed value is unusable, so it is not silently ignored. Tests cover the pasted-URL forms, the rejected forms and a Caddyfile-injection attempt.

Comment on lines +31 to +35
const sha = await hostingApi
.health()
.then((h) => (typeof h.sha === "string" ? h.sha.slice(0, 7) : ""))
.catch(() => "");
if (!sha) {

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 Reject placeholder or malformed build SHAs

When the hosting API is built without GIT_SHA, buildHealthPayload() in apps/self-hosted/hosting/api/src/utils/build-info.ts returns the nonempty value unknown; this check accepts it and generates TAG=sha-unknown, so the promised pinned deployment cannot pull its image. The same problem occurs with any short or non-hex response, so require a valid commit SHA before enabling the download.

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.

Good catch, thank you: fixed in 1aed805. The check was only non-empty, so GIT_SHA unset would have produced TAG=sha-unknown, a bundle that looks fine and fails on the user's first docker compose up. It now requires /^[0-9a-f]{7,40}$/ before enabling the download and refuses with the existing message otherwise. Test asserts that a health response of unknown produces no archive at all.

Comment on lines +708 to +711
describe("self-host branch", () => {
// The whole point of this branch is that it is free and reserves nothing.
// Every test here therefore asserts what was NOT called as much as what was.
async function reachCustomizeStep() {

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 Initialize self-host API mocks within the self-host suite

When these tests are run in isolation with a name filter, the composeConfig and default health mock implementations are never installed because they live in the unrelated first describe's beforeEach; the inherited renewal setup only clears calls. Consequently the download test receives undefined from composeConfig and fails before reaching its assertions. Give this describe its own mock setup so focused and reordered runs are reliable.

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 1aed805: the self-host describe now installs its own composeConfig, health and templates mocks, since the enclosing beforeEach only clears calls. Verified by running the suite with -t "self-host branch", which now passes (5 tests, 23 skipped) rather than failing on an undefined response.

@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. useCallback missing dependencies ✗ Dismissed 📜 Skill insight ≡ Correctness
Description
useDownloadSelfHostBundle() defines download with useCallback(..., []) while closing over
multiple values, risking stale references and violating the hook dependency requirement. This can
cause hard-to-debug behavior changes when surrounding code evolves.
Code

apps/web/src/features/hosting-signup/use-download-self-host-bundle.ts[R63-66]

+    } finally {
+      setBusy(false);
+    }
+  }, []);
Relevance

●●● Strong

Exhaustive-deps/stale-closure fixes are commonly accepted; empty deps on closure values is a typical
change request.

PR-#737
PR-#666

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Compliance ID 2668377 requires hook dependency arrays to be complete. The new callback ends with an
empty dependency array ([]) despite referencing multiple values from the module/hook scope.

apps/web/src/features/hosting-signup/use-download-self-host-bundle.ts[63-66]
Skill: code-review

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

## Issue description
A `useCallback` is declared with an empty dependency array while referencing values from the enclosing scope, violating the hook dependency completeness rule and risking stale closures.

## Issue Context
In `useDownloadSelfHostBundle`, the `download` callback uses `hostingApi`, `buildSelfHostZip`, `i18next`, `setBusy`, and `setError` but ends with `}, []);`.

## Fix Focus Areas
- apps/web/src/features/hosting-signup/use-download-self-host-bundle.ts[22-66]

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



Remediation recommended

2. Unsanitized domain in bundle ✓ Resolved 🐞 Bug ≡ Correctness
Description
The self-host domain is accepted as free-form text and then embedded verbatim into generated files
(notably Caddyfile and .env) with only .trim(), so inputs containing
schemes/paths/whitespace/newlines can produce invalid configs or unintended extra Caddy directives
in the downloaded bundle.
Code

apps/web/src/features/hosting-signup/self-host-bundle.ts[R86-88]

+${domain} {
+    reverse_proxy 127.0.0.1:3000
+}
Relevance

●●● Strong

They regularly sanitize user-controlled strings to prevent newline/log/config injection; likely to
add domain validation/sanitization.

PR-#947
PR-#916

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The domain comes from an unrestricted text input and is passed to the bundle builder; the builder
only trims it and interpolates it into the generated .env and Caddyfile contents.

apps/web/src/features/hosting-signup/destination-picker.tsx[74-86]
apps/web/src/features/hosting-signup/hosting-signup.tsx[347-362]
apps/web/src/features/hosting-signup/self-host-bundle.ts[205-214]
apps/web/src/features/hosting-signup/self-host-bundle.ts[68-78]
apps/web/src/features/hosting-signup/self-host-bundle.ts[81-89]

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 optional self-host domain is currently only trimmed and then interpolated into multiple generated files (`Caddyfile`, `.env`, README). This can yield broken deployment files when users paste common formats (e.g., `https://example.com`) or include whitespace/control characters, and newline/control characters can change the structure of the generated Caddyfile.

### Issue Context
- Domain is collected via a plain text input and passed through to the bundle generator.
- The bundle generator uses the domain string directly in `Caddyfile` (`${domain} { ... }`) and `.env` (`DOMAIN=${domain}`).

### Fix Focus Areas
- apps/web/src/features/hosting-signup/destination-picker.tsx[74-86]
- apps/web/src/features/hosting-signup/hosting-signup.tsx[347-362]
- apps/web/src/features/hosting-signup/self-host-bundle.ts[205-214]
- apps/web/src/features/hosting-signup/self-host-bundle.ts[81-89]

### Suggested remediation
1. Add a small sanitizer/validator for the domain:
  - Reject any value containing whitespace/control characters (including `\r`/`\n`).
  - Reject values containing a scheme (`://`), path (`/`), query (`?`), or fragment (`#`).
  - Optionally validate against a hostname pattern (DNS hostname) and allow `:port` only if you intend to support it.
2. Enforce this in `buildSelfHostBundle()` (not only in the UI) so other callers can’t generate malformed bundles.
3. If invalid, either:
  - treat it as “no domain provided” and fall back to `blog.example.com`, or
  - surface a user-visible error and prevent download.

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


3. e: any in onChange ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
DestinationPicker introduces an explicit any type in the FormControl onChange handler,
weakening type safety and violating the no-any rule for new TypeScript code. This can hide real
event typing issues and runtime errors.
Code

apps/web/src/features/hosting-signup/destination-picker.tsx[R83-86]

+            disabled={disabled}
+            onChange={(e: any) => onDomainChange(e.target.value)}
+            placeholder="blog.example.com"
+          />
Relevance

●●● Strong

Team often removes newly introduced (e: any) in FormControl onChange; multiple recent acceptances
despite one rejection.

PR-#1457
PR-#1438
PR-#1456

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Compliance ID 2668119 disallows introducing any in new/modified TypeScript. The new handler
explicitly types the event parameter as any.

Rule 2668119: Disallow implicit and any types in new TypeScript code
apps/web/src/features/hosting-signup/destination-picker.tsx[83-86]

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

## Issue description
New TypeScript code introduces an explicit `any` type in an event handler.

## Issue Context
`DestinationPicker` uses `onChange={(e: any) => ...}` for a `FormControl` change handler.

## Fix Focus Areas
- apps/web/src/features/hosting-signup/destination-picker.tsx[83-86]

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


4. Hardcoded placeholder blog.example.com ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The new input placeholder uses a hard-coded user-facing string instead of an i18next key, which
breaks translation consistency. This violates the requirement to use i18next for all user-visible
copy.
Code

apps/web/src/features/hosting-signup/destination-picker.tsx[R84-86]

+            onChange={(e: any) => onDomainChange(e.target.value)}
+            placeholder="blog.example.com"
+          />
Relevance

●● Moderate

Repo enforces i18n for user-facing strings, but similar hardcoded attribute strings have been both
accepted and rejected.

PR-#635
PR-#701

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Compliance ID 2668127 requires all user-facing text to come from i18next. The new placeholder is a
literal string rather than i18next.t('...').

Rule 2668127: Use i18next for all user-facing text translations
apps/web/src/features/hosting-signup/destination-picker.tsx[84-86]

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

## Issue description
A new user-facing placeholder string is hard-coded instead of being provided via i18next.

## Issue Context
The `FormControl` placeholder is set to the literal `blog.example.com`.

## Fix Focus Areas
- apps/web/src/features/hosting-signup/destination-picker.tsx[84-86]
- apps/web/src/features/i18n/locales/en-US.json[36-44]

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



Informational

5. Unused DOMAIN env var ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
The generated .env includes DOMAIN and comments that it is used by the Caddyfile, but neither
docker-compose.yml nor Caddyfile references it, which can mislead users into thinking updating
.env updates the HTTPS config.
Code

apps/web/src/features/hosting-signup/self-host-bundle.ts[R76-77]

+# Used by the Caddyfile.
+DOMAIN=${domain}
Relevance

●● Moderate

Unused/misleading env vars are sometimes cleaned up, but no close precedent for generated bundle
.env variables.

PR-#702

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The bundle generator writes DOMAIN into .env, but composeYml() only references TAG and PORT and
caddyfile() inlines the domain string; nothing reads the .env DOMAIN value.

apps/web/src/features/hosting-signup/self-host-bundle.ts[34-66]
apps/web/src/features/hosting-signup/self-host-bundle.ts[68-78]
apps/web/src/features/hosting-signup/self-host-bundle.ts[81-89]

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 self-host bundle emits a `DOMAIN` entry in `.env` and labels it “Used by the Caddyfile”, but the generated `docker-compose.yml` only uses `TAG` and `PORT`, and the generated `Caddyfile` hardcodes the domain at bundle generation time. This makes the bundle’s configuration/docs inconsistent and can confuse users.

### Issue Context
- `.env` contains TAG, PORT, and DOMAIN.
- `docker-compose.yml` references TAG/PORT, not DOMAIN.
- `Caddyfile` embeds the domain literal.

### Fix Focus Areas
- apps/web/src/features/hosting-signup/self-host-bundle.ts[34-66]
- apps/web/src/features/hosting-signup/self-host-bundle.ts[68-78]
- apps/web/src/features/hosting-signup/self-host-bundle.ts[81-89]

### Suggested remediation (pick one)
1. **Simplest:** remove `DOMAIN` from `.env` and delete/adjust the misleading comment.
2. **If you want one source of truth:** redesign the bundle so DOMAIN is actually consumed (e.g., regenerate the Caddyfile from the env value as a documented step), but avoid implying Caddy reads `.env` automatically unless you also document/export it.

ⓘ 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: ⚖️ Balanced: Downgraded extended -> standard: change is below the extended eligibility bar (hunks 13/18, lines 897/200; both must reach the floor). Router rationale: This is a behavior-changing, user-facing self-hosting flow spanning multiple independent paths, API calls, archive generation, deployment configuration, and substantial new logic with several easy-to-miss failure modes.

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/web/src/features/hosting-signup/destination-picker.tsx
Comment thread apps/web/src/features/hosting-signup/destination-picker.tsx Outdated
Comment thread apps/web/src/features/hosting-signup/self-host-bundle.ts
Comment thread apps/web/src/features/hosting-signup/self-host-bundle.ts Outdated
@feruzm

feruzm commented Aug 13, 2026

Copy link
Copy Markdown
Member Author

All three fixed at fc86716.

Community bundles named the wrong editor account. Valid, and the same class as the endpoint findings on #1465: the README said sign in as @${username}, which on a community instance is the keyless hive-NNNNN account. The bundle input now carries owner (falling back to username for a personal blog), the hook passes what it already sends to composeConfig, and the editor instructions use it. Test builds a community bundle and asserts the README names the owner rather than the community.

PORT and the Caddyfile could drift. Valid: .env exposed PORT while the Caddy upstream was a separate literal 3000, so anyone moving off an occupied port got a 502 from a site that was running fine. Both now come from one constant, so they cannot disagree at generation time, and since only a later edit can split them each file points at the other: .env says changing PORT means changing the Caddyfile, and the Caddyfile says its port must match PORT in .env. The README's nginx alternative says the same.

A blank domain left live commands aimed at the example. Valid and the most user-visible of the three: the no-domain README told readers to replace the placeholder in the Caddyfile only, while the generator command and rssFeedUrl carried it too, so following the guide would publish a sitemap and feed advertising blog.example.com. The no-domain branch now opens with a plain statement that the placeholder appears throughout and must be replaced everywhere, and the SEO section repeats it at the point of use. A test asserts the warning appears in the SEO section specifically, and another asserts none of it appears when a domain was given.

Verified by regenerating a community bundle with no domain: CRC-clean archive, README naming @alice as the editor, both warnings present, then extracted and run (container healthy, site 200 on the configured port). 2666 web tests, typecheck and lint clean.

The bot round on this PR has also settled: four Qodo findings resolved and the useCallback one dismissed.

@feruzm
feruzm merged commit 880007b into develop Aug 13, 2026
8 checks passed
@feruzm
feruzm deleted the feature/self-hosted-selfhost-bundle branch August 13, 2026 07:28
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