Skip to content

feat: wire initial Next.js app to Convex backend#65

Open
BASIC-BIT wants to merge 11 commits intomainfrom
feat/wire-web-to-convex
Open

feat: wire initial Next.js app to Convex backend#65
BASIC-BIT wants to merge 11 commits intomainfrom
feat/wire-web-to-convex

Conversation

@BASIC-BIT
Copy link
Owner

Summary

  • add the minimum apps/web Convex provider and client-side query wiring so the homepage can render the existing health:status backend payload
  • bridge the repo-root local Convex URL into the web scripts so pnpm dev:web, pnpm typecheck:web, and pnpm build:web use the local backend without a duplicate frontend env file
  • update app and backend docs to describe the new local workflow and keep the first server-side data pattern as a separate follow-on under #64

Closes #55

@greptile-apps
Copy link

greptile-apps bot commented Mar 13, 2026

Greptile Summary

This PR wires the existing apps/web Next.js app to the convex/ backend for the first time, bridging CONVEX_URL from the repo-root .env.local into NEXT_PUBLIC_CONVEX_URL via a new wrapper script so the homepage can render the live health:status query result without a duplicate hand-maintained env file.

Key changes:

  • scripts/run-web-with-convex-env.mjs: New wrapper that reads .env.local, bridges CONVEX_URLNEXT_PUBLIC_CONVEX_URL, and spawns the downstream pnpm command with the merged env. Multiple rounds of edge-case hardening (signal forwarding, hasOwnProperty env checks, isQuotedValue guard, readFileSync error catch) have been applied through prior iterations.
  • apps/web/src/app/convex-client-provider.tsx: URL-keyed module-level ConvexReactClient singleton, SSR-gated with typeof window === "undefined", with a resetConvexClientForTests() escape hatch for test isolation.
  • apps/web/src/app/convex-runtime-panel.tsx: New client-side panel with a ConvexQueryErrorBoundary (with componentDidCatch logging and a retry-key reset path), split undefined/falsy/truthy status states, and an isClient guard to avoid rendering Convex hooks during SSR.
  • package.json routes dev:web, build:web, and typecheck:web through the new wrapper script; lint:web keeps the direct pnpm call since it doesn't need the env bridge.
  • apps/web/README.md documentation ordering places pnpm bootstrap:backend:local in "useful follow-up commands" rather than the primary first-time setup steps, which is inconsistent with docs/backend/convex-bootstrap.md.

Confidence Score: 3/5

  • Safe to merge for local development wiring, but carries a lingering hydration mismatch risk in the runtime panel and a documentation ordering issue that could confuse first-time contributors.
  • The core Convex client/provider wiring and env-bridging script are functionally correct and have been refined across many iterations addressing edge cases. The main remaining concerns are: (1) the typeof window !== "undefined" check in ConvexRuntimePanel produces different server vs. client render output, which can surface as a hydration warning or layout flash in development; and (2) the apps/web/README.md getting-started block omits the required bootstrap step. Neither issue causes data loss or a broken production build, but they affect DX and correctness of the SSR render path.
  • Pay close attention to apps/web/src/app/convex-runtime-panel.tsx (isClient hydration guard) and apps/web/README.md (bootstrap step ordering).

Important Files Changed

