React SSR

Wahoo renders React through a private Node worker. Go stays the public server.

Why Wahoo Uses a Worker

React runs in JavaScript. Go does not run a Vite React bundle by itself. The worker gives React a normal Node environment and lets Go keep control of HTTP, cookies, access checks, and deployment.

Development Flow

Start the generated application:

npm install --prefix web
npm run dev --prefix web

The script starts Vite on port 4173 and Go on port 8080. It sets:

WAHOO_SSR_URL=http://127.0.0.1:4173/__wahoo_ssr

For a page request, the Go renderer:

  1. Parses WAHOO_SSR_URL.
  2. Adds the original request path and query as the url query value.
  3. Sends the browser cookie header to the worker.
  4. Requests the private /__wahoo_ssr route.
  5. Returns the worker HTML when the response succeeds.

The Vite worker loads src/entry-server.tsx. React renders the App component. The browser then hydrates with src/entry-client.tsx.

SSR Files

File Function
web/src/entry-client.tsx Starts browser hydration.
web/src/entry-server.tsx Renders the React document.
web/dev-server.mjs Runs Vite SSR in development.
web/ssr-server.mjs Runs the built SSR entry in production.
internal/ssr/renderer.go Calls the private worker from Go.

Add Page Data

The starter renders a static page. Add page data in a controlled sequence:

  1. Authenticate and authorize in Go.
  2. Load the small, safe view model in Go.
  3. Send only that view model to the worker.
  4. Render the view model in React.
  5. Serialize the same safe view model for browser hydration.

Do not send session tokens, password hashes, raw provider payloads, or unrestricted database records to the worker or browser.

Use a request deadline for each worker call. Use a small response size limit. Add these controls before production use.

Build for Production

Build the frontend from the application root:

npm run build --prefix web

The build creates:

Path Content
web/dist/assets Hashed browser CSS and JavaScript assets.
web/dist/.vite/manifest.json Client asset manifest.
web/dist/server/entry-server.js Node SSR entry point.

Go serves GET /assets/ from web/dist/assets. The production worker reads the manifest and inserts the correct asset paths into each document.

Start the worker and Go process from the application root:

npm run start:ssr --prefix web
WAHOO_SSR_URL=http://127.0.0.1:4173/__wahoo_ssr go run .

Do not expose port 4173 to the public network.

Fallback Shell

If WAHOO_SSR_URL is unset or the worker request fails, the generated renderer returns a development client shell. This supports Go-only local work.

Do not use this fallback as a production recovery method. It references development modules. Treat a missing SSR worker as a deployment error for public pages.

Add Streaming Later

The starter uses renderToString. Use streaming only after you measure a page that needs it.

To add streaming:

  1. Replace renderToString with a React streaming API.
  2. Make the worker send a chunked response.
  3. Stream the worker response through Go.
  4. Set a render timeout and error boundary.
  5. Test client hydration and cancellation.

Keep the first implementation small. SSR correctness matters more than streaming speed.