Skip to content

feat(entity-todo): Phase 4 - Local dev experience with Vite HMR#629

Merged
viniciusdacal merged 9 commits intomainfrom
ben/phase4-local-dev
Feb 22, 2026
Merged

feat(entity-todo): Phase 4 - Local dev experience with Vite HMR#629
viniciusdacal merged 9 commits intomainfrom
ben/phase4-local-dev

Conversation

@viniciusdacal
Copy link
Copy Markdown
Contributor

Summary

Phase 4: Local dev experience for entity-todo example.

Changes

  1. New dev-server.ts - Unified dev server that brings together:

    • Vite HMR for UI hot-reload
    • @vertz/server for API routes
    • SPA mode for client-side rendering
    • SQLite for local persistence (not D1)
  2. Updated package.json scripts:

    • : starts the unified dev server
    • : builds the production bundle
    • : runs wrangler deploy
  3. Fixes:

    • index.ts: Only mount in browser (not SSR)
    • entry-server.ts: Don't import client-side code

Usage

entity-todo-example@ dev /Users/viniciusdacal/openclaw-workspace/vertz/examples/entity-todo
bun run src/dev-server.ts

📦 SQLite database initialized at: /Users/viniciusdacal/openclaw-workspace/vertz/examples/entity-todo/data/todos.db

🚀 Starting Vertz Dev Server...

✅ Vite dev server initialized

╔═══════════════════════════════════════════════════════════╗
║ ║
║ 🏗️ Vertz Dev Server ║
║ ║
║ Local: http://localhost:3000
║ API: http://localhost:3000/api
║ ║
║ Stack: ║
║ • Vite HMR (UI hot-reload) ✅ ║
║ • @vertz/server (API routes) ✅ ║
║ • SPA mode (client-side rendering) ║
║ • SQLite (local persistence) ✅ ║
║ ║
║ Notes: ║
║ • API routes served locally with SQLite ║
║ • UI uses Vite HMR for hot-reload ║
║ • For SSR, run: pnpm build && pnpm preview ║
║ ║
║ Available endpoints: ║
║ • GET /api/todos List all todos ║
║ • GET /api/todos/:id Get a todo ║
║ • POST /api/todos Create a todo ║
║ • PATCH /api/todos/:id Update a todo ║
║ • DELETE /api/todos/:id Delete a todo ║
║ ║
╚═══════════════════════════════════════════════════════════╝

👋 Shutting down...

The dev server provides:

  • API routes at with SQLite persistence
  • Client-side rendering with Vite HMR
  • Data persists in

For true SSR, run:

Acceptance Criteria

  • pnpm dev starts everything with one command
  • UI changes hot-reload without page refresh
  • Entity changes trigger server restart (manual)
  • Data persists in local SQLite between restarts

Closes #616

Copy link
Copy Markdown
Contributor Author

@viniciusdacal viniciusdacal left a comment

Choose a reason for hiding this comment

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

Code Review: PR #629 - Phase 4: Local Dev Experience

Role: Primary Review (Frontend & API Surface Engineer)

Summary

This PR implements the local development experience for entity-todo. Overall the implementation is solid.

1. Dev Server ✅

  • Uses bun run src/dev-server.ts correctly (Bun is the team runtime)
  • Unified server combines Vite + API routes + SPA + SQLite
  • Port correctly defaults to 3000 from env

2. Vite HMR Integration ✅

  • Properly configured with middlewareMode and appType custom
  • HMR clientPort set correctly
  • Last commit (d764c8e) correctly removed /api proxy to prevent infinite loop with dev-server middleware

3. SQLite Persistence ✅

  • Uses bun:sqlite (correct per team conventions)
  • Full DbDriver interface implementation
  • Creates data/ directory if missing
  • Migration support with proper table creation
  • SQL injection protection: Whitelist approach in list() method for WHERE columns

4. Error Handling ✅

  • db.ts: Try/catch with user-friendly messages
  • entry-server.ts: SSR wrapped in try/catch, returns SPA fallback on error
  • worker.ts: D1 binding validation + try/catch in all handlers + security headers
  • dev-server.ts: Top-level try/catch returns 500

5. Minor Notes

  • The vite.config.ts comment about proxy removal is a bit abrupt but harmless
  • Good separation between dev (SQLite) and prod (D1) persistence

Verdict

Approved. Clean implementation that meets all acceptance criteria.

Copy link
Copy Markdown
Contributor Author

@viniciusdacal viniciusdacal left a comment

Choose a reason for hiding this comment

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

Adversarial Review: Local Dev Experience

Found several issues that could cause problems for developers:

