Skip to content

Hosting signup: customize step before payment - #1438

Open
feruzm wants to merge 1 commit into
developfrom
feature/hosting-signup-customize
Open

Hosting signup: customize step before payment#1438
feruzm wants to merge 1 commit into
developfrom
feature/hosting-signup-customize

Conversation

@feruzm

@feruzm feruzm commented Aug 11, 2026

Copy link
Copy Markdown
Member

Closes #1414.

Signup asked only title and description with the theme hardcoded, so design choices were only reachable after payment and every new instance started identical. The configure step becomes a customize step, ahead of payment.

Client (apps/web):

  • Template picker as cards, rendered from a new GET /v1/templates catalog served by the hosting API (roster ids + name, tagline, palette swatches, heading style), so the signup can never carry its own copy of the template list. A failed catalog load degrades to a plain form and never blocks signup.
  • Accent quick picks plus a free hex field with inline validation; an invalid non-empty value blocks Continue. Font pairing select. Title and description prefilled from the account profile into empty fields only, and taken back out if the name changes before payment so one account's identity cannot ride into another name's tenant.
  • Choices persist per name in localStorage so an abandoned tab resumes where it left off; the draft clears on success. Skipping every choice produces exactly the previous default payload.

Server (hosting API):

  • appearance.ts adds FONT_PRESET_KEYS and ACCENT_HEX_PATTERN next to the template roster, under the same rules (dependency-free, SPA imports them; a lockstep test pins the SPA's FONT_PRESETS to the key set).
  • Both request schemas accept and validate accent and fontPreset; normalizeFlatOverrides maps them under general.styles without planting an empty styles object.
  • The reservation lifecycle now honors the step's promise that the look on screen is the look that activates: a same-owner unpaid reservation is refreshed by re-creation (ownership validated first; the upsert takes the newly submitted config), and the client re-sends creation whenever the name or composed config changed rather than guarding on the name alone. Live tenants and other owners' reservations still 409. Note this is the same trust model first creation always had: an unpaid blog reservation was already claimable unauthenticated, and refresh now lets the real owner overwrite a griefed reservation instead of being stuck with it.

An adversarial review pass drove most of the hardening here: the silent-discard of re-customization (client guard, upsert keeping old config and the 409 path together), the cross-name prefill leak and four surviving test mutants (fontPreset payload, server-side validation, draft restore/clear, prefill) are all fixed with tests pinning them. Totals: hosting API 415 tests, apps/web 2620, self-hosted SPA 880, all typechecks and production builds green.

Known follow-up: the payment page could show the composed look beside the price; that lands with the draft-expiry work (#1415) where the pay-to-keep messaging belongs.

Signup asked only title and description; theme was hardcoded and every
new instance started visually identical. The configure step becomes a
customize step: template cards rendered from a new GET /v1/templates
catalog (roster + display metadata, so the signup can never carry its
own template list), accent quick picks plus a validated free hex field
that blocks Continue while invalid, a font pairing select, and identity
prefilled from the account profile into empty fields only, taken back
out again if the name changes before payment. Choices persist per name
in localStorage so an abandoned tab resumes, and are cleared on
success. Skipping every choice produces exactly the previous default
payload.

Server side: createTenantSchema and the flat PATCH vocabulary accept
accent (#rgb/#rrggbb) and fontPreset (closed key set shared with the
SPA via appearance.ts, with a lockstep test against FONT_PRESETS);
normalizeFlatOverrides maps them under general.styles without planting
an empty styles object.

The reservation lifecycle now honors the step's promise that the look
on screen is the look that activates: a same-owner unpaid reservation
is refreshed by re-creation (route lets it through after ownership
validation; the upsert takes the new config), and the client re-sends
creation whenever the name or the composed config changed instead of
guarding on the name alone. Live tenants and other owners' reservations
still 409.

Closes #1414
@coderabbitai

coderabbitai Bot commented Aug 11, 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: 27 seconds

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: ea7e49ec-080e-4c37-8a95-e308b7370358

📥 Commits

Reviewing files that changed from the base of the PR and between 68bb4c4 and 016e736.

📒 Files selected for processing (17)
  • apps/self-hosted/hosting/api/src/appearance.ts
  • apps/self-hosted/hosting/api/src/index.ts
  • apps/self-hosted/hosting/api/src/routes/templates.test.ts
  • apps/self-hosted/hosting/api/src/routes/templates.ts
  • apps/self-hosted/hosting/api/src/routes/tenant-create-refresh.test.ts
  • apps/self-hosted/hosting/api/src/routes/tenants.ts
  • apps/self-hosted/hosting/api/src/services/flat-overrides.test.ts
  • apps/self-hosted/hosting/api/src/services/tenant-service-cleanup.test.ts
  • apps/self-hosted/hosting/api/src/services/tenant-service.ts
  • apps/self-hosted/hosting/api/src/style-template-display.ts
  • apps/self-hosted/src/styles/style-template-roster.test.ts
  • apps/web/src/features/hosting-signup/accent-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/template-picker.tsx
  • apps/web/src/features/i18n/locales/en-US.json
  • apps/web/src/specs/features/hosting-signup/hosting-signup.spec.tsx

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 11, 2026

Copy link
Copy Markdown

PR Summary by Qodo

Customize hosted sites before payment

✨ Enhancement 🐞 Bug fix 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Adds a pre-payment customization step for templates, accents, fonts, and site identity.
• Serves template metadata from the hosting API while preserving a non-blocking default fallback.
• Refreshes same-owner unpaid reservations so the latest submitted appearance activates.
Diagram

graph TD
  U["Signup user"] --> W["Customize step"] --> A["Create tenant"] --> R["Unpaid reservation"] --> P["Payment activation"]
  W --> C["Template catalog"]
  W --> D["Per-name draft"]
Loading
High-Level Assessment

The API-owned template catalog and server-side closed-set validation are appropriate for preventing roster drift across independently built applications. A shared client/API package would add deployment coupling, while a client-owned catalog would reintroduce duplication; lockstep tests provide a pragmatic safeguard for font keys.

Files changed (17) +1081 / -43

Enhancement (9) +534 / -24
appearance.tsDefine shared appearance validation constants +28/-0

Define shared appearance validation constants

• Adds the accepted font-preset key set and hexadecimal accent pattern for hosting signup validation. Documents the API/SPA boundary and lockstep testing strategy.

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

index.tsRegister the templates catalog route +3/-0

Register the templates catalog route

• Applies the general rate limit and mounts the public '/v1/templates' endpoint.

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

templates.tsExpose a cacheable template catalog +14/-0

Expose a cacheable template catalog

• Adds a public route returning API-owned template picker metadata for the signup UI.

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

style-template-display.tsAdd template picker display metadata +92/-0

Add template picker display metadata

• Defines typed names, taglines, palette swatches, and heading styles for every supported style template. Produces catalog entries with the roster default marker.

apps/self-hosted/hosting/api/src/style-template-display.ts

accent-picker.tsxAdd accent color picker +66/-0

Add accent color picker

• Introduces quick-pick accent colors, a free-form hex input, inline validation, and clearing behavior.

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

hosting-api.tsAdd appearance and template catalog API types +25/-0

Add appearance and template catalog API types

• Extends tenant configuration input with accent and font fields. Defines template-card contracts, client validation, and the template catalog request.

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

hosting-signup.tsxReplace configuration with pre-payment customization +209/-24

Replace configuration with pre-payment customization

• Adds template, accent, font, and identity controls before payment, with profile prefill and per-name localStorage drafts. Recreates a tenant when either its name or composed configuration changes, and clears drafts after successful activation.

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

template-picker.tsxRender selectable template preview cards +83/-0

Render selectable template preview cards

• Renders API-supplied template cards with palette and typography previews. Provides loading placeholders and a non-blocking failure state that retains default signup behavior.

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

en-US.jsonAdd customization step copy +14/-0

Add customization step copy

• Adds English labels, help text, validation feedback, font names, and fallback messaging for the new signup controls.

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

Bug fix (2) +36 / -9
tenants.tsValidate appearance overrides and refresh reservations +22/-1

Validate appearance overrides and refresh reservations

• Accepts validated accent and font preset fields in create and flat update payloads. Allows only the matching owner to recreate an inactive reservation while preserving conflicts for live tenants and other owners.

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

tenant-service.tsPersist latest unpaid reservation configuration +14/-8

Persist latest unpaid reservation configuration

• Changes tenant upsert behavior so a same-owner inactive reservation receives the newest submitted configuration. Maps flat accent and font fields into the existing 'general.styles' document path.

apps/self-hosted/hosting/api/src/services/tenant-service.ts

Tests (6) +511 / -10
templates.test.tsTest template catalog responses +35/-0

Test template catalog responses

• Verifies the catalog covers the template roster, exposes display metadata, identifies one default, and returns cache headers.

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

tenant-create-refresh.test.tsTest appearance validation and reservation refresh +119/-0

Test appearance validation and reservation refresh

• Covers rejected invalid appearance values, accepted valid overrides, same-owner inactive reservation refreshes, and conflict protections.

apps/self-hosted/hosting/api/src/routes/tenant-create-refresh.test.ts

flat-overrides.test.tsTest appearance override normalization +50/-0

Test appearance override normalization

• Verifies accent and font presets map under 'general.styles', avoid empty style objects, and survive config seeding.

apps/self-hosted/hosting/api/src/services/flat-overrides.test.ts

tenant-service-cleanup.test.tsUpdate reservation upsert expectations +6/-2

Update reservation upsert expectations

• Pins the upsert contract that preserves ownership rules while replacing configuration on a same-owner unpaid refresh.

apps/self-hosted/hosting/api/src/services/tenant-service-cleanup.test.ts

style-template-roster.test.tsKeep SPA font presets aligned with API keys +18/-0

Keep SPA font presets aligned with API keys

• Adds a lockstep test ensuring SPA font definitions exactly match API-accepted preset keys.

apps/self-hosted/src/styles/style-template-roster.test.ts

hosting-signup.spec.tsxCover customization signup regressions +283/-8

Cover customization signup regressions

• Adds component coverage for appearance payloads, invalid accent blocking, catalog fallback, font submission, draft restore/clear, profile prefill isolation, and reservation re-customization. Updates existing renders to provide a query client for profile fetching.

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

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (1) 📜 Skill insights (1)

Grey Divider


Action required

1. Stale appearance state leaks across username change 🐞 Bug ≡ Correctness
Description
goCustomize() clears the prefilled title/description when the tenant name changes, but it does not
reset styleTemplate, accent/accentInput, or fontPreset, so appearance choices from a previous name
can linger in component state. As a result, switching from one tenant name to another with no (or
partial) saved draft can silently submit the prior name’s appearance settings in the new name’s
createTenant payload and persist them under the new draft key.
Code

apps/web/src/features/hosting-signup/hosting-signup.tsx[R212-226]

+    // An abandoned tab resumes its customization for the same name.
+    const draft = readCustomizeDraft(tenantUsername);
+    if (draft) {
+      if (draft.styleTemplate !== undefined) setStyleTemplate(draft.styleTemplate);
+      if (draft.accent !== undefined) {
+        setAccent(draft.accent);
+        setAccentInput(draft.accent ?? "");
+      }
+      if (draft.fontPreset !== undefined) setFontPreset(draft.fontPreset);
+      if (draft.title && !nextTitle) nextTitle = draft.title;
+      if (draft.description && !nextDescription) nextDescription = draft.description;
+    }
+    setTitle(nextTitle);
+    setDescription(nextDescription);
+    setStep("customize");
Relevance

●●● Strong

They’ve previously fixed stale signup guards; resetting appearance state on username change matches
accepted correctness patterns.

PR-#1090

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
In hosting-signup.tsx, the name-change guard using plantedRef explicitly “takes back” only the
auto-prefilled title/description when plantedRef.current.name !== tenantUsername (lines 206–211),
with no corresponding reset for styleTemplate/accent/fontPreset. The subsequent draft-restore logic
only sets appearance fields when readCustomizeDraft(tenantUsername) returns a draft for the new name
(lines 214–220), meaning that when no draft exists—or only some keys are
defined—setStyleTemplate/setAccent/setFontPreset are not called for the missing fields and the
previous values remain in component-wide state. Those retained values are then incorporated into the
config object used for tenant creation in goPayment/createTenant (lines 300–308), causing the stale
appearance to be submitted/saved under the new tenant name.

apps/web/src/features/hosting-signup/hosting-signup.tsx[300-312]
apps/web/src/features/hosting-signup/hosting-signup.tsx[104-110]
apps/web/src/features/hosting-signup/hosting-signup.tsx[200-223]
apps/web/src/features/hosting-signup/hosting-signup.tsx[276-307]

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

## Issue description
When the user changes the tenant username after selecting appearance options (style template, accent/accentInput, font preset) for a previous name, those appearance values are not cleared. If the newly entered name has no saved customize draft (or only a partial one), stale appearance state from the previous name persists and is silently included in the createTenant payload and can be saved under the new name’s draft key.

## Issue Context
`goCustomize()` already tracks name changes via `plantedRef` and resets only the prefilled title/description when the tenant name changes, but it does not reset `styleTemplate`, `accent`, `accentInput`, or `fontPreset`. Appearance state lives at the component level, and the draft restoration logic only overwrites fields when `readCustomizeDraft(tenantUsername)` returns a draft and/or when those keys are present; if the draft is missing or incomplete for the new name, old values remain and later get included in the config/payload built during the payment/creation step.

Implement a more robust name-scoped initialization: on tenant name change, initialize all appearance fields to defaults for the destination name, then apply that name’s validated draft as a complete overlay. Also add a test covering “customize Alice → switch to Bob without a draft → verify Bob sends no appearance overrides.”

## Fix Focus Areas
- apps/web/src/features/hosting-signup/hosting-signup.tsx[200-226]
- apps/web/src/features/hosting-signup/hosting-signup.tsx[276-285]
- apps/web/src/features/hosting-signup/hosting-signup.tsx[300-312]

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


2. Unauthenticated reservation overwrite 🐞 Bug ⛨ Security
Description
POST /v1/tenants can replace an inactive reservation’s saved config when a caller-supplied owner
matches the stored owner, but for personal blogs that owner is deterministically derived from an
unauthenticated username, letting any caller overwrite another user’s unpaid
title/description/appearance before activation. Separately, the ?resume flow triggers payment with
empty customization state so the new upsert replaces the inactive row with defaults, causing
legitimate owners to lose their previously reserved look and identity fields.
Code

apps/self-hosted/hosting/api/src/services/tenant-service.ts[239]

+             config = EXCLUDED.config,
Relevance

●● Moderate

Team recently tightened tenant ownership checks, but unauthenticated personal-blog reservation
refresh may be intended trust model.

PR-#1309
PR-#1106

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The create route includes validation and a target lock but no authMiddleware; for personal blogs
it only validates owner === username, and then the SQL upsert accepts any inactive row whose
stored owner equals that request-derived owner and persists the submitted config, recreating a
caller-controlled ownership pattern (similar to a prior communities issue). In the resume path, the
effect confirms an owned inactive tenant and calls goPayment without first restoring the draft via
customization (goCustomize), so goPayment submits null/empty customization state; buildConfig
expands that into defaults and the changed upsert semantics replace the stored config wholesale,
wiping the reservation’s prior customization.

apps/self-hosted/hosting/api/src/routes/tenants.ts[218-258]
apps/self-hosted/hosting/api/src/services/tenant-service.ts[233-249]
apps/web/src/features/hosting-signup/hosting-signup.tsx[371-415]
apps/web/src/features/hosting-signup/hosting-signup.tsx[290-316]
apps/self-hosted/hosting/api/src/services/tenant-service.ts[209-249]
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
Fix the inactive-reservation update semantics so that (1) refreshing/mutating an unpaid reservation cannot be authorized using a forgeable owner derived from an unauthenticated username, and (2) the resume-to-payment flow does not submit empty/default client state that overwrites an existing reservation’s saved customization.

## Issue Context
Anonymous initial creation may remain public, but any subsequent mutation of an existing inactive reservation must require authenticated proof of ownership or a server-issued, unguessable reservation capability/token bound to the reservation row. Additionally, a resume should either preserve/touch the existing config server-side, fetch and restore the saved draft before submitting, or route users through customization before using any replacement/upsert semantics; add tests/coverage showing an unauthenticated caller cannot refresh an existing inactive reservation without proof, and that resuming a reservation with non-default appearance/identity fields preserves those fields after the resume/payment step.

## Fix Focus Areas
- apps/self-hosted/hosting/api/src/routes/tenants.ts[218-258]
- apps/self-hosted/hosting/api/src/services/tenant-service.ts[233-249]
- apps/web/src/features/hosting-signup/hosting-signup.tsx[371-415]
- apps/web/src/features/hosting-signup/hosting-signup.tsx[300-312]

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



Remediation recommended

3. Template fetch lacks cancellation 📜 Skill insight ☼ Reliability
Description
The new template-catalog useEffect performs an async request and calls
setTemplates/setTemplatesFailed without a cancellation guard. This can cause
setState-on-unmounted issues during navigation or fast step changes.
Code

apps/web/src/features/hosting-signup/hosting-signup.tsx[R233-241]

+  useEffect(() => {
+    if (step !== "customize" || templates || templatesFailed) return;
+    Promise.resolve()
+      .then(() => hostingApi.templates?.())
+      .then((r) => {
+        if (r && Array.isArray(r.templates)) setTemplates(r.templates);
+        else setTemplatesFailed(true);
+      })
+      .catch(() => setTemplatesFailed(true));
Relevance

●●● Strong

Unmount/async cleanup guards are commonly accepted here to prevent setState-after-unmount issues.

PR-#711
PR-#990

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2668385 requires guarding async callbacks to avoid state updates after unmount. The
template fetch effect starts an async chain and updates state in .then/.catch without a
cancellation flag or AbortController cleanup.

apps/web/src/features/hosting-signup/hosting-signup.tsx[233-242]
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
An async `useEffect` fetch updates React state without a cancellation/cleanup guard, risking state updates after unmount.

## Issue Context
The compliance checklist requires guarding against setState on unmounted components for async callbacks.

## Fix Focus Areas
- apps/web/src/features/hosting-signup/hosting-signup.tsx[233-242]

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


4. any types in signup 📘 Rule violation ⚙ Maintainability
Description
New TypeScript code introduces any (event handler, profile casting, and test mocks), weakening
type safety and making refactors riskier. This violates the rule disallowing any in new/modified
TS code.
Code

apps/web/src/features/hosting-signup/accent-picker.tsx[R58-59]

+        onChange={(e: any) => onInput(e.target.value)}
+        placeholder="#0066cc"
Relevance

●● Moderate

Repo sometimes tightens types, but also tolerates any in tests/mocks; unclear they’ll refactor all
new anys.

PR-#919
PR-#1050

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2668119 disallows introducing any in changed TypeScript. The PR adds
onChange={(e: any) => ...} in AccentPicker, casts prefillAccount as any in HostingSignup,
and introduces Record<string, any> in the related spec file.

Rule 2668119: Disallow implicit and any types in new TypeScript code
apps/web/src/features/hosting-signup/accent-picker.tsx[55-62]
apps/web/src/features/hosting-signup/hosting-signup.tsx[257-264]
apps/web/src/specs/features/hosting-signup/hosting-signup.spec.tsx[10-16]

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/modified TypeScript introduces `any` (including event handlers and data casting), reducing type safety.

## Issue Context
The compliance checklist requires no new implicit/explicit `any` in changed TS/TSX.

## Fix Focus Areas
- apps/web/src/features/hosting-signup/accent-picker.tsx[55-62]
- apps/web/src/features/hosting-signup/hosting-signup.tsx[257-264]
- apps/web/src/specs/features/hosting-signup/hosting-signup.spec.tsx[10-16]

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



Informational

5. Empty template catalog renders no picker or error 🐞 Bug ☼ Reliability
Description
The customize-step effect treats any array response (including an empty one) from `GET
/v1/templates as success, setting templates to []`; TemplatePicker then renders an empty grid
with no cards and no failure message, leaving the user unable to pick a template with no
explanation. This degrades silently instead of falling back to the plain-form failure path the PR
describes for a failed catalog load.
Code

apps/web/src/features/hosting-signup/hosting-signup.tsx[R237-239]

+      .then((r) => {
+        if (r && Array.isArray(r.templates)) setTemplates(r.templates);
+        else setTemplatesFailed(true);
Relevance

●●● Strong

They usually add explicit empty/error UI instead of silently rendering blank states when data loads
but is unusable.

PR-#670
PR-#1079

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
if (r && Array.isArray(r.templates)) setTemplates(r.templates); else setTemplatesFailed(true);
accepts an empty array as success. TemplatePicker's render logic
(apps/web/src/features/hosting-signup/template-picker.tsx lines 27-38) only shows the 'failed'
message when failed is true and only shows loading skeletons when templates is null; an empty
non-null array falls through to the (empty) grid with no explanatory text.

apps/web/src/features/hosting-signup/template-picker.tsx[26-38]

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 `GET /v1/templates` response with an empty `templates` array is currently treated as a successful catalog load, leaving the signup's customize step with an empty grid and no picker UI and no explanation to the user.

## Issue Context
The fetch effect in HostingSignup only checks `Array.isArray(r.templates)`, not its length, before calling `setTemplates(r.templates)`. TemplatePicker has no dedicated empty-state message; it only shows the load-failed text when `failed` is explicitly true.

## Fix Focus Areas
- apps/web/src/features/hosting-signup/hosting-signup.tsx[233-242]
- apps/web/src/features/hosting-signup/template-picker.tsx[26-38]

Treat a zero-length `templates` array the same as a failed load (call `setTemplatesFailed(true)` instead of `setTemplates([])`), or add an explicit empty-state message in TemplatePicker.

ⓘ 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 is a substantial, behavior-dense cross-stack change spanning signup UI state/persistence, API validation, reservation ownership and refresh semantics, catalog contracts, and payment-adjacent flows, with many independent paths where redundant review could catch subtle defects.

Grey Divider

Tip of the day
💡 Did you know, you can group findings by type and pick your Finding display, from Minimal to Full

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +58 to +59
onChange={(e: any) => onInput(e.target.value)}
placeholder="#0066cc"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

1. any types in signup 📘 Rule violation ⚙ Maintainability

New TypeScript code introduces any (event handler, profile casting, and test mocks), weakening
type safety and making refactors riskier. This violates the rule disallowing any in new/modified
TS code.
Agent Prompt
## Issue description
New/modified TypeScript introduces `any` (including event handlers and data casting), reducing type safety.

## Issue Context
The compliance checklist requires no new implicit/explicit `any` in changed TS/TSX.

## Fix Focus Areas
- apps/web/src/features/hosting-signup/accent-picker.tsx[55-62]
- apps/web/src/features/hosting-signup/hosting-signup.tsx[257-264]
- apps/web/src/specs/features/hosting-signup/hosting-signup.spec.tsx[10-16]

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

Comment on lines +233 to +241
useEffect(() => {
if (step !== "customize" || templates || templatesFailed) return;
Promise.resolve()
.then(() => hostingApi.templates?.())
.then((r) => {
if (r && Array.isArray(r.templates)) setTemplates(r.templates);
else setTemplatesFailed(true);
})
.catch(() => setTemplatesFailed(true));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

2. Template fetch lacks cancellation 📜 Skill insight ☼ Reliability

The new template-catalog useEffect performs an async request and calls
setTemplates/setTemplatesFailed without a cancellation guard. This can cause
setState-on-unmounted issues during navigation or fast step changes.
Agent Prompt
## Issue description
An async `useEffect` fetch updates React state without a cancellation/cleanup guard, risking state updates after unmount.

## Issue Context
The compliance checklist requires guarding against setState on unmounted components for async callbacks.

## Fix Focus Areas
- apps/web/src/features/hosting-signup/hosting-signup.tsx[233-242]

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

THEN EXCLUDED.owner ELSE tenants.owner END,
config = CASE WHEN tenants.subscription_status = 'abandoned'
THEN EXCLUDED.config ELSE tenants.config END,
config = EXCLUDED.config,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

3. Unauthenticated reservation overwrite 🐞 Bug ⛨ Security

POST /v1/tenants can replace an inactive reservation’s saved config when a caller-supplied owner
matches the stored owner, but for personal blogs that owner is deterministically derived from an
unauthenticated username, letting any caller overwrite another user’s unpaid
title/description/appearance before activation. Separately, the ?resume flow triggers payment with
empty customization state so the new upsert replaces the inactive row with defaults, causing
legitimate owners to lose their previously reserved look and identity fields.
Agent Prompt
## Issue description
Fix the inactive-reservation update semantics so that (1) refreshing/mutating an unpaid reservation cannot be authorized using a forgeable owner derived from an unauthenticated username, and (2) the resume-to-payment flow does not submit empty/default client state that overwrites an existing reservation’s saved customization.

## Issue Context
Anonymous initial creation may remain public, but any subsequent mutation of an existing inactive reservation must require authenticated proof of ownership or a server-issued, unguessable reservation capability/token bound to the reservation row. Additionally, a resume should either preserve/touch the existing config server-side, fetch and restore the saved draft before submitting, or route users through customization before using any replacement/upsert semantics; add tests/coverage showing an unauthenticated caller cannot refresh an existing inactive reservation without proof, and that resuming a reservation with non-default appearance/identity fields preserves those fields after the resume/payment step.

## Fix Focus Areas
- apps/self-hosted/hosting/api/src/routes/tenants.ts[218-258]
- apps/self-hosted/hosting/api/src/services/tenant-service.ts[233-249]
- apps/web/src/features/hosting-signup/hosting-signup.tsx[371-415]
- apps/web/src/features/hosting-signup/hosting-signup.tsx[300-312]

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

Comment on lines +212 to +226
// An abandoned tab resumes its customization for the same name.
const draft = readCustomizeDraft(tenantUsername);
if (draft) {
if (draft.styleTemplate !== undefined) setStyleTemplate(draft.styleTemplate);
if (draft.accent !== undefined) {
setAccent(draft.accent);
setAccentInput(draft.accent ?? "");
}
if (draft.fontPreset !== undefined) setFontPreset(draft.fontPreset);
if (draft.title && !nextTitle) nextTitle = draft.title;
if (draft.description && !nextDescription) nextDescription = draft.description;
}
setTitle(nextTitle);
setDescription(nextDescription);
setStep("customize");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

4. Stale appearance state leaks across username change 🐞 Bug ≡ Correctness

goCustomize() clears the prefilled title/description when the tenant name changes, but it does not
reset styleTemplate, accent/accentInput, or fontPreset, so appearance choices from a previous name
can linger in component state. As a result, switching from one tenant name to another with no (or
partial) saved draft can silently submit the prior name’s appearance settings in the new name’s
createTenant payload and persist them under the new draft key.
Agent Prompt
## Issue description
When the user changes the tenant username after selecting appearance options (style template, accent/accentInput, font preset) for a previous name, those appearance values are not cleared. If the newly entered name has no saved customize draft (or only a partial one), stale appearance state from the previous name persists and is silently included in the createTenant payload and can be saved under the new name’s draft key.

## Issue Context
`goCustomize()` already tracks name changes via `plantedRef` and resets only the prefilled title/description when the tenant name changes, but it does not reset `styleTemplate`, `accent`, `accentInput`, or `fontPreset`. Appearance state lives at the component level, and the draft restoration logic only overwrites fields when `readCustomizeDraft(tenantUsername)` returns a draft and/or when those keys are present; if the draft is missing or incomplete for the new name, old values remain and later get included in the config/payload built during the payment/creation step.

Implement a more robust name-scoped initialization: on tenant name change, initialize all appearance fields to defaults for the destination name, then apply that name’s validated draft as a complete overlay. Also add a test covering “customize Alice → switch to Bob without a draft → verify Bob sends no appearance overrides.”

## Fix Focus Areas
- apps/web/src/features/hosting-signup/hosting-signup.tsx[200-226]
- apps/web/src/features/hosting-signup/hosting-signup.tsx[276-285]
- apps/web/src/features/hosting-signup/hosting-signup.tsx[300-312]

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

Comment on lines +237 to +239
.then((r) => {
if (r && Array.isArray(r.templates)) setTemplates(r.templates);
else setTemplatesFailed(true);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Informational

5. Empty template catalog renders no picker or error 🐞 Bug ☼ Reliability

The customize-step effect treats any array response (including an empty one) from `GET
/v1/templates as success, setting templates to []`; TemplatePicker then renders an empty grid
with no cards and no failure message, leaving the user unable to pick a template with no
explanation. This degrades silently instead of falling back to the plain-form failure path the PR
describes for a failed catalog load.
Agent Prompt
## Issue description
A `GET /v1/templates` response with an empty `templates` array is currently treated as a successful catalog load, leaving the signup's customize step with an empty grid and no picker UI and no explanation to the user.

## Issue Context
The fetch effect in HostingSignup only checks `Array.isArray(r.templates)`, not its length, before calling `setTemplates(r.templates)`. TemplatePicker has no dedicated empty-state message; it only shows the load-failed text when `failed` is explicitly true.

## Fix Focus Areas
- apps/web/src/features/hosting-signup/hosting-signup.tsx[233-242]
- apps/web/src/features/hosting-signup/template-picker.tsx[26-38]

Treat a zero-length `templates` array the same as a failed load (call `setTemplatesFailed(true)` instead of `setTemplates([])`), or add an explicit empty-state message in TemplatePicker.

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

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (7) 📘 Rule violations (1) 📎 Requirement gaps (1) 📜 Skill insights (1)

Grey Divider


Action required

1. Stale appearance state leaks across username change 🐞 Bug ≡ Correctness
Description
goCustomize() clears the prefilled title/description when the tenant name changes, but it does not
reset styleTemplate, accent/accentInput, or fontPreset, so appearance choices from a previous name
can linger in component state. As a result, switching from one tenant name to another with no (or
partial) saved draft can silently submit the prior name’s appearance settings in the new name’s
createTenant payload and persist them under the new draft key.
Code

apps/web/src/features/hosting-signup/hosting-signup.tsx[R212-226]

+    // An abandoned tab resumes its customization for the same name.
+    const draft = readCustomizeDraft(tenantUsername);
+    if (draft) {
+      if (draft.styleTemplate !== undefined) setStyleTemplate(draft.styleTemplate);
+      if (draft.accent !== undefined) {
+        setAccent(draft.accent);
+        setAccentInput(draft.accent ?? "");
+      }
+      if (draft.fontPreset !== undefined) setFontPreset(draft.fontPreset);
+      if (draft.title && !nextTitle) nextTitle = draft.title;
+      if (draft.description && !nextDescription) nextDescription = draft.description;
+    }
+    setTitle(nextTitle);
+    setDescription(nextDescription);
+    setStep("customize");
Relevance

●●● Strong

They’ve previously fixed stale signup guards; resetting appearance state on username change matches
accepted correctness patterns.

PR-#1090

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
In hosting-signup.tsx, the name-change guard using plantedRef explicitly “takes back” only the
auto-prefilled title/description when plantedRef.current.name !== tenantUsername (lines 206–211),
with no corresponding reset for styleTemplate/accent/fontPreset. The subsequent draft-restore logic
only sets appearance fields when readCustomizeDraft(tenantUsername) returns a draft for the new name
(lines 214–220), meaning that when no draft exists—or only some keys are
defined—setStyleTemplate/setAccent/setFontPreset are not called for the missing fields and the
previous values remain in component-wide state. Those retained values are then incorporated into the
config object used for tenant creation in goPayment/createTenant (lines 300–308), causing the stale
appearance to be submitted/saved under the new tenant name.

apps/web/src/features/hosting-signup/hosting-signup.tsx[300-312]
apps/web/src/features/hosting-signup/hosting-signup.tsx[104-110]
apps/web/src/features/hosting-signup/hosting-signup.tsx[200-223]
apps/web/src/features/hosting-signup/hosting-signup.tsx[276-307]

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

## Issue description
When the user changes the tenant username after selecting appearance options (style template, accent/accentInput, font preset) for a previous name, those appearance values are not cleared. If the newly entered name has no saved customize draft (or only a partial one), stale appearance state from the previous name persists and is silently included in the createTenant payload and can be saved under the new name’s draft key.
## Issue Context
`goCustomize()` already tracks name changes via `plantedRef` and resets only the prefilled title/description when the tenant name changes, but it does not reset `styleTemplate`, `accent`, `accentInput`, or `fontPreset`. Appearance state lives at the component level, and the draft restoration logic only overwrites fields when `readCustomizeDraft(tenantUsername)` returns a draft and/or when those keys are present; if the draft is missing or incomplete for the new name, old values remain and later get included in the config/payload built during the payment/creation step.
Implement a more robust name-scoped initialization: on tenant name change, initialize all appearance fields to defaults for the destination name, then apply that name’s validated draft as a complete overlay. Also add a test covering “customize Alice → switch to Bob without a draft → verify Bob sends no appearance overrides.”
## Fix Focus Areas
- apps/web/src/features/hosting-signup/hosting-signup.tsx[200-226]
- apps/web/src/features/hosting-signup/hosting-signup.tsx[276-285]
- apps/web/src/features/hosting-signup/hosting-signup.tsx[300-312]

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


2. Unauthenticated reservation overwrite 🐞 Bug ⛨ Security
Description
POST /v1/tenants can replace an inactive reservation’s saved config when a caller-supplied owner
matches the stored owner, but for personal blogs that owner is deterministically derived from an
unauthenticated username, letting any caller overwrite another user’s unpaid
title/description/appearance before activation. Separately, the ?resume flow triggers payment with
empty customization state so the new upsert replaces the inactive row with defaults, causing
legitimate owners to lose their previously reserved look and identity fields.
Code

apps/self-hosted/hosting/api/src/services/tenant-service.ts[239]

+             config = EXCLUDED.config,
Relevance

●● Moderate

Team recently tightened tenant ownership checks, but unauthenticated personal-blog reservation
refresh may be intended trust model.

PR-#1309
PR-#1106

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The create route includes validation and a target lock but no authMiddleware; for personal blogs
it only validates owner === username, and then the SQL upsert accepts any inactive row whose
stored owner equals that request-derived owner and persists the submitted config, recreating a
caller-controlled ownership pattern (similar to a prior communities issue). In the resume path, the
effect confirms an owned inactive tenant and calls goPayment without first restoring the draft via
customization (goCustomize), so goPayment submits null/empty customization state; buildConfig
expands that into defaults and the changed upsert semantics replace the stored config wholesale,
wiping the reservation’s prior customization.

apps/self-hosted/hosting/api/src/routes/tenants.ts[218-258]
apps/self-hosted/hosting/api/src/services/tenant-service.ts[233-249]
apps/web/src/features/hosting-signup/hosting-signup.tsx[371-415]
apps/web/src/features/hosting-signup/hosting-signup.tsx[290-316]
apps/self-hosted/hosting/api/src/services/tenant-service.ts[209-249]
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
Fix the inactive-reservation update semantics so that (1) refreshing/mutating an unpaid reservation cannot be authorized using a forgeable owner derived from an unauthenticated username, and (2) the resume-to-payment flow does not submit empty/default client state that overwrites an existing reservation’s saved customization.
## Issue Context
Anonymous initial creation may remain public, but any subsequent mutation of an existing inactive reservation must require authenticated proof of ownership or a server-issued, unguessable reservation capability/token bound to the reservation row. Additionally, a resume should either preserve/touch the existing config server-side, fetch and restore the saved draft before submitting, or route users through customization before using any replacement/upsert semantics; add tests/coverage showing an unauthenticated caller cannot refresh an existing inactive reservation without proof, and that resuming a reservation with non-default appearance/identity fields preserves those fields after the resume/payment step.
## Fix Focus Areas
- apps/self-hosted/hosting/api/src/routes/tenants.ts[218-258]
- apps/self-hosted/hosting/api/src/services/tenant-service.ts[233-249]
- apps/web/src/features/hosting-signup/hosting-signup.tsx[371-415]
- apps/web/src/features/hosting-signup/hosting-signup.tsx[300-312]

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



Remediation recommended

3. Draft lacks grace-window expiry 📎 Requirement gap ⛨ Security ⭐ New
Description
The new customize draft is persisted in localStorage per name but has no timestamp/TTL and is only
cleared on success, so draft/customization data can persist indefinitely past the unpaid-reservation
grace window. This violates the requirement to discard draft/customization configuration after the
grace period.
Code

apps/web/src/features/hosting-signup/hosting-signup.tsx[R280-283]

+      localStorage.setItem(
+        customizeDraftKey(tenantUsername),
+        JSON.stringify({ styleTemplate, accent, fontPreset, title, description })
+      );
Evidence
PR Compliance ID 1 requires unpaid reservations to expire after a grace window and any stored
draft/customization configuration to be discarded. The added code persists `{ styleTemplate, accent,
fontPreset, title, description } to localStorage` without a timestamp/TTL and only clears it on
success, so the draft can survive beyond the grace period.

Expire unpaid reservations after grace window and discard draft config
apps/web/src/features/hosting-signup/hosting-signup.tsx[276-285]
apps/web/src/features/hosting-signup/hosting-signup.tsx[363-368]

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

## Issue description
`localStorage` customize drafts are saved without any expiry metadata and therefore can persist indefinitely, even after an unpaid reservation should have expired.

## Issue Context
PR adds per-name draft persistence via `customizeDraftKey()` + `localStorage.setItem(...)` and only removes it on `step === "success"`. Compliance requires draft/customization configuration to be discarded after the unpaid-reservation grace window (proposed 7 days).

## Fix Focus Areas
- apps/web/src/features/hosting-signup/hosting-signup.tsx[65-75]
- apps/web/src/features/hosting-signup/hosting-signup.tsx[276-285]
- apps/web/src/features/hosting-signup/hosting-signup.tsx[363-369]

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


4. Appearance leaks across names 🐞 Bug ≡ Correctness ⭐ New
Description
When the user goes Back and changes the tenant name, goCustomize only updates appearance state if
the new name has a draft; otherwise the previous name's template, accent, and font remain selected.
Those retained values are then persisted and submitted for the new tenant, violating the per-name
draft boundary.
Code

apps/web/src/features/hosting-signup/hosting-signup.tsx[R213-215]

+    const draft = readCustomizeDraft(tenantUsername);
+    if (draft) {
+      if (draft.styleTemplate !== undefined) setStyleTemplate(draft.styleTemplate);
Evidence
Appearance is held in one component-level state set. The name transition only calls appearance
setters inside if (draft), after which the persistence effect writes the still-retained values
under the new name and goPayment sends them.

apps/web/src/features/hosting-signup/hosting-signup.tsx[104-110]
apps/web/src/features/hosting-signup/hosting-signup.tsx[181-226]
apps/web/src/features/hosting-signup/hosting-signup.tsx[276-285]
apps/web/src/features/hosting-signup/hosting-signup.tsx[300-307]

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

## Issue description
Appearance state from one tenant name survives when navigating back and selecting another name with no saved draft.

## Issue Context
Initialize every per-name customization field to its default before overlaying the selected name's draft. Preserve explicit `null` values and also reset `accentInput` consistently.

## Fix Focus Areas
- apps/web/src/features/hosting-signup/hosting-signup.tsx[181-226]
- apps/web/src/features/hosting-signup/hosting-signup.tsx[276-285]
- apps/web/src/features/hosting-signup/hosting-signup.tsx[300-307]

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


5. Reload clears wrong draft 🐞 Bug ≡ Correctness ⭐ New
Description
Pending-payment recovery polls the tenant stored in session storage but advances to success without
synchronizing tenantUsername; the new cleanup deletes the draft keyed by that unchanged state. If
a logged-in account paid for a different personal blog, recovery retains the paid blog's stale draft
and may delete the payer's unrelated draft.
Code

apps/web/src/features/hosting-signup/hosting-signup.tsx[R365-367]

+    if (step !== "success" || !tenantUsername) return;
+    try {
+      localStorage.removeItem(customizeDraftKey(tenantUsername));
Evidence
The initial personal-blog username comes from the active account, while one-click HBD availability
is not restricted to that same tenant. Recovery uses pending.tenant only for polling and sets
step to success without updating the form name, so the cleanup effect can target a different key.

apps/web/src/features/hosting-signup/hosting-signup.tsx[93-110]
apps/web/src/features/hosting-signup/hosting-signup.tsx[157-161]
apps/web/src/features/hosting-signup/hosting-signup.tsx[363-369]
apps/web/src/features/hosting-signup/hosting-signup.tsx[556-608]

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

## Issue description
Success cleanup uses the form's current tenant name rather than the tenant recovered from the pending-payment marker.

## Issue Context
Keep the successfully activated tenant as the cleanup source of truth, including the reload recovery path. Add coverage where the active account and pending tenant differ and both have local drafts.

## Fix Focus Areas
- apps/web/src/features/hosting-signup/hosting-signup.tsx[363-369]
- apps/web/src/features/hosting-signup/hosting-signup.tsx[556-608]

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


View review recommended (4)
6. Config overwrite discards concurrent edits on refresh 🐞 Bug ≡ Correctness ⭐ New
Description
The upsert for same-owner inactive reservations now unconditionally sets `tenants.config =
EXCLUDED.config, so any newer configuration changes saved to the tenant (e.g., via PATCH
/v1/tenants/:username` / Configuration Editor or from another tab/device) can be silently
overwritten by an older or default client payload. This is especially risky in the ?resume= flow,
which calls goPayment from fresh/empty appearance state without restoring the saved draft, causing
an activated site to differ from what the user previously customized.
Code

apps/self-hosted/hosting/api/src/services/tenant-service.ts[R236-239]

       ON CONFLICT (username) DO UPDATE
         SET owner = CASE WHEN tenants.subscription_status = 'abandoned'
                          THEN EXCLUDED.owner ELSE tenants.owner END,
-             config = CASE WHEN tenants.subscription_status = 'abandoned'
-                           THEN EXCLUDED.config ELSE tenants.config END,
+             config = EXCLUDED.config,
Evidence
The SQL change replaces the previous conditional assignment (`config = CASE WHEN ... THEN
EXCLUDED.config ELSE tenants.config END) with an unconditional config = EXCLUDED.config`, and the
accompanying comment indicates the intent is for the “latest submission” to win on every same-owner
inactive refresh—even when the client isn’t actually submitting the newest full picture. On the
client side, appearance choices are initialized empty and composed in goPayment; the resume effect
invokes goPayment directly rather than going through goCustomize where drafts are restored,
meaning a resume can submit defaults/flat overrides produced by buildConfig (defaults + client
overrides, not the currently stored tenant config) and thereby overwrite the server’s saved
customization.

apps/self-hosted/hosting/api/src/services/tenant-service.ts[233-250]
apps/self-hosted/hosting/api/src/services/tenant-service.ts[223-249]
apps/web/src/features/hosting-signup/hosting-signup.tsx[100-110]
apps/web/src/features/hosting-signup/hosting-signup.tsx[300-315]
apps/web/src/features/hosting-signup/hosting-signup.tsx[371-415]

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 `POST /v1/tenants` create upsert for same-owner `inactive` reservations now always replaces the stored `tenants.config` with `EXCLUDED.config`, which can discard newer configuration changes (e.g., made via `PATCH /v1/tenants/:username` / Configuration Editor or another tab/device) and can also overwrite saved customization during `?resume=` because the resume path submits from fresh/empty appearance state.

## Issue Context
The customize step wants the latest signup submission’s look to win, but `buildConfig` builds from defaults plus the client’s flat overrides rather than from the currently stored tenant config. Separately, the `?resume=` flow bypasses the customize step and its local draft restoration by calling `goPayment` directly, so resuming before payment can submit default/empty appearance values and—combined with the unconditional upsert—silently reset the stored customization, resulting in the activated site not matching what the user saved.

## Fix Focus Areas
- apps/self-hosted/hosting/api/src/services/tenant-service.ts[209-258]
- apps/self-hosted/hosting/api/src/services/tenant-service.ts[1178-1200]
- apps/self-hosted/hosting/api/src/services/tenant-service.ts[223-249]
- apps/web/src/features/hosting-signup/hosting-signup.tsx[300-315]
- apps/web/src/features/hosting-signup/hosting-signup.tsx[371-415]

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


7. createdForRef dedup key omits owner 🐞 Bug ≡ Correctness ⭐ New
Description
In goPayment, the guard that decides whether to re-send createTenant compares only `{name,
payload}, but payload is derived from a config object that never includes owner`; for a
community signup owner is computed separately from activeUser?.username and passed directly to
createTenant. If the active user changes (e.g., logout/login as a different admin) while the
community id and the rest of the config stay the same, the stale createdForRef entry prevents a
required re-creation with the new owner, so the reservation is not re-associated and the server may
reject the next mutating request as belonging to a different owner.
Code

apps/web/src/features/hosting-signup/hosting-signup.tsx[R300-312]

+      const config = {
+        theme: "system" as const,
+        title: title.trim() || undefined,
+        description: description.trim() || undefined,
+        styleTemplate: styleTemplate ?? undefined,
+        accent: accent ?? undefined,
+        fontPreset: fontPreset ?? undefined,
+        ...(isCommunity ? { type: "community" as const, communityId: uname } : {})
+      };
+      const payload = JSON.stringify(config);
+      if (createdForRef.current?.name !== uname || createdForRef.current?.payload !== payload) {
+        const res = await hostingApi.createTenant(uname, owner, config);
+        createdForRef.current = { name: uname, payload };
Evidence
owner is computed at line 298 (`const owner = isCommunity ? (activeUser?.username ?? "") :
uname;) but is not part of the config object serialized into payload` at line 309, so the
re-create guard at line 310 cannot detect an owner change for a community while uname and the rest
of the config are unchanged.

apps/web/src/features/hosting-signup/hosting-signup.tsx[295-312]

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

## Issue description
`createdForRef` is used to decide whether `createTenant` needs to be re-sent before moving to the payment step. It compares only the tenant name and a JSON-serialized config that does not include `owner`, even though `owner` is passed to `createTenant` and can change independently of the config for a community signup.

## Issue Context
For a community instance, `owner` is derived from `activeUser?.username`, separate from the `config` object. If the active user changes between two calls to `goPayment` while the community id and rest of config are unchanged, the guard incorrectly treats the reservation as already up to date and skips re-creating it with the new owner.

## Fix Focus Areas
- apps/web/src/features/hosting-signup/hosting-signup.tsx[292-361]

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


8. Template fetch lacks cancellation 📜 Skill insight ☼ Reliability
Description
The new template-catalog useEffect performs an async request and calls
setTemplates/setTemplatesFailed without a cancellation guard. This can cause
setState-on-unmounted issues during navigation or fast step changes.
Code

apps/web/src/features/hosting-signup/hosting-signup.tsx[R233-241]

+  useEffect(() => {
+    if (step !== "customize" || templates || templatesFailed) return;
+    Promise.resolve()
+      .then(() => hostingApi.templates?.())
+      .then((r) => {
+        if (r && Array.isArray(r.templates)) setTemplates(r.templates);
+        else setTemplatesFailed(true);
+      })
+      .catch(() => setTemplatesFailed(true));
Relevance

●●● Strong

Unmount/async cleanup guards are commonly accepted here to prevent setState-after-unmount issues.

PR-#711
PR-#990

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2668385 requires guarding async callbacks to avoid state updates after unmount. The
template fetch effect starts an async chain and updates state in .then/.catch without a
cancellation flag or AbortController cleanup.

apps/web/src/features/hosting-signup/hosting-signup.tsx[233-242]
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
An async `useEffect` fetch updates React state without a cancellation/cleanup guard, risking state updates after unmount.
## Issue Context
The compliance checklist requires guarding against setState on unmounted components for async callbacks.
## Fix Focus Areas
- apps/web/src/features/hosting-signup/hosting-signup.tsx[233-242]

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


9. any types in signup 📘 Rule violation ⚙ Maintainability
Description
New TypeScript code introduces any (event handler, profile casting, and test mocks), weakening
type safety and making refactors riskier. This violates the rule disallowing any in new/modified
TS code.
Code

apps/web/src/features/hosting-signup/accent-picker.tsx[R58-59]

+        onChange={(e: any) => onInput(e.target.value)}
+        placeholder="#0066cc"
Relevance

●● Moderate

Repo sometimes tightens types, but also tolerates any in tests/mocks; unclear they’ll refactor all
new anys.

PR-#919
PR-#1050

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2668119 disallows introducing any in changed TypeScript. The PR adds
onChange={(e: any) => ...} in AccentPicker, casts prefillAccount as any in HostingSignup,
and introduces Record in the related spec file.

Rule 2668119: Disallow implicit and any types in new TypeScript code
apps/web/src/features/hosting-signup/accent-picker.tsx[55-62]
apps/web/src/features/hosting-signup/hosting-signup.tsx[257-264]
apps/web/src/specs/features/hosting-signup/hosting-signup.spec.tsx[10-16]

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/modified TypeScript introduces `any` (including event handlers and data casting), reducing type safety.
## Issue Context
The compliance checklist requires no new implicit/explicit `any` in changed TS/TSX.
## Fix Focus Areas
- apps/web/src/features/hosting-signup/accent-picker.tsx[55-62]
- apps/web/src/features/hosting-signup/hosting-signup.tsx[257-264]
- apps/web/src/specs/features/hosting-signup/hosting-signup.spec.tsx[10-16]

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



Informational

10. Empty template catalog renders no picker or error 🐞 Bug ☼ Reliability
Description
The customize-step effect treats any array response (including an empty one) from `GET
/v1/templates as success, setting templates to []`; TemplatePicker then renders an empty grid
with no cards and no failure message, leaving the user unable to pick a template with no
explanation. This degrades silently instead of falling back to the plain-form failure path the PR
describes for a failed catalog load.
Code

apps/web/src/features/hosting-signup/hosting-signup.tsx[R237-239]

+      .then((r) => {
+        if (r && Array.isArray(r.templates)) setTemplates(r.templates);
+        else setTemplatesFailed(true);
Relevance

●●● Strong

They usually add explicit empty/error UI instead of silently rendering blank states when data loads
but is unusable.

PR-#670
PR-#1079

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
if (r && Array.isArray(r.templates)) setTemplates(r.templates); else setTemplatesFailed(true);
accepts an empty array as success. TemplatePicker's render logic
(apps/web/src/features/hosting-signup/template-picker.tsx lines 27-38) only shows the 'failed'
message when failed is true and only shows loading skeletons when templates is null; an empty
non-null array falls through to the (empty) grid with no explanatory text.

apps/web/src/features/hosting-signup/template-picker.tsx[26-38]

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 `GET /v1/templates` response with an empty `templates` array is currently treated as a successful catalog load, leaving the signup's customize step with an empty grid and no picker UI and no explanation to the user.
## Issue Context
The fetch effect in HostingSignup only checks `Array.isArray(r.templates)`, not its length, before calling `setTemplates(r.templates)`. TemplatePicker has no dedicated empty-state message; it only shows the load-failed text when `failed` is explicitly true.
## Fix Focus Areas
- apps/web/src/features/hosting-signup/hosting-signup.tsx[233-242]
- apps/web/src/features/hosting-signup/template-picker.tsx[26-38]
Treat a zero-length `templates` array the same as a failed load (call `setTemplatesFailed(true)` instead of `setTemplates([])`), or add an explicit empty-state message in TemplatePicker.

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


Grey Divider

Context
Review mode: 🧠 Deep: This is a bug-dense behavioral change spanning client signup state/persistence, API validation and reservation ownership/lifecycle, shared catalogs, and payment activation paths, with many independent edit sites where redundant review could catch subtle defects.

Grey Divider

Tip of the day
💡 Did you know, you can group findings by type and pick your Finding display, from Minimal to Full

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +280 to +283
localStorage.setItem(
customizeDraftKey(tenantUsername),
JSON.stringify({ styleTemplate, accent, fontPreset, title, description })
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

1. Draft lacks grace-window expiry 📎 Requirement gap ⛨ Security

The new customize draft is persisted in localStorage per name but has no timestamp/TTL and is only
cleared on success, so draft/customization data can persist indefinitely past the unpaid-reservation
grace window. This violates the requirement to discard draft/customization configuration after the
grace period.
Agent Prompt
## Issue description
`localStorage` customize drafts are saved without any expiry metadata and therefore can persist indefinitely, even after an unpaid reservation should have expired.

## Issue Context
PR adds per-name draft persistence via `customizeDraftKey()` + `localStorage.setItem(...)` and only removes it on `step === "success"`. Compliance requires draft/customization configuration to be discarded after the unpaid-reservation grace window (proposed 7 days).

## Fix Focus Areas
- apps/web/src/features/hosting-signup/hosting-signup.tsx[65-75]
- apps/web/src/features/hosting-signup/hosting-signup.tsx[276-285]
- apps/web/src/features/hosting-signup/hosting-signup.tsx[363-369]

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

Comment on lines +213 to +215
const draft = readCustomizeDraft(tenantUsername);
if (draft) {
if (draft.styleTemplate !== undefined) setStyleTemplate(draft.styleTemplate);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

2. Appearance leaks across names 🐞 Bug ≡ Correctness

When the user goes Back and changes the tenant name, goCustomize only updates appearance state if
the new name has a draft; otherwise the previous name's template, accent, and font remain selected.
Those retained values are then persisted and submitted for the new tenant, violating the per-name
draft boundary.
Agent Prompt
## Issue description
Appearance state from one tenant name survives when navigating back and selecting another name with no saved draft.

## Issue Context
Initialize every per-name customization field to its default before overlaying the selected name's draft. Preserve explicit `null` values and also reset `accentInput` consistently.

## Fix Focus Areas
- apps/web/src/features/hosting-signup/hosting-signup.tsx[181-226]
- apps/web/src/features/hosting-signup/hosting-signup.tsx[276-285]
- apps/web/src/features/hosting-signup/hosting-signup.tsx[300-307]

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

Comment on lines +365 to +367
if (step !== "success" || !tenantUsername) return;
try {
localStorage.removeItem(customizeDraftKey(tenantUsername));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

3. Reload clears wrong draft 🐞 Bug ≡ Correctness

Pending-payment recovery polls the tenant stored in session storage but advances to success without
synchronizing tenantUsername; the new cleanup deletes the draft keyed by that unchanged state. If
a logged-in account paid for a different personal blog, recovery retains the paid blog's stale draft
and may delete the payer's unrelated draft.
Agent Prompt
## Issue description
Success cleanup uses the form's current tenant name rather than the tenant recovered from the pending-payment marker.

## Issue Context
Keep the successfully activated tenant as the cleanup source of truth, including the reload recovery path. Add coverage where the active account and pending tenant differ and both have local drafts.

## Fix Focus Areas
- apps/web/src/features/hosting-signup/hosting-signup.tsx[363-369]
- apps/web/src/features/hosting-signup/hosting-signup.tsx[556-608]

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

Comment on lines 236 to +239
ON CONFLICT (username) DO UPDATE
SET owner = CASE WHEN tenants.subscription_status = 'abandoned'
THEN EXCLUDED.owner ELSE tenants.owner END,
config = CASE WHEN tenants.subscription_status = 'abandoned'
THEN EXCLUDED.config ELSE tenants.config END,
config = EXCLUDED.config,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

4. Config overwrite discards concurrent edits on refresh 🐞 Bug ≡ Correctness

The upsert for same-owner inactive reservations now unconditionally sets `tenants.config =
EXCLUDED.config, so any newer configuration changes saved to the tenant (e.g., via PATCH
/v1/tenants/:username` / Configuration Editor or from another tab/device) can be silently
overwritten by an older or default client payload. This is especially risky in the ?resume= flow,
which calls goPayment from fresh/empty appearance state without restoring the saved draft, causing
an activated site to differ from what the user previously customized.
Agent Prompt
## Issue description
The `POST /v1/tenants` create upsert for same-owner `inactive` reservations now always replaces the stored `tenants.config` with `EXCLUDED.config`, which can discard newer configuration changes (e.g., made via `PATCH /v1/tenants/:username` / Configuration Editor or another tab/device) and can also overwrite saved customization during `?resume=` because the resume path submits from fresh/empty appearance state.

## Issue Context
The customize step wants the latest signup submission’s look to win, but `buildConfig` builds from defaults plus the client’s flat overrides rather than from the currently stored tenant config. Separately, the `?resume=` flow bypasses the customize step and its local draft restoration by calling `goPayment` directly, so resuming before payment can submit default/empty appearance values and—combined with the unconditional upsert—silently reset the stored customization, resulting in the activated site not matching what the user saved.

## Fix Focus Areas
- apps/self-hosted/hosting/api/src/services/tenant-service.ts[209-258]
- apps/self-hosted/hosting/api/src/services/tenant-service.ts[1178-1200]
- apps/self-hosted/hosting/api/src/services/tenant-service.ts[223-249]
- apps/web/src/features/hosting-signup/hosting-signup.tsx[300-315]
- apps/web/src/features/hosting-signup/hosting-signup.tsx[371-415]

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

Comment on lines +300 to +312
const config = {
theme: "system" as const,
title: title.trim() || undefined,
description: description.trim() || undefined,
styleTemplate: styleTemplate ?? undefined,
accent: accent ?? undefined,
fontPreset: fontPreset ?? undefined,
...(isCommunity ? { type: "community" as const, communityId: uname } : {})
};
const payload = JSON.stringify(config);
if (createdForRef.current?.name !== uname || createdForRef.current?.payload !== payload) {
const res = await hostingApi.createTenant(uname, owner, config);
createdForRef.current = { name: uname, payload };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

5. Createdforref dedup key omits owner 🐞 Bug ≡ Correctness

In goPayment, the guard that decides whether to re-send createTenant compares only `{name,
payload}, but payload is derived from a config object that never includes owner`; for a
community signup owner is computed separately from activeUser?.username and passed directly to
createTenant. If the active user changes (e.g., logout/login as a different admin) while the
community id and the rest of the config stay the same, the stale createdForRef entry prevents a
required re-creation with the new owner, so the reservation is not re-associated and the server may
reject the next mutating request as belonging to a different owner.
Agent Prompt
## Issue description
`createdForRef` is used to decide whether `createTenant` needs to be re-sent before moving to the payment step. It compares only the tenant name and a JSON-serialized config that does not include `owner`, even though `owner` is passed to `createTenant` and can change independently of the config for a community signup.

## Issue Context
For a community instance, `owner` is derived from `activeUser?.username`, separate from the `config` object. If the active user changes between two calls to `goPayment` while the community id and rest of config are unchanged, the guard incorrectly treats the reservation as already up to date and skips re-creating it with the new owner.

## Fix Focus Areas
- apps/web/src/features/hosting-signup/hosting-signup.tsx[292-361]

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

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.

Hosting signup: customize step before payment

1 participant