Filename Overview
apps/web/src/app/convex-client-provider.tsx New Client Component that manages a URL-keyed module-level ConvexReactClient singleton. The typeof window === "undefined" SSR guard is correctly in place (per previous thread resolution). Minor concern: getConvexClient is invoked directly in the render body and can mutate the singleton, which is technically a render side-effect. Includes a resetConvexClientForTests() export for test isolation.
apps/web/src/app/convex-runtime-panel.tsx New Client Component with error boundary, retry key, and split loading/empty/data states. The typeof window !== "undefined" isClient guard at line 131 produces different server vs. client render output (hydration mismatch risk that was flagged and addressed in a prior thread). All other prior concerns — error boundary reset, componentDidCatch logging, heading copy during loading — are correctly resolved in this version.
scripts/run-web-with-convex-env.mjs New wrapper script that reads .env.local, bridges CONVEX_URL into NEXT_PUBLIC_CONVEX_URL, and spawns the downstream pnpm command. All major previously-identified issues (SIGTERM forwarding, signal re-raise, hasOwnProperty env forwarding, readFileSync error guard, isQuotedValue trailing-escape guard) appear resolved in this final revision. The NEXT_PUBLIC_CONVEX_URL bridging block at lines 113–122 still uses a truthy guard alongside hasOwnProperty, which is a minor asymmetry with the forwarding loop above.
apps/web/README.md Documentation ordering issue: pnpm bootstrap:backend:local is listed only as a "follow-up command" but is required before pnpm dev:backend:local for first-time setup. The canonical order in docs/backend/convex-bootstrap.md (install → bootstrap → dev backend → dev web) is not reflected here.
apps/web/src/app/layout.tsx Minimal Server Component change: wraps children in ConvexClientProvider. Correct pattern for mounting a Client Component provider in the App Router root layout.
apps/web/src/app/page.tsx Server Component homepage updated to render ConvexRuntimePanel in the aside slot. Scaffold-choice metadata replaced with live Convex status panel; article copy updated to reflect current milestone. No logic concerns.
apps/web/tsconfig.json Adds @convex/* path alias pointing to ../../convex/* so the web app can import from the shared backend generated types without a duplicate package reference. Next.js automatically honours tsconfig paths for bundler resolution, so this is safe.
package.json dev:web, build:web, and typecheck:web now route through run-web-with-convex-env.mjs so the Convex URL is bridged automatically. lint:web deliberately keeps the direct pnpm call since it doesn't need the env bridge, which is a reasonable split.
apps/web/package.json Adds convex ^1.32.0 as a runtime dependency alongside React 19 and Next.js 16. No dependency conflicts visible in the lockfile snapshot.

Sequence Diagram

sequenceDiagram
    participant Dev as Developer
    participant Script as run-web-with-convex-env.mjs
    participant EnvFile as .env.local
    participant Next as Next.js (apps/web)
    participant Provider as ConvexClientProvider
    participant Panel as ConvexRuntimePanel
    participant Convex as Convex Backend (convex/)

    Dev->>Script: pnpm dev:web
    Script->>EnvFile: parseEnvFile(.env.local)
    EnvFile-->>Script: { CONVEX_URL, ... }
    Script->>Script: bridge CONVEX_URL → NEXT_PUBLIC_CONVEX_URL
    Script->>Next: spawn(pnpm --filter web dev, env)

    Note over Next: SSR render
    Next->>Provider: render ConvexClientProvider
    Provider->>Provider: getConvexClient()<br/>(typeof window === "undefined" → null)
    Provider-->>Next: children (no ConvexProvider wrap)
    Next->>Panel: render ConvexRuntimePanel
    Panel->>Panel: isClient = false → static placeholder

    Note over Next: Client hydration
    Next->>Provider: hydrate ConvexClientProvider
    Provider->>Provider: getConvexClient(convexUrl)<br/>→ new ConvexReactClient
    Provider-->>Next: wrap children in ConvexProvider

    Next->>Panel: hydrate ConvexRuntimePanel
    Panel->>Panel: isClient = true
    Panel->>Panel: render ConvexQueryErrorBoundary
    Panel->>Convex: useQuery(api.health.status)
    Convex-->>Panel: { status, backend, project, scope, note }
    Panel-->>Dev: Live status panel rendered
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: apps/web/README.md
Line: 9-13

Comment:
**Bootstrap step missing from primary workflow**

`pnpm dev:backend:local` is listed in the primary getting-started block, but `pnpm bootstrap:backend:local` has been moved to "Useful follow-up commands." For a first-time developer cloning the repo, running `pnpm dev:backend:local` before bootstrapping will fail because there is no local Convex deployment yet.

`docs/backend/convex-bootstrap.md` correctly sequences the steps as: install → bootstrap → dev backend → dev web. The `apps/web/README.md` should mirror that order:

```suggestion
pnpm install
pnpm bootstrap:backend:local
pnpm dev:backend:local
pnpm dev:web
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: apps/web/src/app/convex-client-provider.tsx
Line: 29-38

Comment:
**Side-effectful function called in render body**

`getConvexClient` can call `convexClient?.close()` and `new ConvexReactClient(convexUrl)` when the URL changes. Calling these side effects directly during render — rather than inside a `useEffect` or event handler — is outside React's render-purity contract. In practice the URL is a compile-time constant so the branch that mutates the singleton only runs on the very first render, but React's concurrent renderer can invoke render functions multiple times before committing (e.g. during `startTransition` or hydration), which would incorrectly attempt to close and reopen the WebSocket client on each discarded render.

A light guard would make the mutation safe:

```ts
// Only mutate when the effect is committed (client-side).
// If the URL cannot change at runtime this is low-risk today, but
// explicit side-effect isolation prevents surprises under concurrent features.
```

Consider wrapping the close/create path in `useEffect` (keyed on `convexUrl`) inside `ConvexClientProvider` rather than calling it inline in the render body.

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: b4ecf44

Comment on lines +29 to +38
export function ConvexClientProvider({ children }: { children: ReactNode }) {
const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL;
const client = getConvexClient(convexUrl);

if (!client) {
return <>{children}</>;
}

return <ConvexProvider client={client}>{children}</ConvexProvider>;
}
Copy link

Choose a reason for hiding this comment

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

Side-effectful function called in render body

getConvexClient can call convexClient?.close() and new ConvexReactClient(convexUrl) when the URL changes. Calling these side effects directly during render — rather than inside a useEffect or event handler — is outside React's render-purity contract. In practice the URL is a compile-time constant so the branch that mutates the singleton only runs on the very first render, but React's concurrent renderer can invoke render functions multiple times before committing (e.g. during startTransition or hydration), which would incorrectly attempt to close and reopen the WebSocket client on each discarded render.

A light guard would make the mutation safe:

// Only mutate when the effect is committed (client-side).
// If the URL cannot change at runtime this is low-risk today, but
// explicit side-effect isolation prevents surprises under concurrent features.

Consider wrapping the close/create path in useEffect (keyed on convexUrl) inside ConvexClientProvider rather than calling it inline in the render body.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/web/src/app/convex-client-provider.tsx
Line: 29-38

Comment:
**Side-effectful function called in render body**

`getConvexClient` can call `convexClient?.close()` and `new ConvexReactClient(convexUrl)` when the URL changes. Calling these side effects directly during render — rather than inside a `useEffect` or event handler — is outside React's render-purity contract. In practice the URL is a compile-time constant so the branch that mutates the singleton only runs on the very first render, but React's concurrent renderer can invoke render functions multiple times before committing (e.g. during `startTransition` or hydration), which would incorrectly attempt to close and reopen the WebSocket client on each discarded render.

A light guard would make the mutation safe:

```ts
// Only mutate when the effect is committed (client-side).
// If the URL cannot change at runtime this is low-risk today, but
// explicit side-effect isolation prevents surprises under concurrent features.
```

Consider wrapping the close/create path in `useEffect` (keyed on `convexUrl`) inside `ConvexClientProvider` rather than calling it inline in the render body.

How can I resolve this? If you propose a fix, please make it concise.

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.

[Draft] Wire initial Next.js app to Convex backend

1 participant