Critical Issues

  1. Package Manager Confusion (DX Footgun)

    • package.json uses bun run for dev script (correct for team)
    • But docs/comments reference pnpm dev, pnpm build && pnpm preview
    • Developers will be confused about which package manager to use
  2. Hardcoded localhost in Request conversion

    • toWebRequest() hardcodes http://localhost:PORT
    • Breaks in containerized dev environments or reverse proxies
  3. PR Has Merge Conflicts

    • This PR is marked as CONFLICTING
    • Cannot be merged until resolved

Medium Issues

  1. No CORS Headers

    • API at /api/* has zero CORS configuration
    • Developers calling API from separate frontend origins will fail
  2. Database Not Closed on Shutdown

    • SQLite driver close() is never called in graceful shutdown
    • Can leave WAL files in inconsistent state
  3. Console.log in Production Code

    • console.log(Entity Todo app mounted) in index.ts runs in all environments
    • Should be wrapped in import.meta.DEV
  4. No Request Body Validation

    • POST/PATCH handlers dont validate Content-Type
    • Accepts any body without checking its valid JSON

Minor Issues

  1. No Rate Limiting

    • SQLite dev server has no rate limiting
  2. No Input Length Limits

    • No max length on title field

Summary

The core functionality works, but the DX has several footguns. Package manager mismatch and missing CORS are most impactful.

Ben added 8 commits February 22, 2026 20:35
- Add entry-server.ts: SSR render function using renderPage()
- Add entry-client.ts: Client hydration with hydrate()/mount() fallback
- Update worker.ts: Route splitting (/api/* → JSON, /* → SSR)
- Update vite.config.ts: SSR build configuration for worker + client bundle
- Add wrangler.toml: Cloudflare Workers configuration with static assets
- Add @vertz/cloudflare and @cloudflare/workers-types dependencies

This establishes the SSR + Hydration pipeline for entity-todo on Cloudflare Workers.
Critical fixes:
1. Hydration: Use mount() instead of hydrate() with empty registry
   - The registry-based hydrate() was receiving {} so no components were found
   - Now using mount() which replaces SSR content with client-rendered app

2. API routes: Wire up actual CRUD handlers instead of stubs
   - Implemented in-memory todo store with full CRUD operations
   - Returns proper JSON responses that match SDK expectations

3. Error handling: Wrap renderApp() in try/catch
   - On error, returns fallback HTML that loads client bundle (SPA fallback)
   - Includes error message for debugging

4. Streaming: Already using renderToStream via renderPage
   - No changes needed, this was already implemented

High priority:
5. Security headers: Add basic headers to all responses
   - X-Content-Type-Options, X-Frame-Options, X-XSS-Protection
   - Referrer-Policy, Content-Security-Policy

6. CSS injection: Include CSS in rendered page
   - Added inline <style> tag with globalStyles.css in head option
1. entry-client.ts: Use 'tolerant' hydration mode instead of replace
   - mount() now accepts hydration: 'tolerant' to walk existing SSR DOM nodes
   - This preserves server-rendered content while attaching event handlers

2. worker.ts: Replace in-memory store with D1 database adapter
   - Use createDb() with SQLite dialect and D1 binding
   - Wire CRUD routes to use the database instead of Map

3. mount.ts: Add 'tolerant' hydration option
   - Updated MountOptions interface to support 'tolerant' mode
   - In tolerant mode, app doesn't clear existing DOM content

4. wrangler.toml: Add D1 database binding configuration
- Revert mount.ts to version from main (proper tolerant hydration)
- Add D1 binding validation in worker.ts with security headers
- Simplify entry-client.ts to just call mount with hydration: 'tolerant'
- Export globalStyles from index.ts (entry-server.ts)
- Import D1Database from @cloudflare/workers-types (worker.ts)
- Import isOk from @vertz/fetch instead of @vertz/ui (todo-list.tsx)

Add type tests to verify correct imports.
Phase 4: Local dev experience

- Add dev-server.ts that brings together:
  - Vite HMR for UI hot-reload
  - @vertz/server for API routes
  - SPA mode for client-side rendering
  - SQLite for local persistence (not D1)

- Update package.json scripts:
  - dev: starts the unified dev server
  - build: builds the production bundle
  - deploy: runs wrangler deploy

- Fix index.ts to only mount in browser (not SSR)
- Update entry-server.ts to not import client-side code

The dev server runs at http://localhost:3000 with:
- API routes at /api/*
- Client-side rendering with HMR
- SQLite database persistence in data/todos.db

For true SSR, run: pnpm build && pnpm preview

Closes #616
…Styles

- Remove /api proxy from vite.config.ts (handled by dev-server.ts middleware)
- Add try/catch around SQLite initialization in db.ts with clear error message
- Import globalStyles from ./index instead of duplicating in entry-server.ts
@viniciusdacal viniciusdacal merged commit 267f433 into main Feb 22, 2026
4 checks passed
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.

Phase 4: Local dev experience — vertz dev with Vite HMR

1 participant