Skip to content

feat: release Drops Studio production platform - #13

Merged
svg8bit merged 3 commits into
mainfrom
codex/drops-production-enablement
Jul 31, 2026
Merged

feat: release Drops Studio production platform#13
svg8bit merged 3 commits into
mainfrom
codex/drops-production-enablement

Conversation

@svg8bit

@svg8bit svg8bit commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Outcome

Releases the complete v0/Replit-like Drops Studio surface and production capability control plane while preserving legacy projects, all 12 crypto recipes, /p/{slug}, ZIP export, BYOK providers, DropsTab and Drops Bot/Telegram flows.

Production platform

  • real guest/member Project V2 persistence and reopen flow
  • real named Node 24 Vercel Sandbox build, tests, preview and cleanup
  • Neon-backed transactional project data with optimistic revisions
  • private Blob organization storage and persisted provider health receipts
  • honest capability states for external GitHub App, OIDC, realtime and deployment credentials
  • reference-matched responsive homepage and embedded Project Studio
  • production health cron and bounded live provider verification

Verification

  • npm run lint — pass
  • npm run typecheck — pass
  • npm run guardrails:ui — pass
  • npm run test:unit — 709 total, 707 pass, 2 opt-in skips, 0 fail
  • npm run build:vercel — pass, 22/22 pages
  • npm run build — pass (Cloudflare-compatible Vinext)
  • npm run test:e2e:prepared — 201 total, 161 pass, 40 intentional skips, 0 fail
  • Storybook interaction — 47 pass
  • Storybook visual — 42 pass
  • application visual — 6 pass
  • Lighthouse — all budgets pass over three runs
  • CodeRabbit full review — 7 findings fixed and targeted validation passed

Release notes

Production aliases are updated only after merge and a fresh deployment from the verified origin/main HEAD. External credentials remain disabled and honestly labeled until their owners configure them.

Summary by CodeRabbit

  • New Features

    • Added durable project storage with private cloud and managed database options.
    • Added platform health monitoring and capability status updates.
    • Added separate project-sync availability for guest and member workflows.
    • Added Build and Plan actions with progress feedback in Drops Studio.
    • Added a redesigned studio preview with navigation, toolbar, status bar, and embedded previews.
  • Improvements

    • Improved responsive builder layouts, select controls, link behavior, and access handling.
    • Added support for trusted proxied requests and more reliable project saving.
  • Bug Fixes

    • Prevented layout overflow across key desktop breakpoints.

@vercel

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
drops-studio Ready Ready Preview Jul 31, 2026 7:38am

Request Review

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

ℹ️ 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 +168 to +173
return working(
backendKind === "neon-postgres"
? "transactional-neon-postgres"
: "transactional-private-blob-cas",
"Capability-scoped project data passed a real create, read, optimistic update and isolated cleanup cycle.",
["project-data-create-live", "project-data-cas-live", "project-data-cleanup-live"],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Require capability signing before reporting project data healthy

When durable Blob or Postgres storage is configured but PROJECT_DATA_CAPABILITY_SECRET is absent, this direct backend exercise still emits a working receipt, and platformCapabilitySnapshot() prioritizes that receipt over configuration readiness. However, app/api/project-data/route.ts rejects every request without the signing secret, so the public platform reports working project data while generated apps can only receive 503 responses. Include capability creation and route-level authorization in this health check before publishing readiness.

AGENTS.md reference: AGENTS.md:L9-L10

Useful? React with 👍 / 👎.

Comment on lines +120 to +121
} finally {
await cleanupSandbox?.().catch(() => undefined);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Treat Sandbox deletion failure as a failed health check

If the Node command succeeds but sandbox.delete() rejects, this catch suppresses the failure after the function has already selected the working result. The persisted receipt then includes sandbox-delete-live and states that cleanup succeeded even though the persistent health Sandbox remains allocated, potentially leaking resources on every check. Cleanup failure must make the Sandbox check unavailable rather than being discarded.

AGENTS.md reference: AGENTS.md:L109-L109

Useful? React with 👍 / 👎.

Comment on lines +196 to +200
return status.status === "working"
? working(
"neon-postgres-live",
"The Vercel-native Neon Postgres provider returned a successful bounded health query.",
["neon-marketplace-resource", "postgres-select-live"],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Wire Postgres into managed operations before marking it working

Whenever SELECT 1 succeeds, this receipt causes the public managed-backend capability to be labeled working, but the new createNeonManagedPlatformDriver is referenced only by this health check; the actual managed platform still constructs InMemoryManagedData, and no production CRUD surface uses this driver. A reachable database therefore appears to be a completed managed relational backend even though product operations are not persisted there.

AGENTS.md reference: AGENTS.md:L7-L10

Useful? React with 👍 / 👎.

Comment on lines +57 to +64
const controller = new AbortController();
let timer: ReturnType<typeof setTimeout> | undefined;
try {
const value = await Promise.race([
operation(),
new Promise<never>((_resolve, reject) => {
timer = setTimeout(() => {
controller.abort();

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 Cancel provider operations when their health deadline expires

When an external operation exceeds the deadline, the timeout only aborts an otherwise-unused controller; operation() receives no signal and continues running after Promise.race rejects. Hung Sandbox, Blob, or database work can therefore keep creating, writing, or deleting resources after the check reports unavailable, and subsequent scheduled checks may overlap it. Pass an abort signal through each operation or otherwise ensure timed-out work is terminated.

AGENTS.md reference: AGENTS.md:L109-L109

Useful? React with 👍 / 👎.

Comment on lines +518 to +521
font-size: 12px;
font-weight: 720;
gap: 6px;
min-height: 40px;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Enforce the preview control-size contract

On every home viewport where the new Share and Build project controls are visible, these declarations render their text at 12px and cap their height at 40px. e2e/contracts/ui-standards.spec.ts explicitly audits visible buttons for at least 14px text and 44px targets, so this preview cannot pass the repository's release UI gate; the copilot control also uses 12px text. Raise the new controls to the documented minimums.

AGENTS.md reference: AGENTS.md:L25-L28

Useful? React with 👍 / 👎.

Comment on lines +68 to +72
<div className="field-select-wrap">
<select
aria-label={ariaLabel}
className="field-select"
value={value}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep the Radix select until a covered Base UI migration

This replaces the existing Radix Select with a raw native <select>, rather than the required Base UI 1.6 primitive, and therefore drops the current popup, positioning, and interaction behavior outside a bounded Base UI migration. Preserve the current Radix component or complete the mandated Base UI migration with keyboard, Axe, and visual coverage.

AGENTS.md reference: AGENTS.md:L18-L19

Useful? React with 👍 / 👎.

signal: AbortSignal.timeout(HEALTH_TIMEOUT_MS),
},
);
if (!repositories.ok) throw new Error("repository receipt failed");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Verify an allowlisted repository in the GitHub health check

If the installation token is valid but the installation exposes no repositories—or only repositories different from GITHUB_APP_ALLOWED_REPOSITORIES—this endpoint still returns 200 and the check is marked working because the response body is never inspected. The actual integration route permits platform credentials only for the configured allowlist, so every such operation can then fail at GitHub despite the public capability claiming a healthy installation. Validate that at least one configured allowlisted repository is present in the installation response.

AGENTS.md reference: AGENTS.md:L10-L10

Useful? React with 👍 / 👎.

@@ -0,0 +1,128 @@
const RECEIPT_PATH = "drops-studio/platform-health/v1/latest.json";

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 Namespace persisted health receipts by environment

When preview and production deployments share the same private Blob store, both write this single latest.json object. A preview or local operator check can therefore overwrite the production receipt; production then rejects it because its environment differs and reports all provider capabilities unavailable until the next production check, while a production run similarly invalidates preview evidence. Include the deployment environment in the receipt path.

AGENTS.md reference: AGENTS.md:L10-L10

Useful? React with 👍 / 👎.

Comment thread app/api/access/route.ts
Comment on lines +68 to +69
projectSyncAvailable: context.configured
&& memberProjectSyncReadiness(readinessEnvironment),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Advertise guest sync for every supported Blob auth mode

In a Vercel deployment with BLOB_STORE_ID and the platform-provided VERCEL=1 marker but no literal VERCEL_OIDC_TOKEN environment variable, db/project-v2-snapshots.ts considers durable Project V2 storage configured and can service the route, while memberProjectSyncReadiness() returns false. This newly added guest response consequently tells the client that sync is unavailable, so guest builds never upload or reopen their otherwise supported private snapshots. Derive this flag from the same storage readiness predicate used by the Project V2 route.

Useful? React with 👍 / 👎.

Comment on lines +17 to +20
const secrets = [
process.env.CRON_SECRET?.trim(),
process.env.DROPS_PLATFORM_HEALTH_OPERATOR_SECRET?.trim(),
].filter((value): value is string => Boolean(value));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reject weak health-trigger secrets in production

In production, any nonempty CRON_SECRET or DROPS_PLATFORM_HEALTH_OPERATOR_SECRET is accepted, whereas the existing Sandbox cleanup route rejects secrets shorter than 32 characters. A weak operator secret can be guessed against this unaudited endpoint to repeatedly trigger costly Sandbox creation, Blob mutations, provider requests, and GitHub token issuance. Enforce the same production secret-strength boundary before authorizing the health run.

AGENTS.md reference: AGENTS.md:L109-L109

Useful? React with 👍 / 👎.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9ccbb6c4-d915-4964-b422-0be23a7ac3fa

📥 Commits

Reviewing files that changed from the base of the PR and between 9d3a1ba and 639799e.

⛔ Files ignored due to path filters (89)
  • docs/design/current-home-actual.png is excluded by !**/*.png
  • e2e/visual/home.spec.ts-snapshots/home-builder-chromium-1024-linux-system.png is excluded by !**/*.png
  • e2e/visual/home.spec.ts-snapshots/home-builder-chromium-1440-linux-system.png is excluded by !**/*.png
  • e2e/visual/home.spec.ts-snapshots/home-builder-chromium-390-linux-system.png is excluded by !**/*.png
  • package-lock.json is excluded by !**/package-lock.json
  • storybook-e2e/visual.spec.ts-snapshots/action-engine-desktop-chromium-1024-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/action-engine-desktop-chromium-1024-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/action-engine-desktop-chromium-1440-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/action-engine-desktop-chromium-1440-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/action-engine-desktop-chromium-390-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/action-engine-desktop-chromium-390-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/alpha-channel-disconnected-chromium-1024-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/alpha-channel-disconnected-chromium-1024-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/alpha-channel-disconnected-chromium-1440-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/alpha-channel-disconnected-chromium-1440-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/alpha-channel-disconnected-chromium-390-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/alpha-channel-disconnected-chromium-390-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-aggregator-connected-chromium-1024-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-aggregator-connected-chromium-1024-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-aggregator-connected-chromium-1440-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-aggregator-connected-chromium-1440-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-aggregator-connected-chromium-390-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-aggregator-connected-chromium-390-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-game-desktop-chromium-1024-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-game-desktop-chromium-1024-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-game-desktop-chromium-1440-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-game-desktop-chromium-1440-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-game-desktop-chromium-390-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-game-desktop-chromium-390-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-product-hunt-empty-chromium-1024-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-product-hunt-empty-chromium-1024-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-product-hunt-empty-chromium-1440-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-product-hunt-empty-chromium-1440-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-product-hunt-empty-chromium-390-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-product-hunt-empty-chromium-390-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-radio-playing-chromium-1024-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-radio-playing-chromium-1024-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-radio-playing-chromium-1440-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-radio-playing-chromium-1440-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-radio-playing-chromium-390-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-radio-playing-chromium-390-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-siri-mobile-chromium-1024-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-siri-mobile-chromium-1024-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-siri-mobile-chromium-1440-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-siri-mobile-chromium-1440-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-siri-mobile-chromium-390-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-siri-mobile-chromium-390-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/loading-product-plan-chromium-1024-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/loading-product-plan-chromium-1024-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/loading-product-plan-chromium-1440-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/loading-product-plan-chromium-1440-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/loading-product-plan-chromium-390-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/loading-product-plan-chromium-390-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/morning-alpha-data-error-chromium-1024-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/morning-alpha-data-error-chromium-1024-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/morning-alpha-data-error-chromium-1440-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/morning-alpha-data-error-chromium-1440-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/morning-alpha-data-error-chromium-390-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/morning-alpha-data-error-chromium-390-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/morning-alpha-populated-chromium-1024-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/morning-alpha-populated-chromium-1024-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/morning-alpha-populated-chromium-1440-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/morning-alpha-populated-chromium-1440-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/morning-alpha-populated-chromium-390-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/morning-alpha-populated-chromium-390-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/personal-companion-mobile-chromium-1024-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/personal-companion-mobile-chromium-1024-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/personal-companion-mobile-chromium-1440-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/personal-companion-mobile-chromium-1440-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/personal-companion-mobile-chromium-390-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/personal-companion-mobile-chromium-390-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/portfolio-tamagotchi-empty-chromium-1024-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/portfolio-tamagotchi-empty-chromium-1024-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/portfolio-tamagotchi-empty-chromium-1440-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/portfolio-tamagotchi-empty-chromium-1440-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/portfolio-tamagotchi-empty-chromium-390-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/portfolio-tamagotchi-empty-chromium-390-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/prediction-impact-desktop-chromium-1024-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/prediction-impact-desktop-chromium-1024-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/prediction-impact-desktop-chromium-1440-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/prediction-impact-desktop-chromium-1440-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/prediction-impact-desktop-chromium-390-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/prediction-impact-desktop-chromium-390-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/smart-money-copy-empty-chromium-1024-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/smart-money-copy-empty-chromium-1024-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/smart-money-copy-empty-chromium-1440-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/smart-money-copy-empty-chromium-1440-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/smart-money-copy-empty-chromium-390-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/smart-money-copy-empty-chromium-390-linux.png is excluded by !**/*.png
📒 Files selected for processing (44)
  • .env.example
  • app/api/access/route.ts
  • app/api/platform/capabilities/route.ts
  • app/api/platform/health/route.ts
  • app/api/project-data/route.ts
  • app/api/projects/v2/route.ts
  • app/platform/page.tsx
  • app/styles/drops-studio.builder.css
  • app/styles/drops-studio.previews.css
  • app/styles/drops-studio.responsive.css
  • app/styles/drops-studio.setup.css
  • app/styles/drops-studio.shell.css
  • components/drops-studio-setup.tsx
  • components/drops-studio.tsx
  • components/dropsbot-webhook-connection.tsx
  • components/platform/integration-catalog.tsx
  • components/platform/organization-console.tsx
  • components/platform/platform-overview.tsx
  • components/platform/platform-shell.tsx
  • components/platform/project-library.tsx
  • components/platform/template-catalog.tsx
  • components/preview-canvas.tsx
  • components/project-studio.tsx
  • components/project-v2-agent-intelligence.tsx
  • components/project-v2-studio-surface.tsx
  • design-qa.md
  • e2e/contracts/home-builder-p1.spec.ts
  • lib/access-tier.ts
  • lib/managed-platform/index.ts
  • lib/managed-platform/postgres-driver.ts
  • lib/platform-capabilities.ts
  • lib/platform-health-receipts.ts
  • lib/platform-provider-health.ts
  • lib/project-data/durable-backend.ts
  • lib/project-data/index.ts
  • lib/project-data/types.ts
  • package.json
  • scripts/verify-platform-providers.mjs
  • tests/access-tier.test.mjs
  • tests/platform-capabilities.test.mjs
  • tests/platform-public-surfaces.test.mjs
  • tests/project-data-durable-backend.test.mjs
  • tests/vercel-sandbox-runtime-cleanup.test.mjs
  • vercel.json

📝 Walkthrough

Walkthrough

The change adds private Blob and Neon project storage, provider health receipts, health-aware capability reporting, project-sync access handling, and a redesigned responsive Drops Studio builder and preview.

Changes

Platform storage and health

Layer / File(s) Summary
Durable project storage and request integration
lib/project-data/*, lib/managed-platform/*, app/api/project-data/route.ts, app/api/projects/v2/route.ts, tests/project-data-durable-backend.test.mjs
Project data supports private Vercel Blob and Neon Postgres backends with revision checks, tombstones, asynchronous initialization, and forwarded-origin validation.
Provider health receipts and capability reporting
lib/platform-health-receipts.ts, lib/platform-provider-health.ts, lib/platform-capabilities.ts, app/api/platform/health/route.ts, app/api/platform/capabilities/route.ts, app/platform/page.tsx, scripts/verify-platform-providers.mjs, vercel.json, tests/platform-capabilities.test.mjs
Provider checks create validated receipts. The health route authenticates requests and persists results. Platform surfaces consume valid health receipts.
Project-sync access and Project V2 persistence
app/api/access/route.ts, lib/access-tier.ts, components/drops-studio.tsx, components/project-studio.tsx, components/project-v2-studio-surface.tsx, tests/access-tier.test.mjs
Access metadata exposes project-sync readiness. Guests can use actor-scoped storage when available. Project V2 loading, saving, conflicts, and agent editing use dedicated sync availability.
Studio interface and responsive layout
components/preview-canvas.tsx, components/drops-studio-setup.tsx, app/styles/drops-studio.*.css, components/platform/*, e2e/contracts/home-builder-p1.spec.ts
The builder gains Build and Plan controls. The preview gains a Drops Studio frame. Link buttons use non-native rendering. Responsive layout rules and end-to-end breakpoint checks were added.

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

Sequence Diagram(s)

sequenceDiagram
  participant HealthRoute as /api/platform/health
  participant HealthRunner as runPlatformProviderHealthChecks
  participant ReceiptStore as platform health receipt storage
  participant CapabilityLoader as platformCapabilitySnapshotWithHealth
  HealthRoute->>HealthRunner: run provider health checks
  HealthRunner->>ReceiptStore: write validated receipt
  CapabilityLoader->>ReceiptStore: read private receipt
  CapabilityLoader-->>CapabilityLoader: build capability snapshot
Loading

Possibly related PRs

  • svg8bit/drops-studio#3: Introduced the Project V2 builder that this change extends with project sync, durable storage, and Studio integration.
  • svg8bit/drops-studio#7: Established platform capability infrastructure extended here with provider health checks and health-aware capability reporting.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: releasing the Drops Studio production platform and its supporting capability control plane.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Fix failing CI checks
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/drops-production-enablement

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

@svg8bit
svg8bit merged commit 2fb1310 into main Jul 31, 2026
2 of 4 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 20

Caution

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

⚠️ Outside diff range comments (1)
app/api/project-data/route.ts (1)

103-127: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Two routes now duplicate the forwarded-origin check with different behavior. Both routes derive a visible origin from host and x-forwarded-proto, but only the project-data route normalizes it, and neither restricts host to expected values. Extract one shared helper and give it an allowlist of accepted hosts.

  • app/api/project-data/route.ts#L103-L127: move this implementation into a shared module, for example lib/request-origin.ts, and validate the derived host against a configured allowlist before accepting it.
  • app/api/projects/v2/route.ts#L83-L108: import the shared helper and delete the local copy, which also removes the default-port normalization defect.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/project-data/route.ts` around lines 103 - 127, Extract the
requireSameOrigin implementation from app/api/project-data/route.ts lines
103-127 into a shared helper module, such as lib/request-origin.ts, accepting a
configured allowlist of valid hosts and validating the derived host before
accepting the origin. Update app/api/project-data/route.ts lines 103-127 to
import and use the shared helper. In app/api/projects/v2/route.ts lines 83-108,
remove the duplicated local origin-checking logic and import the same helper so
both routes share normalized forwarded-origin handling.
🧹 Nitpick comments (9)
lib/project-data/durable-backend.ts (1)

89-99: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Use a state-based signal for first-write conflicts.

@vercel/blob provides BlobPreconditionFailedError for ifMatch mismatches, but first-write pathname collisions remain generic BlobError values. Re-read the envelope after a failed first write. If it exists, return conflict; otherwise return storage_unavailable. This avoids relying on provider message text.

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

In `@lib/project-data/durable-backend.ts` around lines 89 - 99, Update the
first-write conflict handling around isBlobCompareAndSwapConflict to stop
matching BlobError.message text. After a failed write without a current ETag,
re-read the envelope and return conflict when it now exists; return
storage_unavailable when it does not. Preserve BlobPreconditionFailedError
handling for ifMatch mismatches.
tests/access-tier.test.mjs (1)

195-205: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add an assertion for the nested account.projectSync value.

The test locks the top-level projectSync and the note text. It does not lock guestWithPrivateStorage.account.projectSync, which stays false by design in lib/access-tier.ts at Line 385. Assert that value so the intended difference between the two fields is protected.

💚 Proposed addition
   assert.equal(guestWithPrivateStorage.projectSync, true);
   assert.equal(guestWithPrivateStorage.account.connected, false);
+  assert.equal(guestWithPrivateStorage.account.projectSync, false);
   assert.match(guestWithPrivateStorage.account.note, /Guest projects use actor-owned private storage/);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/access-tier.test.mjs` around lines 195 - 205, Add an assertion for
guestWithPrivateStorage.account.projectSync in the accessMetadata test,
expecting false, while preserving the existing top-level projectSync assertion
and account note checks.
lib/platform-provider-health.ts (2)

115-119: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add observability to the empty catch blocks.

Every check converts any failure into an unavailable receipt without recording the cause. When a provider degrades, operators see the status but not the reason. Log the error at warning level with the check id and without secret values.

Also applies to: 245-254, 295-300, 367-372, 405-410, 440-445

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

In `@lib/platform-provider-health.ts` around lines 115 - 119, Update the catch
blocks in the health-check functions, including the block returning the
"vercel-sandbox-health-failed" receipt and the additional listed checks, to
accept the caught error and emit a warning-level log containing the check id and
sanitized error details. Preserve each existing unavailable receipt and avoid
logging secret values.

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

Guard the failure-path cleanup.

Line 177 awaits cleanupProject?.() without a catch. Every other failure path in this module uses best-effort cleanup. runPlatformProviderHealthChecks runs all checks with Promise.all, so a rejection here would fail the whole health run instead of returning one unavailable check.

♻️ Proposed change
   } catch {
-    await cleanupProject?.();
+    await cleanupProject?.().catch(() => undefined);
     return unavailable(
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/platform-provider-health.ts` around lines 176 - 182, Update the catch
branch in runPlatformProviderHealthChecks to make cleanupProject best-effort by
handling and suppressing any rejection from cleanupProject?.(). Ensure cleanup
failure does not replace the existing unavailable("project-data-health-failed",
...) result or reject the overall Promise.all health run.
lib/access-tier.ts (1)

381-388: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Two fields named projectSync carry different meanings.

For a guest with private storage configured, access.projectSync is true at Line 360 while access.account.projectSync is false at Line 385. The note at Lines 386-388 states that guest projects already use private storage. A consumer that reads the nested field concludes that sync is off; a consumer that reads the top-level field concludes that sync is on. Rename the nested field to express the account-bound meaning, for example accountProjectSync, or document the distinction at the type level.

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

In `@lib/access-tier.ts` around lines 381 - 388, Rename the nested account-level
projectSync field in the access object returned by the relevant access-tier
logic to accountProjectSync, and update its type and all consumers accordingly.
Preserve the top-level projectSync field for guest private-storage availability
while making the nested field explicitly represent account-bound
synchronization.
components/drops-studio.tsx (1)

1708-1717: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use a ref instead of an aria-label selector to focus the prompt.

The EDIT PLAN branch locates the textarea with document.querySelector('textarea[aria-label="Describe your crypto project"]'). This ties runtime behavior to an accessible-name string. If that label is reworded or localized, the query returns null and the focus is lost without any error. The component already uses refs for carouselRef and guestIdRef.

♻️ Proposed refactor
   const carouselRef = useRef<HTMLDivElement>(null);
+  const promptRef = useRef<HTMLTextAreaElement>(null);
     if (label === "EDIT PLAN") {
       if (!prompt.trim()) {
-        document
-          .querySelector<HTMLTextAreaElement>(
-            'textarea[aria-label="Describe your crypto project"]',
-          )
-          ?.focus();
+        promptRef.current?.focus();
         setToast("Describe your product, then create an editable plan.");
         return;
       }
                 <textarea
+                  ref={promptRef}
                   value={prompt}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/drops-studio.tsx` around lines 1708 - 1717, Replace the
document.querySelector focus lookup in the EDIT PLAN branch with a dedicated
textarea ref, following the existing carouselRef and guestIdRef patterns. Attach
the ref to the prompt textarea and focus it through the ref when prompt.trim()
is empty, without changing the toast or return behavior.
tests/platform-capabilities.test.mjs (1)

58-89: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider covering the DROPS_MANAGED_POSTGRES_URL alias.

This test exercises DROPS_MANAGED_DATABASE_URL. The implementation at lib/platform-capabilities.ts line 56 also accepts DROPS_MANAGED_POSTGRES_URL as an alternative marker. Add one case for that alias so a future removal of either branch fails a test.

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

In `@tests/platform-capabilities.test.mjs` around lines 58 - 89, Extend the fresh
production provider receipt coverage in the test around
platformCapabilitySnapshot to include a case using DROPS_MANAGED_POSTGRES_URL
instead of DROPS_MANAGED_DATABASE_URL. Assert the same matching capability
upgrade and receipt redaction behavior so both accepted provider-marker branches
remain covered.
lib/platform-capabilities.ts (2)

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

Bound the per-request receipt read, or cache it in process.

platformCapabilitySnapshotWithHealth awaits readPlatformHealthReceipt() on every call. Both app/platform/page.tsx and app/api/platform/capabilities/route.ts are force-dynamic, so each page render and each API request performs an uncached private Blob read. readPlatformHealthReceipt catches errors but does not bound latency, so a slow Blob response delays the response.

The receipt written by runPlatformProviderHealthChecks is valid for 36 hours, so a per-request read adds no freshness. Add a short in-process TTL cache, or pass an AbortSignal.timeout(...) through the read.

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

In `@lib/platform-capabilities.ts` around lines 220 - 230, The
platformCapabilitySnapshotWithHealth flow must avoid an unbounded private Blob
read on every request. Add a short in-process TTL cache around
readPlatformHealthReceipt, reusing cached receipt data within the TTL while
preserving refresh and error behavior; alternatively, propagate an
AbortSignal.timeout through the receipt-read path. Keep
platformCapabilitySnapshot’s existing inputs and health snapshot behavior
unchanged.

113-209: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider extracting the repeated receipt-resolution pattern.

The same shape health?.field ?? (flag ? working : missing) repeats for nine capabilities. Line 125 and line 126 also nest three ternaries on one line. A small helper would remove the duplication and make the precedence explicit.

One behavioral detail is worth confirming: when a receipt reports status: "unavailable", mode, detail, and evidence still come from that receipt while state falls back to the credential-derived value. For managed-backend and collaboration, that produces a failed-check detail next to state: "working-local-test". Confirm this pairing is intended.

♻️ Sketch of the helper
+  const resolve = (
+    health: PlatformProviderHealthCheck | undefined,
+    configuredState: PlatformCapabilityState,
+    unconfiguredState: PlatformCapabilityState,
+    configured: boolean,
+  ) => ({
+    state: health?.status === "working"
+      ? ("working" as const)
+      : configured ? configuredState : unconfiguredState,
+  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/platform-capabilities.ts` around lines 113 - 209, Extract the repeated
receipt-resolution logic in the platform capability definitions into a small
helper that makes receipt precedence explicit for mode, detail, and evidence
while deriving state from receipt status or the credential/local-test fallback.
Apply it across all nine capabilities, including the nested state/mode
expressions for project-data and managed-backend. Preserve the behavior where an
unavailable receipt supplies its own mode, detail, and evidence while state uses
the fallback value, including the managed-backend and collaboration local-test
pairings.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/api/access/route.ts`:
- Around line 68-69: Update the projectSyncAvailable calculation in the
/api/access handler to avoid treating a merely present request OIDC header as
readiness; derive projectSync only from verified provider-backed readiness or a
validated token that Project V2 can use as storage credentials. Keep the
existing configured check, and ensure BLOB_STORE_ID-only requests cannot return
projectSync: true.

In `@app/api/platform/health/route.ts`:
- Around line 28-41: Bound the side effects in GET by guarding
runPlatformProviderHealthChecks with a shared single-flight lock and minimum-run
interval. When a run is active or the interval has not elapsed, return the last
persisted health receipt without starting provider checks; otherwise execute
once, persist the receipt, and release the guard reliably. Ensure POST,
currently aliased to GET, uses the same guard and cannot trigger a concurrent
duplicate run.

In `@app/api/project-data/route.ts`:
- Around line 30-50: Update the backend() initialization flow so backendPromise
is reset to null when the cached initialization promise rejects, allowing later
requests to retry createDurableProjectDataBackend(). Preserve successful caching
and globalThis.__DROPS_STUDIO_PROJECT_DATA_BACKEND_V2__ assignment, and mirror
the failure-cache clearing behavior used by
PostgresProjectDataBackend.#ensureSchema.

In `@app/api/projects/v2/route.ts`:
- Around line 90-104: Normalize visibleOrigin using URL origin canonicalization
before comparing it with parsedOrigin, matching the established approach in the
project-data route. Update the origin validation block around parsedOrigin so
default ports and equivalent forwarded Host values compare consistently while
preserving the existing fallback to request.nextUrl.origin.

In `@app/styles/drops-studio.previews.css`:
- Around line 790-808: The `@media` rules for .landing-studio-copilot conflict
across the 1260px and 900px breakpoints. Update the responsive styles so the
copilot remains hidden throughout the intended tablet range, or restore it only
with an explicit min-width condition, while keeping the surrounding layout
behavior consistent.
- Around line 512-523: Update the .landing-studio-toolbar button rule so its
min-height is 44px instead of 40px, ensuring visible Share and Build controls
meet the required interactive target size.

In `@components/drops-studio.tsx`:
- Around line 1824-1839: Remove the aria-label attribute from the build button
in the runPrompt("build") control. Preserve the existing visible span text and
loading-state behavior so the button’s accessible name reflects “Build now,”
“Planning…,” or “Building…” as displayed.
- Around line 1605-1635: Update the `/api/access` save flow around
`applyAccessStatus`, `projectSyncAvailable`, and `saveMemberProjectToCloud` to
pass the fresh access payload into `applyAccessStatus` and use its returned sync
availability for both the member-save gate and toast selection. Also separate
the builder snapshot and member-project save results, or rename the combined
flag to explicitly reflect intentional coupling, so each save outcome is
represented accurately.

In `@components/preview-canvas.tsx`:
- Around line 311-316: Replace the native button elements for the Share and
Build project controls in the preview canvas, along with the editable-plan
controls around the referenced section, with the shared Base UI 1.6 Button
component. Preserve each control’s existing onClick behavior, labels, icons, and
primary styling by mapping them to the Button API.
- Around line 422-425: Update the landing studio status bar in PreviewCanvas so
the “DropsTab receipt verified” label is driven by validated
receipt/provider-health state from the server capability flow, not the
client-side dataMode value. Extend or reuse the appropriate PreviewCanvasProps
evidence field and render the verified label only when that evidence is present;
retain the sample-data label for non-verified data.

In `@design-qa.md`:
- Line 5: Update the release approval entry in design-qa.md to record the
results of npm audit --omit=dev --audit-level=high, npm run build:vercel, npm
run test:e2e:prepared, and npm run test:lighthouse:prepared for release
candidate 6e06b28311d5. Retain the approval only when all four required gates
pass.

In `@lib/managed-platform/postgres-driver.ts`:
- Around line 36-79: Update the Postgres client setup in transaction() to
configure an explicit connection timeout and per-statement timeout, and ensure
the transaction queries inherit that statement bound. Update health() to use the
same connection timeout configuration and bound its SELECT 1 AS ok health query
with an explicit timeout, preserving the existing validation and latency
behavior.
- Around line 40-62: Update the transaction implementation around Pool creation
so the pool is initialized once at module scope and reused across calls, rather
than instantiated inside each transaction. Remove the per-transaction pool.end()
cleanup while preserving client acquisition, transaction commit/rollback,
release, and operation behavior.

In `@lib/platform-provider-health.ts`:
- Around line 361-366: The provider health receipts claim operations that were
not executed. In lib/platform-provider-health.ts lines 361-366, either compare
the repositories returned by the preceding check with
GITHUB_APP_ALLOWED_REPOSITORIES before emitting github-app-installation-live, or
remove the bounded-scope wording and implied allowlist evidence. In lines
466-479, either perform a real audit append and checksummed restore probe before
emitting private-recovery-storage-live, or replace that token and “configured
and live” detail with wording explicitly derived from the managed-backend and
organizations checks.
- Around line 52-76: Update timed and its callers so the AbortController signal
is passed into operation and forwarded to each provider request, including
GitHub, Blob, Postgres, and fetch calls, allowing timed-out work to stop. In
sandboxHealth, assign cleanupSandbox before starting the command and ensure the
named sandbox is deleted if creation remains pending when the timeout aborts.
- Around line 330-360: Update the timed callback around the installation token
fetch and repository request to revoke the token via DELETE
https://api.github.com/installation/token in a finally block after successful
token creation. Use the token for the revocation request and ensure revocation
runs whether the repository health check succeeds or throws, while preserving
the existing success and failure behavior.

In `@lib/project-data/durable-backend.ts`:
- Around line 281-311: Make deleted projects recreatable in both durable
backends by removing records on deletion rather than persisting tombstones. In
lib/project-data/durable-backend.ts lines 281-311, update deleteProject to call
the typed BlobStorage.del member with the existing revision/precondition
handling. In lib/project-data/durable-backend.ts lines 411-440, replace the
tombstone UPDATE with a conditional DELETE from drops_project_data_snapshots
using project_key, project_id, and store_revision, preserving conflict behavior
for revision mismatches.
- Around line 23-39: Update the information_schema.columns probe in
POSTGRES_SCHEMA_MIGRATION to constrain table_schema to the current schema,
ensuring the conditional check and subsequent ALTER TABLE target the same
search-path table rather than a same-named table elsewhere.

In `@scripts/verify-platform-providers.mjs`:
- Around line 3-6: Update the invocation of runPlatformProviderHealthChecks to
require separate explicit environment-flag opt-ins for live provider checks and
receipt persistence. Replace the current opt-out includeSandbox logic with an
opt-in condition, and gate persist: true behind its own explicit persistence
flag so both values are false by default; preserve the existing health-check
call and receipt behavior when each corresponding flag is enabled.
- Line 1: Update the execution configuration for
scripts/verify-platform-providers.mjs so its TypeScript import works across the
declared Node >=22.13.0 range, using the required runtime flag or an
appropriately raised minimum version. Add a package.json npm script that invokes
verify-platform-providers.mjs.

---

Outside diff comments:
In `@app/api/project-data/route.ts`:
- Around line 103-127: Extract the requireSameOrigin implementation from
app/api/project-data/route.ts lines 103-127 into a shared helper module, such as
lib/request-origin.ts, accepting a configured allowlist of valid hosts and
validating the derived host before accepting the origin. Update
app/api/project-data/route.ts lines 103-127 to import and use the shared helper.
In app/api/projects/v2/route.ts lines 83-108, remove the duplicated local
origin-checking logic and import the same helper so both routes share normalized
forwarded-origin handling.

---

Nitpick comments:
In `@components/drops-studio.tsx`:
- Around line 1708-1717: Replace the document.querySelector focus lookup in the
EDIT PLAN branch with a dedicated textarea ref, following the existing
carouselRef and guestIdRef patterns. Attach the ref to the prompt textarea and
focus it through the ref when prompt.trim() is empty, without changing the toast
or return behavior.

In `@lib/access-tier.ts`:
- Around line 381-388: Rename the nested account-level projectSync field in the
access object returned by the relevant access-tier logic to accountProjectSync,
and update its type and all consumers accordingly. Preserve the top-level
projectSync field for guest private-storage availability while making the nested
field explicitly represent account-bound synchronization.

In `@lib/platform-capabilities.ts`:
- Around line 220-230: The platformCapabilitySnapshotWithHealth flow must avoid
an unbounded private Blob read on every request. Add a short in-process TTL
cache around readPlatformHealthReceipt, reusing cached receipt data within the
TTL while preserving refresh and error behavior; alternatively, propagate an
AbortSignal.timeout through the receipt-read path. Keep
platformCapabilitySnapshot’s existing inputs and health snapshot behavior
unchanged.
- Around line 113-209: Extract the repeated receipt-resolution logic in the
platform capability definitions into a small helper that makes receipt
precedence explicit for mode, detail, and evidence while deriving state from
receipt status or the credential/local-test fallback. Apply it across all nine
capabilities, including the nested state/mode expressions for project-data and
managed-backend. Preserve the behavior where an unavailable receipt supplies its
own mode, detail, and evidence while state uses the fallback value, including
the managed-backend and collaboration local-test pairings.

In `@lib/platform-provider-health.ts`:
- Around line 115-119: Update the catch blocks in the health-check functions,
including the block returning the "vercel-sandbox-health-failed" receipt and the
additional listed checks, to accept the caught error and emit a warning-level
log containing the check id and sanitized error details. Preserve each existing
unavailable receipt and avoid logging secret values.
- Around line 176-182: Update the catch branch in
runPlatformProviderHealthChecks to make cleanupProject best-effort by handling
and suppressing any rejection from cleanupProject?.(). Ensure cleanup failure
does not replace the existing unavailable("project-data-health-failed", ...)
result or reject the overall Promise.all health run.

In `@lib/project-data/durable-backend.ts`:
- Around line 89-99: Update the first-write conflict handling around
isBlobCompareAndSwapConflict to stop matching BlobError.message text. After a
failed write without a current ETag, re-read the envelope and return conflict
when it now exists; return storage_unavailable when it does not. Preserve
BlobPreconditionFailedError handling for ifMatch mismatches.

In `@tests/access-tier.test.mjs`:
- Around line 195-205: Add an assertion for
guestWithPrivateStorage.account.projectSync in the accessMetadata test,
expecting false, while preserving the existing top-level projectSync assertion
and account note checks.

In `@tests/platform-capabilities.test.mjs`:
- Around line 58-89: Extend the fresh production provider receipt coverage in
the test around platformCapabilitySnapshot to include a case using
DROPS_MANAGED_POSTGRES_URL instead of DROPS_MANAGED_DATABASE_URL. Assert the
same matching capability upgrade and receipt redaction behavior so both accepted
provider-marker branches remain covered.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3b5b7194-45c5-431c-9938-4c5bedcc6538

📥 Commits

Reviewing files that changed from the base of the PR and between 9d3a1ba and 639799e.

⛔ Files ignored due to path filters (89)
  • docs/design/current-home-actual.png is excluded by !**/*.png
  • e2e/visual/home.spec.ts-snapshots/home-builder-chromium-1024-linux-system.png is excluded by !**/*.png
  • e2e/visual/home.spec.ts-snapshots/home-builder-chromium-1440-linux-system.png is excluded by !**/*.png
  • e2e/visual/home.spec.ts-snapshots/home-builder-chromium-390-linux-system.png is excluded by !**/*.png
  • package-lock.json is excluded by !**/package-lock.json
  • storybook-e2e/visual.spec.ts-snapshots/action-engine-desktop-chromium-1024-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/action-engine-desktop-chromium-1024-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/action-engine-desktop-chromium-1440-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/action-engine-desktop-chromium-1440-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/action-engine-desktop-chromium-390-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/action-engine-desktop-chromium-390-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/alpha-channel-disconnected-chromium-1024-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/alpha-channel-disconnected-chromium-1024-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/alpha-channel-disconnected-chromium-1440-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/alpha-channel-disconnected-chromium-1440-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/alpha-channel-disconnected-chromium-390-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/alpha-channel-disconnected-chromium-390-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-aggregator-connected-chromium-1024-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-aggregator-connected-chromium-1024-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-aggregator-connected-chromium-1440-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-aggregator-connected-chromium-1440-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-aggregator-connected-chromium-390-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-aggregator-connected-chromium-390-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-game-desktop-chromium-1024-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-game-desktop-chromium-1024-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-game-desktop-chromium-1440-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-game-desktop-chromium-1440-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-game-desktop-chromium-390-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-game-desktop-chromium-390-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-product-hunt-empty-chromium-1024-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-product-hunt-empty-chromium-1024-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-product-hunt-empty-chromium-1440-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-product-hunt-empty-chromium-1440-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-product-hunt-empty-chromium-390-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-product-hunt-empty-chromium-390-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-radio-playing-chromium-1024-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-radio-playing-chromium-1024-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-radio-playing-chromium-1440-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-radio-playing-chromium-1440-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-radio-playing-chromium-390-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-radio-playing-chromium-390-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-siri-mobile-chromium-1024-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-siri-mobile-chromium-1024-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-siri-mobile-chromium-1440-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-siri-mobile-chromium-1440-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-siri-mobile-chromium-390-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/crypto-siri-mobile-chromium-390-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/loading-product-plan-chromium-1024-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/loading-product-plan-chromium-1024-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/loading-product-plan-chromium-1440-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/loading-product-plan-chromium-1440-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/loading-product-plan-chromium-390-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/loading-product-plan-chromium-390-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/morning-alpha-data-error-chromium-1024-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/morning-alpha-data-error-chromium-1024-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/morning-alpha-data-error-chromium-1440-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/morning-alpha-data-error-chromium-1440-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/morning-alpha-data-error-chromium-390-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/morning-alpha-data-error-chromium-390-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/morning-alpha-populated-chromium-1024-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/morning-alpha-populated-chromium-1024-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/morning-alpha-populated-chromium-1440-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/morning-alpha-populated-chromium-1440-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/morning-alpha-populated-chromium-390-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/morning-alpha-populated-chromium-390-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/personal-companion-mobile-chromium-1024-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/personal-companion-mobile-chromium-1024-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/personal-companion-mobile-chromium-1440-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/personal-companion-mobile-chromium-1440-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/personal-companion-mobile-chromium-390-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/personal-companion-mobile-chromium-390-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/portfolio-tamagotchi-empty-chromium-1024-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/portfolio-tamagotchi-empty-chromium-1024-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/portfolio-tamagotchi-empty-chromium-1440-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/portfolio-tamagotchi-empty-chromium-1440-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/portfolio-tamagotchi-empty-chromium-390-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/portfolio-tamagotchi-empty-chromium-390-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/prediction-impact-desktop-chromium-1024-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/prediction-impact-desktop-chromium-1024-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/prediction-impact-desktop-chromium-1440-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/prediction-impact-desktop-chromium-1440-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/prediction-impact-desktop-chromium-390-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/prediction-impact-desktop-chromium-390-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/smart-money-copy-empty-chromium-1024-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/smart-money-copy-empty-chromium-1024-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/smart-money-copy-empty-chromium-1440-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/smart-money-copy-empty-chromium-1440-linux.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/smart-money-copy-empty-chromium-390-linux-system.png is excluded by !**/*.png
  • storybook-e2e/visual.spec.ts-snapshots/smart-money-copy-empty-chromium-390-linux.png is excluded by !**/*.png
📒 Files selected for processing (44)
  • .env.example
  • app/api/access/route.ts
  • app/api/platform/capabilities/route.ts
  • app/api/platform/health/route.ts
  • app/api/project-data/route.ts
  • app/api/projects/v2/route.ts
  • app/platform/page.tsx
  • app/styles/drops-studio.builder.css
  • app/styles/drops-studio.previews.css
  • app/styles/drops-studio.responsive.css
  • app/styles/drops-studio.setup.css
  • app/styles/drops-studio.shell.css
  • components/drops-studio-setup.tsx
  • components/drops-studio.tsx
  • components/dropsbot-webhook-connection.tsx
  • components/platform/integration-catalog.tsx
  • components/platform/organization-console.tsx
  • components/platform/platform-overview.tsx
  • components/platform/platform-shell.tsx
  • components/platform/project-library.tsx
  • components/platform/template-catalog.tsx
  • components/preview-canvas.tsx
  • components/project-studio.tsx
  • components/project-v2-agent-intelligence.tsx
  • components/project-v2-studio-surface.tsx
  • design-qa.md
  • e2e/contracts/home-builder-p1.spec.ts
  • lib/access-tier.ts
  • lib/managed-platform/index.ts
  • lib/managed-platform/postgres-driver.ts
  • lib/platform-capabilities.ts
  • lib/platform-health-receipts.ts
  • lib/platform-provider-health.ts
  • lib/project-data/durable-backend.ts
  • lib/project-data/index.ts
  • lib/project-data/types.ts
  • package.json
  • scripts/verify-platform-providers.mjs
  • tests/access-tier.test.mjs
  • tests/platform-capabilities.test.mjs
  • tests/platform-public-surfaces.test.mjs
  • tests/project-data-durable-backend.test.mjs
  • tests/vercel-sandbox-runtime-cleanup.test.mjs
  • vercel.json

Comment thread app/api/access/route.ts
Comment on lines +28 to +41
export async function GET(request: NextRequest) {
if (!authorized(request)) {
return NextResponse.json(
{ error: "Platform health authorization is required." },
{ status: 401, headers: { "cache-control": "private, no-store" } },
);
}
const receipt = await runPlatformProviderHealthChecks();
return NextResponse.json(receipt, {
headers: { "cache-control": "private, no-store" },
});
}

export const POST = GET;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Bound the external side effects of this endpoint.

Each authorized request runs the full provider suite. That suite creates a persistent Vercel Sandbox, writes and deletes private Blob objects, writes and deletes real project-data rows, mints a GitHub installation token, and overwrites the shared health receipt. Two concurrent requests, for example the cron invocation plus one operator call, run all of that twice in parallel and race on the single receipt path. Add a single-flight guard plus a minimum interval between runs, and return the last stored receipt when a run is already in progress.

Also note that GET and POST share one non-idempotent, state-mutating handler.

As per coding guidelines: "Every external or destructive tool must have explicit approval, timeout, quota, audit record, bounded output, and idempotency behavior."

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

In `@app/api/platform/health/route.ts` around lines 28 - 41, Bound the side
effects in GET by guarding runPlatformProviderHealthChecks with a shared
single-flight lock and minimum-run interval. When a run is active or the
interval has not elapsed, return the last persisted health receipt without
starting provider checks; otherwise execute once, persist the receipt, and
release the guard reliably. Ensure POST, currently aliased to GET, uses the same
guard and cannot trigger a concurrent duplicate run.

Source: Coding guidelines

Comment on lines +30 to 50
let backendPromise: Promise<ProjectDataBackend> | null = null;

async function backend() {
if (globalThis.__DROPS_STUDIO_PROJECT_DATA_BACKEND_V2__) {
return globalThis.__DROPS_STUDIO_PROJECT_DATA_BACKEND_V2__;
}
if (process.env.DROPS_STUDIO_LOCAL_PROJECT_DATA !== "1") {
backendPromise ??= (async () => {
const durable = await createDurableProjectDataBackend();
if (durable) return durable;
if (process.env.DROPS_STUDIO_LOCAL_PROJECT_DATA === "1") {
return new MemoryProjectDataBackend();
}
throw new ProjectDataError(
"storage_unavailable",
"Project data storage is not configured. The generated app can continue with its labelled browser-local fallback.",
);
}
globalThis.__DROPS_STUDIO_PROJECT_DATA_BACKEND_V2__ = new MemoryProjectDataBackend();
return globalThis.__DROPS_STUDIO_PROJECT_DATA_BACKEND_V2__;
})();
const resolved = await backendPromise;
globalThis.__DROPS_STUDIO_PROJECT_DATA_BACKEND_V2__ = resolved;
return resolved;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Reset backendPromise when initialization fails.

??= caches the rejected promise. After one failed createDurableProjectDataBackend() call, every later request awaits the same rejection and returns 503 for the lifetime of the process. PostgresProjectDataBackend.#ensureSchema already clears its cached promise on failure. Apply the same handling here.

🐛 Proposed fix to clear the cache on failure
-  const resolved = await backendPromise;
-  globalThis.__DROPS_STUDIO_PROJECT_DATA_BACKEND_V2__ = resolved;
-  return resolved;
+  try {
+    const resolved = await backendPromise;
+    globalThis.__DROPS_STUDIO_PROJECT_DATA_BACKEND_V2__ = resolved;
+    return resolved;
+  } catch (error) {
+    backendPromise = null;
+    throw error;
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let backendPromise: Promise<ProjectDataBackend> | null = null;
async function backend() {
if (globalThis.__DROPS_STUDIO_PROJECT_DATA_BACKEND_V2__) {
return globalThis.__DROPS_STUDIO_PROJECT_DATA_BACKEND_V2__;
}
if (process.env.DROPS_STUDIO_LOCAL_PROJECT_DATA !== "1") {
backendPromise ??= (async () => {
const durable = await createDurableProjectDataBackend();
if (durable) return durable;
if (process.env.DROPS_STUDIO_LOCAL_PROJECT_DATA === "1") {
return new MemoryProjectDataBackend();
}
throw new ProjectDataError(
"storage_unavailable",
"Project data storage is not configured. The generated app can continue with its labelled browser-local fallback.",
);
}
globalThis.__DROPS_STUDIO_PROJECT_DATA_BACKEND_V2__ = new MemoryProjectDataBackend();
return globalThis.__DROPS_STUDIO_PROJECT_DATA_BACKEND_V2__;
})();
const resolved = await backendPromise;
globalThis.__DROPS_STUDIO_PROJECT_DATA_BACKEND_V2__ = resolved;
return resolved;
}
let backendPromise: Promise<ProjectDataBackend> | null = null;
async function backend() {
if (globalThis.__DROPS_STUDIO_PROJECT_DATA_BACKEND_V2__) {
return globalThis.__DROPS_STUDIO_PROJECT_DATA_BACKEND_V2__;
}
backendPromise ??= (async () => {
const durable = await createDurableProjectDataBackend();
if (durable) return durable;
if (process.env.DROPS_STUDIO_LOCAL_PROJECT_DATA === "1") {
return new MemoryProjectDataBackend();
}
throw new ProjectDataError(
"storage_unavailable",
"Project data storage is not configured. The generated app can continue with its labelled browser-local fallback.",
);
})();
try {
const resolved = await backendPromise;
globalThis.__DROPS_STUDIO_PROJECT_DATA_BACKEND_V2__ = resolved;
return resolved;
} catch (error) {
backendPromise = null;
throw error;
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/project-data/route.ts` around lines 30 - 50, Update the backend()
initialization flow so backendPromise is reset to null when the cached
initialization promise rejects, allowing later requests to retry
createDurableProjectDataBackend(). Preserve successful caching and
globalThis.__DROPS_STUDIO_PROJECT_DATA_BACKEND_V2__ assignment, and mirror the
failure-cache clearing behavior used by
PostgresProjectDataBackend.#ensureSchema.

Comment thread app/api/projects/v2/route.ts
Comment on lines +512 to +523
.landing-studio-toolbar button {
background: #fff;
border: 1px solid #dce4ef;
border-radius: 9px;
color: #31415b;
cursor: pointer;
font-size: 12px;
font-weight: 720;
gap: 6px;
min-height: 40px;
padding: 0 11px;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Increase the toolbar target height to 44px.

Line 521 sets visible Share and Build targets to 40px high. This is below the required 44 by 44 CSS pixels.

As per coding guidelines, "Every visible interactive target must be at least 44 by 44 CSS pixels."

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

In `@app/styles/drops-studio.previews.css` around lines 512 - 523, Update the
.landing-studio-toolbar button rule so its min-height is 44px instead of 40px,
ensuring visible Share and Build controls meet the required interactive target
size.

Source: Coding guidelines

Comment on lines +361 to +366
return working(
"github-app-installation-live",
"The GitHub App exchanged an installation token and read its bounded repository scope.",
["github-app-jwt-live", "github-installation-repositories-live"],
receipt.latencyMs,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Provider evidence tokens describe operations that this module never performed. Both receipts build their detail and evidence from configuration presence or from neighbouring check results, not from the operation named in the token. Apply one rule: emit an evidence token only for an operation that the current run executed.

  • lib/platform-provider-health.ts#L361-L366: compare the repositories returned at Line 348 against GITHUB_APP_ALLOWED_REPOSITORIES, or drop the "bounded repository scope" wording and the implied allowlist evidence.
  • lib/platform-provider-health.ts#L466-L479: run a real audit append and a checksummed restore probe, or replace private-recovery-storage-live and the "configured and live" detail with wording that states the result is derived from the managed-backend and organizations checks.

As per coding guidelines: "Keep external actions explicit and consent-based, and never claim trades, alerts, channels, deployments, or connections succeeded without verified provider evidence."

📍 Affects 1 file
  • lib/platform-provider-health.ts#L361-L366 (this comment)
  • lib/platform-provider-health.ts#L466-L479
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/platform-provider-health.ts` around lines 361 - 366, The provider health
receipts claim operations that were not executed. In
lib/platform-provider-health.ts lines 361-366, either compare the repositories
returned by the preceding check with GITHUB_APP_ALLOWED_REPOSITORIES before
emitting github-app-installation-live, or remove the bounded-scope wording and
implied allowlist evidence. In lines 466-479, either perform a real audit append
and checksummed restore probe before emitting private-recovery-storage-live, or
replace that token and “configured and live” detail with wording explicitly
derived from the managed-backend and organizations checks.

Source: Coding guidelines

Comment on lines +23 to +39
const POSTGRES_SCHEMA_MIGRATION = `
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'drops_project_data_snapshots'
AND column_name = 'snapshot_json'
AND data_type = 'jsonb'
) THEN
ALTER TABLE drops_project_data_snapshots
ALTER COLUMN snapshot_json TYPE TEXT
USING snapshot_json::text;
END IF;
END
$$
`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Restrict the migration probe to the current schema.

The information_schema.columns lookup matches drops_project_data_snapshots in any schema of the connected database. If another schema holds a same-named table, the probe can report jsonb and the ALTER TABLE then runs against the search-path table. Add a table_schema predicate.

🛡️ Proposed fix to scope the probe
     IF EXISTS (
       SELECT 1
       FROM information_schema.columns
-      WHERE table_name = 'drops_project_data_snapshots'
+      WHERE table_schema = current_schema()
+        AND table_name = 'drops_project_data_snapshots'
         AND column_name = 'snapshot_json'
         AND data_type = 'jsonb'
     ) THEN
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const POSTGRES_SCHEMA_MIGRATION = `
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'drops_project_data_snapshots'
AND column_name = 'snapshot_json'
AND data_type = 'jsonb'
) THEN
ALTER TABLE drops_project_data_snapshots
ALTER COLUMN snapshot_json TYPE TEXT
USING snapshot_json::text;
END IF;
END
$$
`;
const POSTGRES_SCHEMA_MIGRATION = `
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = current_schema()
AND table_name = 'drops_project_data_snapshots'
AND column_name = 'snapshot_json'
AND data_type = 'jsonb'
) THEN
ALTER TABLE drops_project_data_snapshots
ALTER COLUMN snapshot_json TYPE TEXT
USING snapshot_json::text;
END IF;
END
$$
`;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/project-data/durable-backend.ts` around lines 23 - 39, Update the
information_schema.columns probe in POSTGRES_SCHEMA_MIGRATION to constrain
table_schema to the current schema, ensuring the conditional check and
subsequent ALTER TABLE target the same search-path table rather than a
same-named table elsewhere.

Comment on lines +281 to +311
async deleteProject(
projectIdInput: string,
expectedStoreRevision: number,
): Promise<void> {
const projectId = validateProjectDataProjectId(projectIdInput);
const current = await this.#readEnvelope(projectId);
if (currentRevision(current.envelope) !== expectedStoreRevision) {
throw conflict(currentRevision(current.envelope));
}
if (!current.envelope || current.envelope.deleted) return;
try {
await (await this.#client()).put(
this.#path(projectId),
JSON.stringify(nextEnvelope(projectId, null, expectedStoreRevision + 1)),
{
access: "private",
addRandomSuffix: false,
allowOverwrite: true,
cacheControlMaxAge: 60,
contentType: "application/json; charset=utf-8",
...(current.etag ? { ifMatch: current.etag } : {}),
},
);
} catch (error) {
if (error instanceof BlobPreconditionFailedError) {
const latest = await this.#readEnvelope(projectId).catch(() => ({ envelope: null }));
throw conflict(currentRevision(latest.envelope));
}
throw storageFailure();
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🔴 Critical | 🏗️ Heavy lift

Tombstones make a deleted project permanently unwritable in both durable backends. read() returns null for a tombstone, so ProjectDataStore treats the project as empty and calls compareAndSwap(projectId, 0, next), while the backend still reports the tombstone revision. Every later write then throws conflict. Either remove the record on delete, or accept a tombstone revision during recreation.

  • lib/project-data/durable-backend.ts#L281-L311: delete the blob with the already-typed del member of BlobStorage instead of writing a tombstone envelope, or accept expectedStoreRevision = 0 when the current envelope is a tombstone.
  • lib/project-data/durable-backend.ts#L411-L440: replace the tombstone UPDATE with a DELETE FROM drops_project_data_snapshots WHERE project_key = $1 AND project_id = $2 AND store_revision = $3, or extend the create branch to ON CONFLICT (project_key) DO UPDATE ... WHERE drops_project_data_snapshots.deleted = TRUE.
📍 Affects 1 file
  • lib/project-data/durable-backend.ts#L281-L311 (this comment)
  • lib/project-data/durable-backend.ts#L411-L440
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/project-data/durable-backend.ts` around lines 281 - 311, Make deleted
projects recreatable in both durable backends by removing records on deletion
rather than persisting tombstones. In lib/project-data/durable-backend.ts lines
281-311, update deleteProject to call the typed BlobStorage.del member with the
existing revision/precondition handling. In lib/project-data/durable-backend.ts
lines 411-440, replace the tombstone UPDATE with a conditional DELETE from
drops_project_data_snapshots using project_key, project_id, and store_revision,
preserving conflict behavior for revision mismatches.

@@ -0,0 +1,19 @@
import { runPlatformProviderHealthChecks } from "../lib/platform-provider-health.ts";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check the declared Node version, the script wiring, and whether other .mjs files already import .ts modules.
set -uo pipefail

fd -H -t f 'package.json' -d 2 --exec sh -c 'echo "== $1"; jq "{name, engines, scripts}" "$1"' _ {}
fd -H -t f '.node-version|.nvmrc|.tool-versions' -d 2 --exec sh -c 'echo "== $1"; cat "$1"' _ {}

echo "== workflows node-version"
fd -t f -e yml -e yaml . .github 2>/dev/null | xargs -r rg -n 'node-version' || true

echo "== other .mjs files importing .ts modules"
rg -n --glob '*.mjs' 'from "\.\.?/[^"]+\.ts"' | head -40

echo "== references to verify-platform-providers"
rg -n 'verify-platform-providers' || true

Repository: svg8bit/drops-studio

Length of output: 2042


🏁 Script executed:

#!/bin/bash
set -u

echo "== script and package locations"
fd -H -t f 'verify-platform-providers.mjs|package.json' -d 3 -x sh -c 'echo "== $1"; if [[ "$1" == *verify-platform-providers.mjs ]]; then cat -n "$1"; else jq "{engines, scripts, dependencies, devDependencies}" "$1"; fi' _ {}

echo "== exact script references"
rg -n -F 'scripts/verify-platform-providers.mjs' . || true
rg -n -F 'verify-platform-providers' . || true

echo "== Node runtime capabilities"
node --version 2>&1 || true
node --help 2>&1 | rg -n 'strip-types|experimental-strip-types' || true

Repository: svg8bit/drops-studio

Length of output: 4850


Add an npm entry point and support the declared Node range.

package.json declares Node >=22.13.0, but this .ts import requires native type stripping enabled by default in newer Node versions or an explicit runtime flag. Add the required flag or raise the minimum Node version. Add an npm script for scripts/verify-platform-providers.mjs; no current npm script references it.

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

In `@scripts/verify-platform-providers.mjs` at line 1, Update the execution
configuration for scripts/verify-platform-providers.mjs so its TypeScript import
works across the declared Node >=22.13.0 range, using the required runtime flag
or an appropriately raised minimum version. Add a package.json npm script that
invokes verify-platform-providers.mjs.

Source: Coding guidelines

Comment on lines +3 to +6
const receipt = await runPlatformProviderHealthChecks({
includeSandbox: process.env.DROPS_SKIP_SANDBOX_HEALTH !== "1",
persist: 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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Require an explicit opt-in before live provider checks and receipt persistence.

The script runs live provider checks by default and persists the result with persist: true. runPlatformProviderHealthChecks creates a real Vercel Sandbox, writes and deletes real project-data rows, calls the GitHub and Vercel APIs, and then overwrites the shared health receipt through writePlatformHealthReceipt. Any shell that has production credentials in its environment will therefore mutate production state and replace the receipt that platformCapabilitySnapshotWithHealth serves to /platform.

Sandbox execution is currently opt-out through DROPS_SKIP_SANDBOX_HEALTH, and persistence has no gate at all. Invert both: require an explicit flag to run the live checks and a separate explicit flag to persist.

🔒️ Proposed gating
+if (process.env.DROPS_RUN_PROVIDER_HEALTH !== "1") {
+  console.error(
+    "Refusing to run live provider health checks. Set DROPS_RUN_PROVIDER_HEALTH=1 to opt in.",
+  );
+  process.exitCode = 1;
+} else {
 const receipt = await runPlatformProviderHealthChecks({
-  includeSandbox: process.env.DROPS_SKIP_SANDBOX_HEALTH !== "1",
-  persist: true,
+  includeSandbox: process.env.DROPS_INCLUDE_SANDBOX_HEALTH === "1",
+  persist: process.env.DROPS_PERSIST_HEALTH_RECEIPT === "1",
 });

As per coding guidelines: "Every external or destructive tool must have explicit approval, timeout, quota, audit record, bounded output, and idempotency behavior," and "Keep external actions explicit and consent-based."

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

In `@scripts/verify-platform-providers.mjs` around lines 3 - 6, Update the
invocation of runPlatformProviderHealthChecks to require separate explicit
environment-flag opt-ins for live provider checks and receipt persistence.
Replace the current opt-out includeSandbox logic with an opt-in condition, and
gate persist: true behind its own explicit persistence flag so both values are
false by default; preserve the existing health-check call and receipt behavior
when each corresponding flag is enabled.

Source: Coding guidelines

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