Repository: dai-shi/waku
Pull request #961 (2024-10-10 13:12:31)

new defineEntries (and context middleware)
This is to prepare for upcoming api route support.

- [x] context middleware (replacing header middleware)
- [x] new_defineEntries
  - [x] renderRsc
  - [x] renderHtml
  - [x] build
  - [x] server functions


vercel[bot]: [vc]: #wD6dDsTVcseE7uCl/pZUG5YZBaQ6OR6FZG3LfzW7Gkg=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJ3YWt1IiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL2RhaXNoaXMtcHJvamVjdHMvd2FrdS84OTRaekhhQktGVlQ3YWpvWmlGM1NYYUdrNlB2IiwicHJldmlld1VybCI6Indha3UtZ2l0LWlkZWEtcmVuZGVyLWVudHJ5LWRhaXNoaXMtcHJvamVjdHMudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJQRU5ESU5HIn1dfQ==
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Updated (UTC) |
| :--- | :----- | :------ | :------ |
| **waku** | 🔄 Building ([Inspect](https://vercel.com/daishis-projects/waku/894ZzHaBKFVT7ajoZiF3SXaGk6Pv)) | [Visit Preview](https://waku-git-idea-render-entry-daishis-projects.vercel.app) | Oct 10, 2024 1:12pm |



Patch:
diff --git a/docs/redirects.mdx b/docs/redirects.mdx
index 89302acc..b239a12b 100644
--- a/docs/redirects.mdx
+++ b/docs/redirects.mdx
@@ -52,9 +52,9 @@ import type { Config } from 'waku/config';
 
 export default {
   middleware: () => [
+    import('waku/middleware/context'),
     import('./src/middleware/redirects.js'),
     import('waku/middleware/dev-server'),
-    import('waku/middleware/headers'),
     import('waku/middleware/rsc'),
     import('waku/middleware/ssr'),
   ],
diff --git a/examples/12_nossr/waku.config.ts b/examples/12_nossr/waku.config.ts
index 8b8ba425..770a600a 100644
--- a/examples/12_nossr/waku.config.ts
+++ b/examples/12_nossr/waku.config.ts
@@ -1,6 +1,7 @@
 /** @type {import('waku/config').Config} */
 export default {
   middleware: () => [
+    import('waku/middleware/context'),
     import('waku/middleware/dev-server'),
     import('waku/middleware/rsc'),
     import('waku/middleware/fallback'),
diff --git a/examples/31_minimal/src/entries.tsx b/examples/31_minimal/src/entries.tsx
index 894d8e6d..3c3b3483 100644
--- a/examples/31_minimal/src/entries.tsx
+++ b/examples/31_minimal/src/entries.tsx
@@ -1,27 +1,18 @@
-import { defineEntries } from 'waku/server';
-import { Slot } from 'waku/client';
+import { new_defineEntries } from 'waku/minimal/server';
+import { Slot } from 'waku/minimal/client';
 
 import App from './components/App';
 
-export default defineEntries(
-  // renderEntries
-  async (rscPath) => {
-    return {
-      App: <App name={rscPath || 'Waku'} />,
-    };
-  },
-  // getBuildConfig
-  async () => [{ pathname: '/', entries: [{ rscPath: '' }] }],
-  // getSsrConfig
-  async (pathname) => {
-    switch (pathname) {
-      case '/':
-        return {
-          rscPath: '',
-          html: <Slot id="App" />,
-        };
-      default:
-        return null;
+export default new_defineEntries({
+  unstable_handleRequest: async (input, { renderRsc, renderHtml }) => {
+    if (input.type === 'component') {
+      return renderRsc({ App: <App name={input.rscPath || 'Waku'} /> });
+    }
+    if (input.type === 'custom' && input.pathname === '/') {
+      return renderHtml({ App: <App name="Waku" /> }, <Slot id="App" />, '');
     }
   },
-);
+  unstable_getBuildConfig: async () => [
+    { pathname: '/', entries: [{ rscPath: '' }] },
+  ],
+});
diff --git a/examples/31_minimal/waku.config.ts b/examples/31_minimal/waku.config.ts
new file mode 100644
index 00000000..d2d8ee9d
--- /dev/null
+++ b/examples/31_minimal/waku.config.ts
@@ -0,0 +1,12 @@
+// This is a temporary file while experimenting new_defineEntries
+
+import { defineConfig } from 'waku/config';
+
+export default defineConfig({
+  middleware: () => [
+    import('waku/middleware/context'),
+    import('waku/middleware/dev-server'),
+    import('waku/middleware/handler'),
+    import('waku/middleware/fallback'),
+  ],
+});
diff --git a/examples/34_functions/src/als.ts b/examples/34_functions/src/als.ts
new file mode 100644
index 00000000..92f2240f
--- /dev/null
+++ b/examples/34_functions/src/als.ts
@@ -0,0 +1,16 @@
+import { AsyncLocalStorage } from 'node:async_hooks';
+
+type Rerender = (rscPath: string) => void;
+
+const store = new AsyncLocalStorage<Rerender>();
+
+export const runWithRerender = <T>(rerender: Rerender, fn: () => T): T => {
+  return store.run(rerender, fn);
+};
+
+export const rerender = (rscPath: string) => {
+  const fn = store.getStore();
+  if (fn) {
+    fn(rscPath);
+  }
+};
diff --git a/examples/34_functions/src/components2/funcs.ts b/examples/34_functions/src/components2/funcs.ts
index c8172f34..a9e6141a 100644
--- a/examples/34_functions/src/components2/funcs.ts
+++ b/examples/34_functions/src/components2/funcs.ts
@@ -1,13 +1,9 @@
 'use server';
 
-import {
-  rerender,
-  unstable_getCustomContext as getCustomContext,
-} from 'waku/server';
+import { rerender } from '../als';
 
 export const greet = async (name: string) => {
   await Promise.resolve();
-  console.log('Custom Context:', getCustomContext()); // ---> {}
   return `Hello ${name} from server!`;
 };
 
diff --git a/examples/34_functions/src/components2/funcs2.ts b/examples/34_functions/src/components2/funcs2.ts
index 1514c5c6..e9056791 100644
--- a/examples/34_functions/src/components2/funcs2.ts
+++ b/examples/34_functions/src/components2/funcs2.ts
@@ -1,6 +1,6 @@
 'use server';
 
-import { unstable_getCustomContext as getCustomContext } from 'waku/server';
+import { getContext } from 'waku/middleware/context';
 
 export const greet = async (name: string) => {
   await Promise.resolve();
@@ -9,6 +9,6 @@ export const greet = async (name: string) => {
 
 export const hello = async (name: string) => {
   await Promise.resolve();
-  console.log('Custom Context:', getCustomContext()); // ---> {}
+  console.log('Context:', getContext());
   console.log('Hello', name, '!');
 };
diff --git a/examples/34_functions/src/entries.tsx b/examples/34_functions/src/entries.tsx
index 852bca1c..b037ebb0 100644
--- a/examples/34_functions/src/entries.tsx
+++ b/examples/34_functions/src/entries.tsx
@@ -1,27 +1,29 @@
-import { defineEntries } from 'waku/server';
-import { Slot } from 'waku/client';
+import { new_defineEntries } from 'waku/minimal/server';
+import { Slot } from 'waku/minimal/client';
 
 import App from './components2/App';
+import { runWithRerender } from './als';
 
-export default defineEntries(
-  // renderEntries
-  async (rscPath) => {
-    return {
-      App: <App name={rscPath || 'Waku'} />,
-    };
-  },
-  // getBuildConfig
-  async () => [{ pathname: '/', entries: [{ rscPath: '' }] }],
-  // getSsrConfig
-  async (pathname) => {
-    switch (pathname) {
-      case '/':
-        return {
-          rscPath: '',
-          html: <Slot id="App" />,
-        };
-      default:
-        return null;
+export default new_defineEntries({
+  unstable_handleRequest: async (input, { renderRsc, renderHtml }) => {
+    if (input.type === 'component') {
+      return renderRsc({ App: <App name={input.rscPath || 'Waku'} /> });
+    }
+    if (input.type === 'function') {
+      const elements: Record<string, unknown> = {};
+      const rerender = (rscPath: string) => {
+        elements.App = <App name={rscPath || 'Waku'} />;
+      };
+      const value = await runWithRerender(rerender, () =>
+        input.fn(...input.args),
+      );
+      return renderRsc({ ...elements, _value: value });
+    }
+    if (input.type === 'custom' && input.pathname === '/') {
+      return renderHtml({ App: <App name="Waku" /> }, <Slot id="App" />, '');
     }
   },
-);
+  unstable_getBuildConfig: async () => [
+    { pathname: '/', entries: [{ rscPath: '' }] },
+  ],
+});
diff --git a/examples/34_functions/tsconfig.json b/examples/34_functions/tsconfig.json
index 84d0d542..33d25f48 100644
--- a/examples/34_functions/tsconfig.json
+++ b/examples/34_functions/tsconfig.json
@@ -9,7 +9,7 @@
     "skipLibCheck": true,
     "noUncheckedIndexedAccess": true,
     "exactOptionalPropertyTypes": true,
-    "types": ["react/experimental"],
+    "types": ["node", "react/experimental"],
     "jsx": "react-jsx"
   }
 }
diff --git a/examples/34_functions/waku.config.ts b/examples/34_functions/waku.config.ts
new file mode 100644
index 00000000..d2d8ee9d
--- /dev/null
+++ b/examples/34_functions/waku.config.ts
@@ -0,0 +1,12 @@
+// This is a temporary file while experimenting new_defineEntries
+
+import { defineConfig } from 'waku/config';
+
+export default defineConfig({
+  middleware: () => [
+    import('waku/middleware/context'),
+    import('waku/middleware/dev-server'),
+    import('waku/middleware/handler'),
+    import('waku/middleware/fallback'),
+  ],
+});
diff --git a/examples/35_nesting/src/components/Counter.tsx b/examples/35_nesting/src/components/Counter.tsx
index b1397860..6de58400 100644
--- a/examples/35_nesting/src/components/Counter.tsx
+++ b/examples/35_nesting/src/components/Counter.tsx
@@ -1,7 +1,7 @@
 'use client';
 
 import { useState, useTransition } from 'react';
-import { Slot, useRefetch } from 'waku/client';
+import { Slot, useRefetch } from 'waku/minimal/client';
 
 export const Counter = ({ enableInnerApp }: { enableInnerApp?: boolean }) => {
   const [count, setCount] = useState(0);
diff --git a/examples/38_cookies/waku.config.ts b/examples/38_cookies/waku.config.ts
index e8db632d..395ecb42 100644
--- a/examples/38_cookies/waku.config.ts
+++ b/examples/38_cookies/waku.config.ts
@@ -1,9 +1,9 @@
 /** @type {import('waku/config').Config} */
 export default {
   middleware: () => [
+    import('waku/middleware/context'),
     import('./src/middleware/cookie.js'),
     import('waku/middleware/dev-server'),
-    import('waku/middleware/headers'),
     import('waku/middleware/rsc'),
     import('waku/middleware/ssr'),
   ],
diff --git a/examples/39_api/waku.config.ts b/examples/39_api/waku.config.ts
index fd6aa031..ef7abba9 100644
--- a/examples/39_api/waku.config.ts
+++ b/examples/39_api/waku.config.ts
@@ -1,9 +1,9 @@
 /** @type {import('waku/config').Config} */
 export default {
   middleware: () => [
+    import('waku/middleware/context'),
     import('./src/middleware/api.js'),
     import('waku/middleware/dev-server'),
-    import('waku/middleware/headers'),
     import('waku/middleware/rsc'),
     import('waku/middleware/ssr'),
   ],
diff --git a/packages/waku/package.json b/packages/waku/package.json
index 93046aa9..22866ef6 100644
--- a/packages/waku/package.json
+++ b/packages/waku/package.json
@@ -42,6 +42,14 @@
       "types": "./dist/client.d.ts",
       "default": "./dist/client.js"
     },
+    "./minimal/server": {
+      "types": "./dist/minimal/server.d.ts",
+      "default": "./dist/minimal/server.js"
+    },
+    "./minimal/client": {
+      "types": "./dist/minimal/client.d.ts",
+      "default": "./dist/minimal/client.js"
+    },
     "./server": {
       "types": "./dist/server.d.ts",
       "default": "./dist/server.js"
diff --git a/packages/waku/src/client.ts b/packages/waku/src/client.ts
index 49c7f0c3..ba760b34 100644
--- a/packages/waku/src/client.ts
+++ b/packages/waku/src/client.ts
@@ -1,321 +1,14 @@
-/// <reference types="react/canary" />
-'use client';
-
 import {
-  Component,
-  createContext,
-  createElement,
-  memo,
-  use,
-  useCallback,
-  useEffect,
-  useState,
-} from 'react';
-import type { ReactNode } from 'react';
-import RSDWClient from 'react-server-dom-webpack/client';
-
-import { encodeRscPath, encodeFuncId } from './lib/renderers/utils.js';
-
-const { createFromFetch, encodeReply } = RSDWClient;
-
-declare global {
-  interface ImportMeta {
-    readonly env: Record<string, string>;
-  }
-}
-
-const BASE_PATH = `${import.meta.env?.WAKU_CONFIG_BASE_PATH}${
-  import.meta.env?.WAKU_CONFIG_RSC_BASE
-}/`;
-
-const checkStatus = async (
-  responsePromise: Promise<Response>,
-): Promise<Response> => {
-  const response = await responsePromise;
-  if (!response.ok) {
-    const err = new Error(response.statusText);
-    (err as any).statusCode = response.status;
-    throw err;
-  }
-  return response;
-};
-
-type Elements = Promise<Record<string, ReactNode>> & {
-  prev?: Record<string, ReactNode> | undefined;
-};
-
-const getCached = <T>(c: () => T, m: WeakMap<object, T>, k: object): T =>
-  (m.has(k) ? m : m.set(k, c())).get(k) as T;
-const cache1 = new WeakMap();
-const mergeElements = (a: Elements, b: Elements): Elements => {
-  const getResult = () => {
-    const promise: Elements = new Promise((resolve, reject) => {
-      Promise.all([a, b])
-        .then(([a, b]) => {
-          const nextElements = { ...a, ...b };
-          delete nextElements._value;
-          promise.prev = a;
-          resolve(nextElements);
-        })
-        .catch((e) => {
-          a.then(
-            (a) => {
-              promise.prev = a;
-              reject(e);
-            },
-            () => {
-              promise.prev = a.prev;
-              reject(e);
-            },
-          );
-        });
-    });
-    return promise;
-  };
-  const cache2 = getCached(() => new WeakMap(), cache1, a);
-  return getCached(getResult, cache2, b);
-};
-
-type SetElements = (updater: (prev: Elements) => Elements) => void;
-type EnhanceCreateData = (
-  createData: (
-    responsePromise: Promise<Response>,
-  ) => Promise<Record<string, ReactNode>>,
-) => (responsePromise: Promise<Response>) => Promise<Record<string, ReactNode>>;
-
-const ENTRY = 'e';
-const SET_ELEMENTS = 's';
-const ENHANCE_CREATE_DATA = 'd';
-
-type FetchCache = {
-  [ENTRY]?: [rscPath: string, rscParams: unknown, elements: Elements];
-  [SET_ELEMENTS]?: SetElements;
-  [ENHANCE_CREATE_DATA]?: EnhanceCreateData | undefined;
-};
-
-const defaultFetchCache: FetchCache = {};
-
-/**
- * callServer callback
- * This is not a public API.
- */
-export const callServerRsc = async (
-  funcId: string,
-  args: unknown[],
-  fetchCache = defaultFetchCache,
-) => {
-  const enhanceCreateData = fetchCache[ENHANCE_CREATE_DATA] || ((d) => d);
-  const createData = (responsePromise: Promise<Response>) =>
-    createFromFetch<Awaited<Elements>>(checkStatus(responsePromise), {
-      callServer: (funcId: string, args: unknown[]) =>
-        callServerRsc(funcId, args, fetchCache),
-    });
-  const url = BASE_PATH + encodeRscPath(encodeFuncId(funcId));
-  const responsePromise =
-    args.length === 1 && args[0] instanceof URLSearchParams
-      ? fetch(url + '?' + args[0])
-      : encodeReply(args).then((body) => fetch(url, { method: 'POST', body }));
-  const data = enhanceCreateData(createData)(responsePromise);
-  // FIXME this causes rerenders even if data is empty
-  fetchCache[SET_ELEMENTS]?.((prev) => mergeElements(prev, data));
-  return (await data)._value;
-};
-
-const prefetchedParams = new WeakMap<Promise<unknown>, unknown>();
-
-const fetchRscInternal = (url: string, rscParams: unknown) =>
-  rscParams === undefined
-    ? fetch(url)
-    : rscParams instanceof URLSearchParams
-      ? fetch(url + '?' + rscParams)
-      : encodeReply(rscParams).then((body) =>
-          fetch(url, { method: 'POST', body }),
-        );
-
-export const fetchRsc = (
-  rscPath: string,
-  rscParams?: unknown,
-  fetchCache = defaultFetchCache,
-): Elements => {
-  const entry = fetchCache[ENTRY];
-  if (entry && entry[0] === rscPath && entry[1] === rscParams) {
-    return entry[2];
-  }
-  const enhanceCreateData = fetchCache[ENHANCE_CREATE_DATA] || ((d) => d);
-  const createData = (responsePromise: Promise<Response>) =>
-    createFromFetch<Awaited<Elements>>(checkStatus(responsePromise), {
-      callServer: (funcId: string, args: unknown[]) =>
-        callServerRsc(funcId, args, fetchCache),
-    });
-  const prefetched = ((globalThis as any).__WAKU_PREFETCHED__ ||= {});
-  const url = BASE_PATH + encodeRscPath(rscPath);
-  const hasValidPrefetchedResponse =
-    !!prefetched[url] &&
-    // HACK .has() is for the initial hydration
-    // It's limited and may result in a wrong result. FIXME
-    (!prefetchedParams.has(prefetched[url]) ||
-      prefetchedParams.get(prefetched[url]) === rscParams);
-  const responsePromise = hasValidPrefetchedResponse
-    ? prefetched[url]
-    : fetchRscInternal(url, rscParams);
-  delete prefetched[url];
-  const data = enhanceCreateData(createData)(responsePromise);
-  // eslint-disable-next-line @typescript-eslint/no-floating-promises
-  fetchCache[ENTRY] = [rscPath, rscParams, data];
-  return data;
-};
-
-export const prefetchRsc = (rscPath: string, rscParams?: unknown): void => {
-  const prefetched = ((globalThis as any).__WAKU_PREFETCHED__ ||= {});
-  const url = BASE_PATH + encodeRscPath(rscPath);
-  if (!(url in prefetched)) {
-    prefetched[url] = fetchRscInternal(url, rscParams);
-    prefetchedParams.set(prefetched[url], rscParams);
-  }
-};
-
-const RefetchContext = createContext<
-  (rscPath: string, rscParams?: unknown) => void
->(() => {
-  throw new Error('Missing Root component');
-});
-const ElementsContext = createContext<Elements | null>(null);
-
-export const Root = ({
-  initialRscPath,
-  initialRscParams,
-  fetchCache = defaultFetchCache,
-  unstable_enhanceCreateData,
-  children,
-}: {
-  initialRscPath?: string;
-  initialRscParams?: unknown;
-  fetchCache?: FetchCache;
-  unstable_enhanceCreateData?: EnhanceCreateData;
-  children: ReactNode;
-}) => {
-  fetchCache[ENHANCE_CREATE_DATA] = unstable_enhanceCreateData;
-  const [elements, setElements] = useState(() =>
-    fetchRsc(initialRscPath || '', initialRscParams, fetchCache),
-  );
-  useEffect(() => {
-    fetchCache[SET_ELEMENTS] = setElements;
-  }, [fetchCache, setElements]);
-  const refetch = useCallback(
-    (rscPath: string, rscParams?: unknown) => {
-      // clear cache entry before fetching
-      delete fetchCache[ENTRY];
-      const data = fetchRsc(rscPath, rscParams, fetchCache);
-      setElements((prev) => mergeElements(prev, data));
-    },
-    [fetchCache],
-  );
-  return createElement(
-    RefetchContext.Provider,
-    { value: refetch },
-    createElement(ElementsContext.Provider, { value: elements }, children),
-  );
-};
-
-export const useRefetch = () => use(RefetchContext);
-
-const ChildrenContext = createContext<ReactNode>(undefined);
-const ChildrenContextProvider = memo(ChildrenContext.Provider);
-
-type OuterSlotProps = {
-  elementsPromise: Elements;
-  shouldRenderPrev: ((err: unknown) => boolean) | undefined;
-  renderSlot: (elements: Record<string, ReactNode>) => ReactNode;
-  children?: ReactNode;
-};
-
-class OuterSlot extends Component<OuterSlotProps, { error?: unknown }> {
-  constructor(props: OuterSlotProps) {
-    super(props);
-    this.state = {};
-  }
-  static getDerivedStateFromError(error: unknown) {
-    return { error };
-  }
-  render() {
-    if ('error' in this.state) {
-      const e = this.state.error;
-      if (e instanceof Error && !('statusCode' in e)) {
-        // HACK we assume any error as Not Found,
-        // probably caused by history api fallback
-        (e as any).statusCode = 404;
-      }
-      if (this.props.shouldRenderPrev?.(e) && this.props.elementsPromise.prev) {
-        const elements = this.props.elementsPromise.prev;
-        return this.props.renderSlot(elements);
-      } else {
-        throw e;
-      }
-    }
-    return this.props.children;
-  }
-}
-
-const InnerSlot = ({
-  elementsPromise,
-  renderSlot,
-}: {
-  elementsPromise: Elements;
-  renderSlot: (elements: Record<string, ReactNode>) => ReactNode;
-}) => {
-  const elements = use(elementsPromise);
-  return renderSlot(elements);
-};
-
-export const Slot = ({
-  id,
-  children,
-  fallback,
-  unstable_shouldRenderPrev,
-}: {
-  id: string;
-  children?: ReactNode;
-  fallback?: ReactNode;
-  unstable_shouldRenderPrev?: (err: unknown) => boolean;
-}) => {
-  const elementsPromise = use(ElementsContext);
-  if (!elementsPromise) {
-    throw new Error('Missing Root component');
-  }
-  const renderSlot = (elements: Record<string, ReactNode>) => {
-    if (!(id in elements)) {
-      if (fallback) {
-        return fallback;
-      }
-      throw new Error('Not found: ' + id);
-    }
-    return createElement(
-      ChildrenContextProvider,
-      { value: children },
-      elements[id],
-    );
-  };
-  return createElement(
-    OuterSlot,
-    {
-      elementsPromise,
-      shouldRenderPrev: unstable_shouldRenderPrev,
-      renderSlot,
-    },
-    createElement(InnerSlot, { elementsPromise, renderSlot }),
-  );
-};
-
-export const Children = () => use(ChildrenContext);
-
-/**
- * ServerRoot for SSR
- * This is not a public API.
- */
-export const ServerRoot = ({
-  elements,
-  children,
-}: {
-  elements: Elements;
-  children: ReactNode;
-}) => createElement(ElementsContext.Provider, { value: elements }, children);
+  Children as ChildrenOrig,
+  Slot as SlotOrig,
+  Root as RootOrig,
+  ServerRootInternal as ServerRootOrig,
+} from './minimal/client.js';
+/** @deprecated */
+export const Children = ChildrenOrig;
+/** @deprecated */
+export const Slot = SlotOrig;
+/** @deprecated */
+export const Root = RootOrig;
+/** @deprecated */
+export const ServerRoot = ServerRootOrig;
diff --git a/packages/waku/src/config.ts b/packages/waku/src/config.ts
index b52820ff..c84efad2 100644
--- a/packages/waku/src/config.ts
+++ b/packages/waku/src/config.ts
@@ -41,8 +41,8 @@ export interface Config {
    * Middleware to use
    * Defaults to:
    * () => [
+   *   import('waku/middleware/context'),
    *   import('waku/middleware/dev-server'),
-   *   import('waku/middleware/headers'),
    *   import('waku/middleware/rsc'),
    *   import('waku/middleware/ssr'),
    * ]
diff --git a/packages/waku/src/lib/builder/build.ts b/packages/waku/src/lib/builder/build.ts
index 361b8a3c..f362b970 100644
--- a/packages/waku/src/lib/builder/build.ts
+++ b/packages/waku/src/lib/builder/build.ts
@@ -4,13 +4,20 @@ import { pipeline } from 'node:stream/promises';
 import { build as buildVite, resolveConfig as resolveViteConfig } from 'vite';
 import viteReact from '@vitejs/plugin-react';
 import type { LoggingFunction, RollupLog } from 'rollup';
+import type { ReactNode } from 'react';
 
 import type { Config } from '../../config.js';
 import { unstable_getPlatformObject } from '../../server.js';
-import type { BuildConfig, EntriesPrd } from '../../server.js';
+import type {
+  BuildConfig,
+  EntriesPrd,
+  EntriesDev,
+  new_defineEntries,
+} from '../../minimal/server.js';
 import type { ResolvedConfig } from '../config.js';
 import { resolveConfig } from '../config.js';
 import { EXTENSIONS } from '../constants.js';
+import { stringToStream } from '../utils/stream.js';
 import type { PathSpec } from '../utils/path.js';
 import {
   decodeFilePathFromAbsolute,
@@ -32,6 +39,11 @@ import {
   writeFile,
 } from '../utils/node-fs.js';
 import { encodeRscPath, generatePrefetchCode } from '../renderers/utils.js';
+import {
+  collectClientModules,
+  renderRsc as renderRscNew,
+} from '../renderers/rsc.js';
+import { renderHtml as renderHtmlNew } from '../renderers/html.js';
 import {
   getBuildConfig,
   getSsrConfig,
@@ -97,7 +109,13 @@ const analyzeEntries = async (rootDir: string, config: ResolvedConfig) => {
   const wakuClientDist = decodeFilePathFromAbsolute(
     joinPath(fileURLToFilePath(import.meta.url), '../../../client.js'),
   );
-  const clientFileSet = new Set<string>([wakuClientDist]);
+  const wakuMinimalClientDist = decodeFilePathFromAbsolute(
+    joinPath(fileURLToFilePath(import.meta.url), '../../../minimal/client.js'),
+  );
+  const clientFileSet = new Set<string>([
+    wakuClientDist,
+    wakuMinimalClientDist,
+  ]);
   const serverFileSet = new Set<string>();
   const fileHashMap = new Map<string, string>();
   const moduleFileMap = new Map<string, string>(); // module id -> full path
@@ -148,7 +166,7 @@ const analyzeEntries = async (rootDir: string, config: ResolvedConfig) => {
   });
   const clientEntryFiles = Object.fromEntries(
     Array.from(clientFileSet).map((fname, i) => [
-      `${DIST_ASSETS}/rsc${i}-${fileHashMap.get(fname)}`,
+      `${DIST_ASSETS}/rsc${i}-${fileHashMap.get(fname) || 'lib'}`, // FIXME 'lib' is a workaround to avoid `undefined`
       fname,
     ]),
   );
@@ -640,6 +658,251 @@ export const publicIndexHtml = ${JSON.stringify(publicIndexHtml)};
   await appendFile(distEntriesFile, code);
 };
 
+// FIXME this is too hacky
+const willEmitPublicIndexHtmlNew = async (
+  distEntries: Omit<EntriesPrd, keyof EntriesDev> & {
+    default: ReturnType<typeof new_defineEntries>;
+  },
+  buildConfig: BuildConfig,
+) => {
+  const hasConfig = buildConfig.some(({ pathname }) => {
+    const pathSpec =
+      typeof pathname === 'string' ? pathname2pathSpec(pathname) : pathname;
+    return !!getPathMapping(pathSpec, '/');
+  });
+  if (!hasConfig) {
+    return false;
+  }
+  const utils = {
+    renderRsc: () => {
+      throw new Error('Cannot render RSC in HTML build');
+    },
+    renderHtml: () => {
+      const headers = { 'content-type': 'text/html; charset=utf-8' };
+      return {
+        body: stringToStream('DUMMY'), // HACK this might not work in an edge case
+        headers,
+      };
+    },
+  };
+  const input = {
+    type: 'custom',
+    pathname: '/',
+    req: {
+      body: null,
+      url: new URL('http://localhost/'),
+      method: 'GET',
+      headers: {},
+    },
+  } as const;
+  const res = await distEntries.default.unstable_handleRequest(input, utils);
+  return !!res;
+};
+
+// TODO too long... we need to refactor and organize this function
+const emitStaticFiles = async (
+  rootDir: string,
+  config: ResolvedConfig,
+  distEntriesFile: string,
+  distEntries: Omit<EntriesPrd, keyof EntriesDev> & {
+    default: ReturnType<typeof new_defineEntries>;
+  },
+  buildConfig: BuildConfig,
+  cssAssets: string[],
+) => {
+  const unstable_modules = {
+    rsdwServer: await distEntries.loadModule('rsdw-server'),
+    rdServer: await distEntries.loadModule(CLIENT_PREFIX + 'rd-server'),
+    rsdwClient: await distEntries.loadModule(CLIENT_PREFIX + 'rsdw-client'),
+    wakuMinimalClient: await distEntries.loadModule(
+      CLIENT_PREFIX + 'waku-minimal-client',
+    ),
+  };
+  const basePrefix = config.basePath + config.rscBase + '/';
+  const publicIndexHtmlFile = joinPath(
+    rootDir,
+    config.distDir,
+    DIST_PUBLIC,
+    'index.html',
+  );
+  const publicIndexHtml = await readFile(publicIndexHtmlFile, {
+    encoding: 'utf8',
+  });
+  if (await willEmitPublicIndexHtmlNew(distEntries, buildConfig)) {
+    await unlink(publicIndexHtmlFile);
+  }
+  const publicIndexHtmlHead = publicIndexHtml.replace(
+    /.*?<head>(.*?)<\/head>.*/s,
+    '$1',
+  );
+  const dynamicHtmlPathMap = new Map<PathSpec, string>();
+  await Promise.all(
+    Array.from(buildConfig).map(
+      async ({ pathname, isStatic, entries, customCode }) => {
+        const moduleIdsForPrefetch = new Set<string>();
+        for (const { rscPath, isStatic } of entries || []) {
+          if (!isStatic) {
+            continue;
+          }
+          const destRscFile = joinPath(
+            rootDir,
+            config.distDir,
+            DIST_PUBLIC,
+            config.rscBase,
+            encodeRscPath(rscPath),
+          );
+          // Skip if the file already exists.
+          if (existsSync(destRscFile)) {
+            continue;
+          }
+          await mkdir(joinPath(destRscFile, '..'), { recursive: true });
+          const utils = {
+            renderRsc: (elements: Record<string, unknown>) =>
+              renderRscNew(config, { unstable_modules }, elements, (id) =>
+                moduleIdsForPrefetch.add(id),
+              ),
+            renderHtml: () => {
+              throw new Error('Cannot render HTML in RSC build');
+            },
+          };
+          const input = {
+            type: 'component',
+            rscPath,
+            rscParams: undefined,
+            req: {
+              body: null,
+              url: new URL(
+                'http://localhost/' +
+                  config.rscBase +
+                  '/' +
+                  encodeRscPath(rscPath),
+              ),
+              method: 'GET',
+              headers: {},
+            },
+          } as const;
+          const res = await distEntries.default.unstable_handleRequest(
+            input,
+            utils,
+          );
+          const rscReadable = res instanceof ReadableStream ? res : res?.body;
+          await pipeline(
+            Readable.fromWeb(rscReadable as never),
+            createWriteStream(destRscFile),
+          );
+        }
+        const pathSpec =
+          typeof pathname === 'string' ? pathname2pathSpec(pathname) : pathname;
+        let htmlStr = publicIndexHtml;
+        let htmlHead = publicIndexHtmlHead;
+        if (cssAssets.length) {
+          const cssStr = cssAssets
+            .map(
+              (asset) =>
+                `<link rel="stylesheet" href="${config.basePath}${asset}">`,
+            )
+            .join('\n');
+          // HACK is this too naive to inject style code?
+          htmlStr = htmlStr.replace(/<\/head>/, cssStr);
+          htmlHead += cssStr;
+        }
+        const rscPathsForPrefetch = new Set<string>();
+        for (const { rscPath, skipPrefetch } of entries || []) {
+          if (!skipPrefetch) {
+            rscPathsForPrefetch.add(rscPath);
+          }
+        }
+        const code =
+          generatePrefetchCode(
+            basePrefix,
+            rscPathsForPrefetch,
+            moduleIdsForPrefetch,
+          ) + (customCode || '');
+        if (code) {
+          // HACK is this too naive to inject script code?
+          htmlStr = htmlStr.replace(
+            /<\/head>/,
+            `<script type="module" async>${code}</script></head>`,
+          );
+          htmlHead += `<script type="module" async>${code}</script>`;
+        }
+        if (!isStatic) {
+          dynamicHtmlPathMap.set(pathSpec, htmlHead);
+          return;
+        }
+        pathname = pathSpec2pathname(pathSpec);
+        const destHtmlFile = joinPath(
+          rootDir,
+          config.distDir,
+          DIST_PUBLIC,
+          extname(pathname)
+            ? pathname
+            : pathname === '/404'
+              ? '404.html' // HACK special treatment for 404, better way?
+              : pathname + '/index.html',
+        );
+        // In partial mode, skip if the file already exists.
+        if (existsSync(destHtmlFile)) {
+          return;
+        }
+        const utils = {
+          renderRsc: (elements: Record<string, unknown>) =>
+            renderRscNew(config, { unstable_modules }, elements),
+          renderHtml: (
+            elements: Record<string, ReactNode>,
+            html: ReactNode,
+            rscPath: string,
+          ) => {
+            const readable = renderHtmlNew(
+              config,
+              { unstable_modules },
+              htmlHead,
+              elements,
+              html,
+              rscPath,
+            );
+            const headers = { 'content-type': 'text/html; charset=utf-8' };
+            return {
+              body: readable,
+              headers,
+            };
+          },
+        };
+        const input = {
+          type: 'custom',
+          pathname,
+          req: {
+            body: null,
+            url: new URL('http://localhost' + pathname),
+            method: 'GET',
+            headers: {},
+          },
+        } as const;
+        const res = await distEntries.default.unstable_handleRequest(
+          input,
+          utils,
+        );
+        const htmlReadable = res instanceof ReadableStream ? res : res?.body;
+        await mkdir(joinPath(destHtmlFile, '..'), { recursive: true });
+        if (htmlReadable) {
+          await pipeline(
+            Readable.fromWeb(htmlReadable as never),
+            createWriteStream(destHtmlFile),
+          );
+        } else {
+          await writeFile(destHtmlFile, htmlStr);
+        }
+      },
+    ),
+  );
+  const dynamicHtmlPaths = Array.from(dynamicHtmlPathMap);
+  const code = `
+export const dynamicHtmlPaths = ${JSON.stringify(dynamicHtmlPaths)};
+export const publicIndexHtml = ${JSON.stringify(publicIndexHtml)};
+`;
+  await appendFile(distEntriesFile, code);
+};
+
 // For Deploy
 // FIXME Is this a good approach? I wonder if there's something missing.
 const buildDeploy = async (rootDir: string, config: ResolvedConfig) => {
@@ -750,27 +1013,49 @@ export async function build(options: {
   const distEntries = await import(filePathToFileURL(distEntriesFile));
 
   // TODO: Add progress indication for static builds.
-  const buildConfig = await getBuildConfig(
-    { env, config },
-    { entries: distEntries },
-  );
-  const { getClientModules } = await emitRscFiles(
-    rootDir,
-    env,
-    config,
-    distEntries,
-    buildConfig,
-  );
-  await emitHtmlFiles(
-    rootDir,
-    env,
-    config,
-    distEntriesFile,
-    distEntries,
-    buildConfig,
-    getClientModules,
-    clientBuildOutput,
-  );
+  if ('unstable_handleRequest' in distEntries.default) {
+    const buildConfig = await distEntries.default.unstable_getBuildConfig({
+      unstable_collectClientModules: (elements: never) =>
+        collectClientModules(
+          config,
+          distEntries.loadModule('rsdw-server'), // FIXME hard-coded id
+          elements,
+        ),
+    });
+    const cssAssets = clientBuildOutput.output.flatMap(({ type, fileName }) =>
+      type === 'asset' && fileName.endsWith('.css') ? [fileName] : [],
+    );
+    await emitStaticFiles(
+      rootDir,
+      config,
+      distEntriesFile,
+      distEntries,
+      buildConfig,
+      cssAssets,
+    );
+  } else {
+    const buildConfig = await getBuildConfig(
+      { env, config },
+      { entries: distEntries },
+    );
+    const { getClientModules } = await emitRscFiles(
+      rootDir,
+      env,
+      config,
+      distEntries,
+      buildConfig,
+    );
+    await emitHtmlFiles(
+      rootDir,
+      env,
+      config,
+      distEntriesFile,
+      distEntries,
+      buildConfig,
+      getClientModules,
+      clientBuildOutput,
+    );
+  }
 
   platformObject.buildOptions.unstable_phase = 'buildDeploy';
   await buildDeploy(rootDir, config);
diff --git a/packages/waku/src/lib/config.ts b/packages/waku/src/lib/config.ts
index 401173d8..1dfc11ad 100644
--- a/packages/waku/src/lib/config.ts
+++ b/packages/waku/src/lib/config.ts
@@ -8,9 +8,14 @@ type DeepRequired<T> = T extends (...args: any[]) => any
 
 export type ResolvedConfig = DeepRequired<Config>;
 
+export type PureConfig = Omit<
+  DeepRequired<Config>,
+  'middleware' | 'unstable_honoEnhancer'
+>;
+
 const DEFAULT_MIDDLEWARE = () => [
+  import('waku/middleware/context'),
   import('waku/middleware/dev-server'),
-  import('waku/middleware/headers'),
   import('waku/middleware/rsc'),
   import('waku/middleware/ssr'),
 ];
diff --git a/packages/waku/src/lib/hono/ctx.ts b/packages/waku/src/lib/hono/ctx.ts
index a71fa173..5148ec8b 100644
--- a/packages/waku/src/lib/hono/ctx.ts
+++ b/packages/waku/src/lib/hono/ctx.ts
@@ -1,12 +1,13 @@
 import type { Context, Env } from 'hono';
 
-import { unstable_getCustomContext } from '../../server.js';
+// This can't be relative import
+import { getContext } from 'waku/middleware/context';
 
 // Internal context key
 const HONO_CONTEXT = '__hono_context';
 
 export const getHonoContext = <E extends Env = Env>() => {
-  const c = unstable_getCustomContext()[HONO_CONTEXT];
+  const c = getContext().data[HONO_CONTEXT];
   if (!c) {
     throw new Error('Hono context is not available');
   }
diff --git a/packages/waku/src/lib/hono/engine.ts b/packages/waku/src/lib/hono/engine.ts
index 71e0fa6a..cdb3740b 100644
--- a/packages/waku/src/lib/hono/engine.ts
+++ b/packages/waku/src/lib/hono/engine.ts
@@ -6,13 +6,6 @@ import type { HandlerContext, MiddlewareOptions } from '../middleware/types.js';
 // Internal context key
 const HONO_CONTEXT = '__hono_context';
 
-const createEmptyReadableStream = () =>
-  new ReadableStream({
-    start(controller) {
-      controller.close();
-    },
-  });
-
 // serverEngine returns hono middleware that runs Waku middleware.
 export const serverEngine = (options: MiddlewareOptions): MiddlewareHandler => {
   const entriesPromise =
@@ -35,7 +28,7 @@ export const serverEngine = (options: MiddlewareOptions): MiddlewareHandler => {
   return async (c, next) => {
     const ctx: HandlerContext = {
       req: {
-        body: c.req.raw.body || createEmptyReadableStream(),
+        body: c.req.raw.body,
         url: new URL(c.req.url),
         method: c.req.method,
         headers: c.req.header(),
@@ -44,6 +37,9 @@ export const serverEngine = (options: MiddlewareOptions): MiddlewareHandler => {
       context: {
         [HONO_CONTEXT]: c,
       },
+      data: {
+        [HONO_CONTEXT]: c,
+      },
     };
     const handlers = await handlersPromise;
     const run = async (index: number) => {
diff --git a/packages/waku/src/lib/middleware/context.ts b/packages/waku/src/lib/middleware/context.ts
new file mode 100644
index 00000000..a155ed55
--- /dev/null
+++ b/packages/waku/src/lib/middleware/context.ts
@@ -0,0 +1,53 @@
+import type { AsyncLocalStorage as AsyncLocalStorageType } from 'node:async_hooks';
+
+import type { HandlerReq, Middleware } from './types.js';
+
+type Context = {
+  readonly req: HandlerReq;
+  readonly data: Record<string, unknown>;
+};
+
+let contextStorage: AsyncLocalStorageType<Context> | undefined;
+
+try {
+  const { AsyncLocalStorage } = await import('node:async_hooks');
+  contextStorage = new AsyncLocalStorage();
+} catch {
+  console.warn('AsyncLocalStorage is not available');
+}
+
+let previousContext: Context | undefined;
+let currentContext: Context | undefined;
+
+const runWithContext = <T>(context: Context, fn: () => T): T => {
+  if (contextStorage) {
+    return contextStorage.run(context, fn);
+  }
+  previousContext = currentContext;
+  currentContext = context;
+  try {
+    return fn();
+  } finally {
+    currentContext = previousContext;
+  }
+};
+
+export const context: Middleware = () => {
+  return async (ctx, next) => {
+    const context: Context = {
+      req: ctx.req,
+      data: ctx.data,
+    };
+    return runWithContext(context, next);
+  };
+};
+
+export function getContext() {
+  const context = contextStorage?.getStore() ?? currentContext;
+  if (!context) {
+    throw new Error(
+      'Context is not available. Make sure to use the context middleware.',
+    );
+  }
+  return context;
+}
diff --git a/packages/waku/src/lib/middleware/dev-server-impl.ts b/packages/waku/src/lib/middleware/dev-server-impl.ts
index b2863882..7ce7d139 100644
--- a/packages/waku/src/lib/middleware/dev-server-impl.ts
+++ b/packages/waku/src/lib/middleware/dev-server-impl.ts
@@ -4,7 +4,7 @@ import { AsyncLocalStorage } from 'node:async_hooks';
 import { createServer as createViteServer } from 'vite';
 import viteReact from '@vitejs/plugin-react';
 
-import type { EntriesDev } from '../../server.js';
+import type { EntriesDev } from '../../minimal/server.js';
 import { resolveConfig } from '../config.js';
 import { SRC_MAIN, SRC_ENTRIES } from '../constants.js';
 import {
@@ -247,6 +247,7 @@ const createRscViteServer = (
           conditions: ['react-server'],
           externalConditions: ['react-server'],
         },
+        external: ['waku/middleware/context'],
         noExternal: /^(?!node:)/,
         optimizeDeps: {
           include: [
@@ -336,10 +337,10 @@ export const devServer: Middleware = (options) => {
   let initialModules: ClonableModuleNode[];
 
   return async (ctx, next) => {
-    const [{ middleware: _removed, ...config }, vite] = await Promise.all([
-      configPromise,
-      vitePromise,
-    ]);
+    const [
+      { middleware: _removed1, unstable_honoEnhancer: _removed2, ...config },
+      vite,
+    ] = await Promise.all([configPromise, vitePromise]);
 
     if (!initialModules) {
       const processedModules = new Set<string>();
@@ -411,7 +412,9 @@ export const devServer: Middleware = (options) => {
     }
 
     const viteUrl = ctx.req.url.toString().slice(ctx.req.url.origin.length);
-    const viteReq: any = Readable.fromWeb(ctx.req.body as any);
+    const viteReq: any = ctx.req.body
+      ? Readable.fromWeb(ctx.req.body as never)
+      : Readable.from([]);
     viteReq.method = ctx.req.method;
     viteReq.url = viteUrl;
     viteReq.headers = ctx.req.headers;
diff --git a/packages/waku/src/lib/middleware/fallback.ts b/packages/waku/src/lib/middleware/fallback.ts
index 797a4b0e..ee7e86c3 100644
--- a/packages/waku/src/lib/middleware/fallback.ts
+++ b/packages/waku/src/lib/middleware/fallback.ts
@@ -8,7 +8,8 @@ export const fallback: Middleware = (options) => {
     return async (ctx, next) => {
       if (!ctx.res.body) {
         const config = await configPromise;
-        ctx.req.url = new URL(config.basePath, ctx.req.url);
+        const newUrl = new URL(config.basePath, ctx.req.url);
+        ctx.req.url.pathname = newUrl.pathname;
       }
       return next();
     };
diff --git a/packages/waku/src/lib/middleware/handler.ts b/packages/waku/src/lib/middleware/handler.ts
new file mode 100644
index 00000000..44f57d60
--- /dev/null
+++ b/packages/waku/src/lib/middleware/handler.ts
@@ -0,0 +1,170 @@
+import type { ReactNode } from 'react';
+
+import { resolveConfig } from '../config.js';
+import type { PureConfig } from '../config.js';
+import { setAllEnvInternal } from '../../server.js';
+import type { Middleware, HandlerContext } from './types.js';
+import type { new_defineEntries } from '../../minimal/server.js';
+import { renderRsc, decodeBody } from '../renderers/rsc.js';
+import { renderHtml } from '../renderers/html.js';
+import { decodeRscPath, decodeFuncId } from '../renderers/utils.js';
+import { filePathToFileURL, getPathMapping } from '../utils/path.js';
+
+type HandleRequest = Parameters<
+  typeof new_defineEntries
+>[0]['unstable_handleRequest'];
+
+// TODO avoid copy-pasting
+const SERVER_MODULE_MAP = {
+  'rsdw-server': 'react-server-dom-webpack/server.edge',
+} as const;
+const CLIENT_MODULE_MAP = {
+  'rd-server': 'react-dom/server.edge',
+  'rsdw-client': 'react-server-dom-webpack/client.edge',
+  'waku-minimal-client': 'waku/minimal/client',
+} as const;
+const CLIENT_PREFIX = 'client/';
+
+const getInput = async (
+  config: PureConfig,
+  ctx: HandlerContext,
+  loadServerModule: (fileId: string) => Promise<unknown>,
+): Promise<Parameters<HandleRequest>[0] | null> => {
+  if (!ctx.req.url.pathname.startsWith(config.basePath)) {
+    return null;
+  }
+  const basePrefix = config.basePath + config.rscBase + '/';
+  if (ctx.req.url.pathname.startsWith(basePrefix)) {
+    const rscPath = decodeRscPath(
+      decodeURI(ctx.req.url.pathname.slice(basePrefix.length)),
+    );
+    const decodedBody = await decodeBody(ctx);
+    const funcId = decodeFuncId(rscPath);
+    if (funcId) {
+      const args = Array.isArray(decodedBody)
+        ? decodedBody
+        : decodedBody instanceof URLSearchParams
+          ? [decodedBody]
+          : [];
+      const [fileId, name] = funcId.split('#') as [string, string];
+      const mod: any = await loadServerModule(fileId);
+      return { type: 'function', fn: mod[name], args, req: ctx.req };
+    }
+    return { type: 'component', rscPath, rscParams: decodedBody, req: ctx.req };
+  }
+  return {
+    type: 'custom',
+    pathname: '/' + ctx.req.url.pathname.slice(config.basePath.length),
+    req: ctx.req,
+  };
+};
+
+export const handler: Middleware = (options) => {
+  const env = options.env || {};
+  setAllEnvInternal(env);
+  const entriesPromise =
+    options.cmd === 'start'
+      ? options.loadEntries()
+      : ('Error: loadEntries are not available' as never);
+  const configPromise =
+    options.cmd === 'start'
+      ? entriesPromise.then((entries) =>
+          entries.loadConfig().then((config) => resolveConfig(config)),
+        )
+      : resolveConfig(options.config);
+
+  return async (ctx, next) => {
+    const { unstable_devServer: devServer } = ctx;
+    const [
+      { middleware: _removed1, unstable_honoEnhancer: _removed2, ...config },
+      entriesPrd,
+    ] = await Promise.all([configPromise, entriesPromise]);
+    const entriesDev = devServer && (await devServer.loadEntriesDev(config));
+    const entries: { default: object } = devServer ? entriesDev! : entriesPrd;
+    const rsdwServer = devServer
+      ? await devServer.loadServerModuleRsc(SERVER_MODULE_MAP['rsdw-server'])
+      : await entriesPrd.loadModule('rsdw-server');
+    const rdServer = devServer
+      ? await devServer.loadServerModuleMain(CLIENT_MODULE_MAP['rd-server'])
+      : await entriesPrd.loadModule(CLIENT_PREFIX + 'rd-server');
+    const rsdwClient = devServer
+      ? await devServer.loadServerModuleMain(CLIENT_MODULE_MAP['rsdw-client'])
+      : await entriesPrd.loadModule(CLIENT_PREFIX + 'rsdw-client');
+    const wakuMinimalClient = devServer
+      ? await devServer.loadServerModuleMain(
+          CLIENT_MODULE_MAP['waku-minimal-client'],
+        )
+      : await entriesPrd.loadModule(CLIENT_PREFIX + 'waku-minimal-client');
+    ctx.unstable_modules = {
+      rsdwServer,
+      rdServer,
+      rsdwClient,
+      wakuMinimalClient,
+    };
+    const loadServerModule = (fileId: string) => {
+      if (devServer) {
+        return devServer.loadServerModuleRsc(filePathToFileURL(fileId));
+      } else {
+        return entriesPrd.loadModule(fileId + '.js');
+      }
+    };
+    const htmlHead =
+      (!devServer &&
+        entriesPrd.dynamicHtmlPaths.find(([pathSpec]) =>
+          getPathMapping(pathSpec, ctx.req.url.pathname),
+        )?.[1]) ||
+      '';
+    const transformIndexHtml =
+      devServer && (await devServer.transformIndexHtml(ctx.req.url.pathname));
+    const utils = {
+      renderRsc: (elements: Record<string, unknown>) =>
+        renderRsc(config, ctx, elements),
+      renderHtml: (
+        elements: Record<string, ReactNode>,
+        html: ReactNode,
+        rscPath: string,
+      ) => {
+        const readable = renderHtml(
+          config,
+          ctx,
+          htmlHead,
+          elements,
+          html,
+          rscPath,
+        );
+        const headers = { 'content-type': 'text/html; charset=utf-8' };
+        return {
+          body: transformIndexHtml
+            ? readable.pipeThrough(transformIndexHtml)
+            : readable,
+          headers,
+        };
+      },
+    };
+    if ('unstable_handleRequest' in entries.default) {
+      const input = await getInput(config, ctx, loadServerModule);
+      if (input) {
+        const res = await (
+          entries.default.unstable_handleRequest as HandleRequest
+        )(input, utils);
+        if (res instanceof ReadableStream) {
+          ctx.res.body = res;
+        } else if (res) {
+          if (res.body) {
+            ctx.res.body = res.body;
+          }
+          if (res.status) {
+            ctx.res.status = res.status;
+          }
+          if (res.headers) {
+            Object.assign((ctx.res.headers ||= {}), res.headers);
+          }
+        }
+        if (ctx.res.body || ctx.res.status) {
+          return;
+        }
+      }
+    }
+    await next();
+  };
+};
diff --git a/packages/waku/src/lib/middleware/headers.ts b/packages/waku/src/lib/middleware/headers.ts
deleted file mode 100644
index 72230c56..00000000
--- a/packages/waku/src/lib/middleware/headers.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import type { Middleware } from './types.js';
-
-export const REQUEST_HEADERS = '__waku_requestHeaders';
-
-export const headers: Middleware = () => {
-  return async (ctx, next) => {
-    ctx.context[REQUEST_HEADERS] = ctx.req.headers;
-    await next();
-  };
-};
diff --git a/packages/waku/src/lib/middleware/rsc.ts b/packages/waku/src/lib/middleware/rsc.ts
index ee92e860..bcb27578 100644
--- a/packages/waku/src/lib/middleware/rsc.ts
+++ b/packages/waku/src/lib/middleware/rsc.ts
@@ -19,10 +19,10 @@ export const rsc: Middleware = (options) => {
       : resolveConfig(options.config);
 
   return async (ctx, next) => {
-    const [{ middleware: _removed, ...config }, entries] = await Promise.all([
-      configPromise,
-      entriesPromise,
-    ]);
+    const [
+      { middleware: _removed1, unstable_honoEnhancer: _removed2, ...config },
+      entries,
+    ] = await Promise.all([configPromise, entriesPromise]);
     const basePrefix = config.basePath + config.rscBase + '/';
     if (ctx.req.url.pathname.startsWith(basePrefix)) {
       const { headers } = ctx.req;
diff --git a/packages/waku/src/lib/middleware/ssr.ts b/packages/waku/src/lib/middleware/ssr.ts
index 06e29095..09179dfe 100644
--- a/packages/waku/src/lib/middleware/ssr.ts
+++ b/packages/waku/src/lib/middleware/ssr.ts
@@ -22,10 +22,10 @@ export const ssr: Middleware = (options) => {
 
   return async (ctx, next) => {
     const { unstable_devServer: devServer } = ctx;
-    const [{ middleware: _removed, ...config }, entries] = await Promise.all([
-      configPromise,
-      entriesPromise,
-    ]);
+    const [
+      { middleware: _removed1, unstable_honoEnhancer: _removed2, ...config },
+      entries,
+    ] = await Promise.all([configPromise, entriesPromise]);
     const entriesDev = devServer && (await devServer.loadEntriesDev(config));
     try {
       const htmlHead = devServer
diff --git a/packages/waku/src/lib/middleware/types.ts b/packages/waku/src/lib/middleware/types.ts
index e09c4663..97c39196 100644
--- a/packages/waku/src/lib/middleware/types.ts
+++ b/packages/waku/src/lib/middleware/types.ts
@@ -1,13 +1,15 @@
 import type { Config } from '../../config.js';
-import type { EntriesDev, EntriesPrd } from '../../server.js';
+
+// TODO should we move this to somewhere else?
+import type { EntriesDev, EntriesPrd } from '../../minimal/server.js';
 
 export type ClonableModuleNode = { url: string; file: string };
 
 export type HandlerReq = {
-  body: ReadableStream;
-  url: URL;
-  method: string;
-  headers: Record<string, string>;
+  readonly body: ReadableStream | null;
+  readonly url: URL;
+  readonly method: string;
+  readonly headers: Readonly<Record<string, string>>;
 };
 
 export type HandlerRes = {
@@ -19,7 +21,9 @@ export type HandlerRes = {
 export type HandlerContext = {
   readonly req: HandlerReq;
   readonly res: HandlerRes;
+  /** @deprecated use `data` */
   readonly context: Record<string, unknown>;
+  readonly data: Record<string, unknown>;
   unstable_devServer?: {
     rootDir: string;
     resolveClientEntry: (id: string) => string;
@@ -30,6 +34,12 @@ export type HandlerContext = {
       pathname: string,
     ) => Promise<TransformStream<any, any>>;
   };
+  unstable_modules?: {
+    rsdwServer: unknown;
+    rdServer: unknown;
+    rsdwClient: unknown;
+    wakuMinimalClient: unknown;
+  };
 };
 
 export type Handler = (
diff --git a/packages/waku/src/lib/plugins/vite-plugin-rsc-transform.ts b/packages/waku/src/lib/plugins/vite-plugin-rsc-transform.ts
index fdde76a4..0f84d4e4 100644
--- a/packages/waku/src/lib/plugins/vite-plugin-rsc-transform.ts
+++ b/packages/waku/src/lib/plugins/vite-plugin-rsc-transform.ts
@@ -60,7 +60,7 @@ const transformClient = (
     const exportNames = collectExportNames(mod);
     let newCode = `
 import { createServerReference } from 'react-server-dom-webpack/client';
-import { callServerRsc } from 'waku/client';
+import { callServerRsc } from 'waku/minimal/client';
 `;
     for (const name of exportNames) {
       newCode += `
diff --git a/packages/waku/src/lib/renderers/html-renderer.ts b/packages/waku/src/lib/renderers/html-renderer.ts
index b2cf077b..94a6f75d 100644
--- a/packages/waku/src/lib/renderers/html-renderer.ts
+++ b/packages/waku/src/lib/renderers/html-renderer.ts
@@ -5,9 +5,9 @@ import type { default as RSDWClientType } from 'react-server-dom-webpack/client.
 import { injectRSCPayload } from 'rsc-html-stream/server';
 
 import type * as WakuClientType from '../../client.js';
-import type { EntriesPrd } from '../../server.js';
+import type { EntriesPrd } from '../../minimal/server.js';
 import { SRC_MAIN } from '../constants.js';
-import type { ResolvedConfig } from '../config.js';
+import type { PureConfig } from '../config.js';
 import { concatUint8Arrays } from '../utils/stream.js';
 import {
   joinPath,
@@ -24,6 +24,7 @@ export const CLIENT_MODULE_MAP = {
   'rd-server': 'react-dom/server.edge',
   'rsdw-client': 'react-server-dom-webpack/client.edge',
   'waku-client': 'waku/client',
+  'waku-minimal-client': 'waku/minimal/client',
 } as const;
 export const CLIENT_PREFIX = 'client/';
 
@@ -168,7 +169,7 @@ const rectifyHtml = () => {
 
 export const renderHtml = async (
   opts: {
-    config: Omit<ResolvedConfig, 'middleware'>;
+    config: PureConfig;
     pathname: string;
     searchParams: URLSearchParams;
     htmlHead: string;
diff --git a/packages/waku/src/lib/renderers/html.ts b/packages/waku/src/lib/renderers/html.ts
new file mode 100644
index 00000000..5a3560a9
--- /dev/null
+++ b/packages/waku/src/lib/renderers/html.ts
@@ -0,0 +1,269 @@
+import { createElement } from 'react';
+import type { ReactNode, FunctionComponent, ComponentProps } from 'react';
+import type * as RDServerType from 'react-dom/server.edge';
+import type { default as RSDWClientType } from 'react-server-dom-webpack/client.edge';
+import { injectRSCPayload } from 'rsc-html-stream/server';
+
+import type * as WakuMinimalClientType from '../../minimal/client.js';
+import type { PureConfig } from '../config.js';
+import { SRC_MAIN } from '../constants.js';
+import { concatUint8Arrays, streamFromPromise } from '../utils/stream.js';
+import {
+  joinPath,
+  filePathToFileURL,
+  fileURLToFilePath,
+  encodeFilePathToAbsolute,
+} from '../utils/path.js';
+import { encodeRscPath } from './utils.js';
+import { renderRsc, renderRscElement } from './rsc.js';
+// TODO move types somewhere
+import type { HandlerContext } from '../middleware/types.js';
+
+// HACK depending on these constants is not ideal
+import { DEFAULT_HTML_HEAD } from '../plugins/vite-plugin-rsc-index.js';
+
+type Elements = Record<string, ReactNode>;
+
+const fakeFetchCode = `
+Promise.resolve(new Response(new ReadableStream({
+  start(c) {
+    const d = (self.__FLIGHT_DATA ||= []);
+    const t = new TextEncoder();
+    const f = (s) => c.enqueue(typeof s === 'string' ? t.encode(s) : s);
+    d.forEach(f);
+    d.push = f;
+    if (document.readyState === 'loading') {
+      document.addEventListener('DOMContentLoaded', () => c.close());
+    } else {
+      c.close();
+    }
+  }
+})))
+`
+  .split('\n')
+  .map((line) => line.trim())
+  .join('');
+
+const CLOSING_HEAD = '</head>';
+const CLOSING_BODY = '</body>';
+
+const injectHtmlHead = (
+  urlForFakeFetch: string,
+  htmlHead: string,
+  mainJsPath: string, // for DEV only, pass `''` for PRD
+) => {
+  const modifyHeadAndBody = (data: string) => {
+    const closingHeadIndex = data.indexOf(CLOSING_HEAD);
+    let [head, body] =
+      closingHeadIndex === -1
+        ? ['<head>' + CLOSING_HEAD, data]
+        : [
+            data.slice(0, closingHeadIndex + CLOSING_HEAD.length),
+            data.slice(closingHeadIndex + CLOSING_HEAD.length),
+          ];
+    head =
+      head.slice(0, -CLOSING_HEAD.length) +
+      DEFAULT_HTML_HEAD +
+      htmlHead +
+      CLOSING_HEAD;
+    const matchPrefetched = head.match(
+      // HACK This is very brittle
+      /(.*<script[^>]*>\nglobalThis\.__WAKU_PREFETCHED__ = {\n)(.*?)(\n};.*)/s,
+    );
+    if (matchPrefetched) {
+      head =
+        matchPrefetched[1] +
+        `  '${urlForFakeFetch}': ${fakeFetchCode},` +
+        matchPrefetched[3];
+    }
+    let code = `
+globalThis.__WAKU_HYDRATE__ = true;
+`;
+    if (!matchPrefetched) {
+      code += `
+globalThis.__WAKU_PREFETCHED__ = {
+  '${urlForFakeFetch}': ${fakeFetchCode},
+};
+`;
+    }
+    if (code) {
+      head =
+        head.slice(0, -CLOSING_HEAD.length) +
+        `<script type="module" async>${code}</script>` +
+        CLOSING_HEAD;
+    }
+    if (mainJsPath) {
+      const closingBodyIndex = body.indexOf(CLOSING_BODY);
+      const [firstPart, secondPart] =
+        closingBodyIndex === -1
+          ? [body, '']
+          : [body.slice(0, closingBodyIndex), body.slice(closingBodyIndex)];
+      body =
+        firstPart +
+        `<script src="${mainJsPath}" async type="module"></script>` +
+        secondPart;
+    }
+    return head + body;
+  };
+  const encoder = new TextEncoder();
+  const decoder = new TextDecoder();
+  let headSent = false;
+  let data = '';
+  return new TransformStream({
+    transform(chunk, controller) {
+      if (!(chunk instanceof Uint8Array)) {
+        throw new Error('Unknown chunk type');
+      }
+      data += decoder.decode(chunk);
+      if (!headSent) {
+        if (!/<body[^>]*>/.test(data)) {
+          return;
+        }
+        headSent = true;
+        data = modifyHeadAndBody(data);
+      }
+      controller.enqueue(encoder.encode(data));
+      data = '';
+    },
+    flush(controller) {
+      if (!headSent) {
+        headSent = true;
+        data = modifyHeadAndBody(data);
+        controller.enqueue(encoder.encode(data));
+        data = '';
+      }
+    },
+  });
+};
+
+// HACK for now, do we want to use HTML parser?
+const rectifyHtml = () => {
+  const pending: Uint8Array[] = [];
+  const decoder = new TextDecoder();
+  let timer: ReturnType<typeof setTimeout> | undefined;
+  return new TransformStream({
+    transform(chunk, controller) {
+      if (!(chunk instanceof Uint8Array)) {
+        throw new Error('Unknown chunk type');
+      }
+      pending.push(chunk);
+      if (/<\/\w+>$/.test(decoder.decode(chunk))) {
+        clearTimeout(timer);
+        timer = setTimeout(() => {
+          controller.enqueue(concatUint8Arrays(pending.splice(0)));
+        });
+      }
+    },
+    flush(controller) {
+      clearTimeout(timer);
+      if (pending.length) {
+        controller.enqueue(concatUint8Arrays(pending.splice(0)));
+      }
+    },
+  });
+};
+
+export function renderHtml(
+  config: PureConfig,
+  ctx: Pick<HandlerContext, 'unstable_modules' | 'unstable_devServer'>,
+  htmlHead: string,
+  elements: Elements,
+  html: ReactNode,
+  rscPath: string,
+): ReadableStream {
+  const modules = ctx.unstable_modules;
+  if (!modules) {
+    throw new Error('handler middleware required (missing modules)');
+  }
+  const {
+    default: { renderToReadableStream },
+  } = modules.rdServer as { default: typeof RDServerType };
+  const {
+    default: { createFromReadableStream },
+  } = modules.rsdwClient as { default: typeof RSDWClientType };
+  const { ServerRootInternal: ServerRoot } =
+    modules.wakuMinimalClient as typeof WakuMinimalClientType;
+
+  const stream = renderRsc(config, ctx, elements);
+  const htmlStream = renderRscElement(config, ctx, html);
+  const isDev = !!ctx.unstable_devServer;
+  const rootDir = ctx.unstable_devServer?.rootDir || '';
+  const moduleMap = new Proxy(
+    {} as Record<string, Record<string, ImportManifestEntry>>,
+    {
+      get(_target, filePath: string) {
+        return new Proxy(
+          {},
+          {
+            get(_target, name: string) {
+              if (isDev) {
+                // TODO too long, we need to refactor this logic
+                let file = filePath
+                  .slice(config.basePath.length)
+                  .split('?')[0]!;
+                const isFsPath = file.startsWith('@fs/');
+                file = isFsPath ? file.slice('@fs'.length) : file;
+                const fileWithAbsolutePath = isFsPath
+                  ? file
+                  : encodeFilePathToAbsolute(joinPath(rootDir, file));
+                const wakuDist = joinPath(
+                  fileURLToFilePath(import.meta.url),
+                  '../../..',
+                );
+                if (fileWithAbsolutePath.startsWith(wakuDist)) {
+                  const id =
+                    'waku' +
+                    fileWithAbsolutePath
+                      .slice(wakuDist.length)
+                      .replace(/\.\w+$/, '');
+                  (globalThis as any).__WAKU_CLIENT_CHUNK_LOAD__(id);
+                  return { id, chunks: [id], name };
+                }
+                const id = filePathToFileURL(file);
+                (globalThis as any).__WAKU_CLIENT_CHUNK_LOAD__(id);
+                return { id, chunks: [id], name };
+              }
+              // !isDev
+              const id = filePath.slice(config.basePath.length);
+              (globalThis as any).__WAKU_CLIENT_CHUNK_LOAD__(id);
+              return { id, chunks: [id], name };
+            },
+          },
+        );
+      },
+    },
+  );
+  const [stream1, stream2] = stream.tee();
+  const elementsPromise: Promise<Elements> = createFromReadableStream(stream1, {
+    ssrManifest: { moduleMap, moduleLoading: null },
+  });
+  const htmlNode: Promise<ReactNode> = createFromReadableStream(htmlStream, {
+    ssrManifest: { moduleMap, moduleLoading: null },
+  });
+  const readable = streamFromPromise(
+    renderToReadableStream(
+      createElement(
+        ServerRoot as FunctionComponent<
+          Omit<ComponentProps<typeof ServerRoot>, 'children'>
+        >,
+        { elements: elementsPromise },
+        htmlNode as any,
+      ),
+      {
+        onError(err: unknown) {
+          console.error(err);
+        },
+      },
+    ),
+  )
+    .pipeThrough(rectifyHtml())
+    .pipeThrough(
+      injectHtmlHead(
+        config.basePath + config.rscBase + '/' + encodeRscPath(rscPath),
+        htmlHead,
+        isDev ? `${config.basePath}${config.srcDir}/${SRC_MAIN}` : '',
+      ),
+    )
+    .pipeThrough(injectRSCPayload(stream2));
+  return readable;
+}
diff --git a/packages/waku/src/lib/renderers/rsc-renderer.ts b/packages/waku/src/lib/renderers/rsc-renderer.ts
index e9aac4e9..45ca3149 100644
--- a/packages/waku/src/lib/renderers/rsc-renderer.ts
+++ b/packages/waku/src/lib/renderers/rsc-renderer.ts
@@ -3,12 +3,11 @@ import type { default as RSDWServerType } from 'react-server-dom-webpack/server.
 
 import { unstable_getPlatformObject } from '../../server.js';
 import type {
-  EntriesDev,
-  EntriesPrd,
   setAllEnvInternal as setAllEnvInternalType,
   runWithRenderStoreInternal as runWithRenderStoreInternalType,
 } from '../../server.js';
-import type { ResolvedConfig } from '../config.js';
+import type { EntriesDev, EntriesPrd } from '../../minimal/server.js';
+import type { PureConfig } from '../config.js';
 import { filePathToFileURL } from '../utils/path.js';
 import { streamToArrayBuffer } from '../utils/stream.js';
 import { decodeFuncId } from '../renderers/utils.js';
@@ -25,12 +24,12 @@ const resolveClientEntryForPrd = (id: string, config: { basePath: string }) => {
 
 export type RenderRscArgs = {
   env: Record<string, string>;
-  config: Omit<ResolvedConfig, 'middleware'>;
+  config: PureConfig;
   rscPath: string;
   context: Record<string, unknown> | undefined;
   // TODO we hope to get only decoded one
   decodedBody?: unknown;
-  body?: ReadableStream | undefined;
+  body?: ReadableStream | null;
   contentType?: string | undefined;
   moduleIdCallback?: ((id: string) => void) | undefined;
   onError?: (err: unknown) => void;
@@ -231,7 +230,7 @@ export async function renderRsc(
 
 type GetBuildConfigArgs = {
   env: Record<string, string>;
-  config: Omit<ResolvedConfig, 'middleware'>;
+  config: PureConfig;
 };
 
 type GetBuildConfigOpts = { entries: EntriesPrd };
@@ -300,7 +299,7 @@ export async function getBuildConfig(
 
 export type GetSsrConfigArgs = {
   env: Record<string, string>;
-  config: Omit<ResolvedConfig, 'middleware'>;
+  config: PureConfig;
   pathname: string;
   searchParams: URLSearchParams;
 };
diff --git a/packages/waku/src/lib/renderers/rsc.ts b/packages/waku/src/lib/renderers/rsc.ts
new file mode 100644
index 00000000..c54b61a3
--- /dev/null
+++ b/packages/waku/src/lib/renderers/rsc.ts
@@ -0,0 +1,152 @@
+import type { ReactNode } from 'react';
+import type { default as RSDWServerType } from 'react-server-dom-webpack/server.edge';
+
+import type { PureConfig } from '../config.js';
+// TODO move types somewhere
+import type { HandlerContext } from '../middleware/types.js';
+import { filePathToFileURL } from '../utils/path.js';
+import { streamToArrayBuffer } from '../utils/stream.js';
+import { bufferToString, parseFormData } from '../utils/buffer.js';
+
+const resolveClientEntryForPrd = (id: string, config: { basePath: string }) => {
+  return config.basePath + id + '.js';
+};
+
+export function renderRsc(
+  config: PureConfig,
+  ctx: Pick<HandlerContext, 'unstable_modules' | 'unstable_devServer'>,
+  elements: Record<string, unknown>,
+  moduleIdCallback?: (id: string) => void,
+): ReadableStream {
+  const modules = ctx.unstable_modules;
+  if (!modules) {
+    throw new Error('handler middleware required (missing modules)');
+  }
+  const {
+    default: {
+      renderToReadableStream,
+      // decodeReply,
+    },
+  } = modules.rsdwServer as { default: typeof RSDWServerType };
+  const resolveClientEntry = ctx.unstable_devServer
+    ? ctx.unstable_devServer.resolveClientEntry
+    : resolveClientEntryForPrd;
+  const clientBundlerConfig = new Proxy(
+    {},
+    {
+      get(_target, encodedId: string) {
+        const [file, name] = encodedId.split('#') as [string, string];
+        const id = resolveClientEntry(file, config);
+        moduleIdCallback?.(id);
+        return { id, chunks: [id], name, async: true };
+      },
+    },
+  );
+  return renderToReadableStream(elements, clientBundlerConfig);
+}
+
+export function renderRscElement(
+  config: PureConfig,
+  ctx: Pick<HandlerContext, 'unstable_modules' | 'unstable_devServer'>,
+  element: ReactNode,
+): ReadableStream {
+  const modules = ctx.unstable_modules;
+  if (!modules) {
+    throw new Error('handler middleware required (missing modules)');
+  }
+  const {
+    default: {
+      renderToReadableStream,
+      // decodeReply,
+    },
+  } = modules.rsdwServer as { default: typeof RSDWServerType };
+  const resolveClientEntry = ctx.unstable_devServer
+    ? ctx.unstable_devServer.resolveClientEntry
+    : resolveClientEntryForPrd;
+  const clientBundlerConfig = new Proxy(
+    {},
+    {
+      get(_target, encodedId: string) {
+        const [file, name] = encodedId.split('#') as [string, string];
+        const id = resolveClientEntry(file, config);
+        return { id, chunks: [id], name, async: true };
+      },
+    },
+  );
+  return renderToReadableStream(element, clientBundlerConfig);
+}
+
+export async function collectClientModules(
+  config: PureConfig,
+  rsdwServer: { default: typeof RSDWServerType },
+  elements: Record<string, unknown>,
+): Promise<string[]> {
+  const {
+    default: { renderToReadableStream },
+  } = rsdwServer;
+  const idSet = new Set<string>();
+  const clientBundlerConfig = new Proxy(
+    {},
+    {
+      get(_target, encodedId: string) {
+        const [file, name] = encodedId.split('#') as [string, string];
+        const id = resolveClientEntryForPrd(file, config);
+        idSet.add(id);
+        return { id, chunks: [id], name, async: true };
+      },
+    },
+  );
+  const readable = renderToReadableStream(elements, clientBundlerConfig);
+  await new Promise<void>((resolve, reject) => {
+    const writable = new WritableStream({
+      close() {
+        resolve();
+      },
+      abort(reason) {
+        reject(reason);
+      },
+    });
+    readable.pipeTo(writable).catch(reject);
+  });
+  return Array.from(idSet);
+}
+
+export async function decodeBody(
+  ctx: Pick<HandlerContext, 'unstable_modules' | 'unstable_devServer' | 'req'>,
+): Promise<unknown> {
+  const isDev = !!ctx.unstable_devServer;
+  const modules = ctx.unstable_modules;
+  if (!modules) {
+    throw new Error('handler middleware required (missing modules)');
+  }
+  const {
+    default: { decodeReply },
+  } = modules.rsdwServer as { default: typeof RSDWServerType };
+  const serverBundlerConfig = new Proxy(
+    {},
+    {
+      get(_target, encodedId: string) {
+        const [fileId, name] = encodedId.split('#') as [string, string];
+        const id = isDev ? filePathToFileURL(fileId) : fileId + '.js';
+        return { id, chunks: [id], name, async: true };
+      },
+    },
+  );
+  let decodedBody: unknown = ctx.req.url.searchParams;
+  if (ctx.req.body) {
+    const bodyBuf = await streamToArrayBuffer(ctx.req.body);
+    const contentType = ctx.req.headers['content-type'];
+    if (
+      typeof contentType === 'string' &&
+      contentType.startsWith('multipart/form-data')
+    ) {
+      // XXX This doesn't support streaming unlike busboy
+      const formData = await parseFormData(bodyBuf, contentType);
+      decodedBody = await decodeReply(formData, serverBundlerConfig);
+    } else if (bodyBuf.byteLength > 0) {
+      const bodyStr = bufferToString(bodyBuf);
+      decodedBody = await decodeReply(bodyStr, serverBundlerConfig);
+    }
+  }
+  return decodedBody;
+}
diff --git a/packages/waku/src/lib/utils/stream.ts b/packages/waku/src/lib/utils/stream.ts
index 5dfa4f15..1e32da6c 100644
--- a/packages/waku/src/lib/utils/stream.ts
+++ b/packages/waku/src/lib/utils/stream.ts
@@ -66,3 +66,23 @@ export const stringToStream = (str: string): ReadableStream => {
     },
   });
 };
+
+export const streamFromPromise = (promise: Promise<ReadableStream>) =>
+  new ReadableStream({
+    async start(controller) {
+      try {
+        const stream = await promise;
+        const reader = stream.getReader();
+        let result: ReadableStreamReadResult<unknown>;
+        do {
+          result = await reader.read();
+          if (result.value) {
+            controller.enqueue(result.value);
+          }
+        } while (!result.done);
+        controller.close();
+      } catch (err) {
+        controller.error(err);
+      }
+    },
+  });
diff --git a/packages/waku/src/middleware/context.ts b/packages/waku/src/middleware/context.ts
new file mode 100644
index 00000000..19fbf300
--- /dev/null
+++ b/packages/waku/src/middleware/context.ts
@@ -0,0 +1,2 @@
+export { context as default } from '../lib/middleware/context.js';
+export { getContext } from '../lib/middleware/context.js';
diff --git a/packages/waku/src/middleware/handler.ts b/packages/waku/src/middleware/handler.ts
new file mode 100644
index 00000000..d95239b5
--- /dev/null
+++ b/packages/waku/src/middleware/handler.ts
@@ -0,0 +1,1 @@
+export { handler as default } from '../lib/middleware/handler.js';
diff --git a/packages/waku/src/middleware/headers.ts b/packages/waku/src/middleware/headers.ts
deleted file mode 100644
index 6aeae397..00000000
--- a/packages/waku/src/middleware/headers.ts
+++ /dev/null
@@ -1,1 +0,0 @@
-export { headers as default } from '../lib/middleware/headers.js';
diff --git a/packages/waku/src/minimal/client.ts b/packages/waku/src/minimal/client.ts
new file mode 100644
index 00000000..da3915a1
--- /dev/null
+++ b/packages/waku/src/minimal/client.ts
@@ -0,0 +1,335 @@
+/// <reference types="react/canary" />
+'use client';
+
+import {
+  Component,
+  createContext,
+  createElement,
+  memo,
+  use,
+  useCallback,
+  useEffect,
+  useState,
+} from 'react';
+import type { ReactNode } from 'react';
+import RSDWClient from 'react-server-dom-webpack/client';
+
+import { encodeRscPath, encodeFuncId } from '../lib/renderers/utils.js';
+
+const { createFromFetch, encodeReply } = RSDWClient;
+
+declare global {
+  interface ImportMeta {
+    readonly env: Record<string, string>;
+  }
+}
+
+const BASE_PATH = `${import.meta.env?.WAKU_CONFIG_BASE_PATH}${
+  import.meta.env?.WAKU_CONFIG_RSC_BASE
+}/`;
+
+const checkStatus = async (
+  responsePromise: Promise<Response>,
+): Promise<Response> => {
+  const response = await responsePromise;
+  if (!response.ok) {
+    const err = new Error(response.statusText);
+    (err as any).statusCode = response.status;
+    throw err;
+  }
+  return response;
+};
+
+type Elements = Promise<Record<string, ReactNode>> & {
+  prev?: Record<string, ReactNode> | undefined;
+};
+
+const getCached = <T>(c: () => T, m: WeakMap<object, T>, k: object): T =>
+  (m.has(k) ? m : m.set(k, c())).get(k) as T;
+const cache1 = new WeakMap();
+const mergeElements = (a: Elements, b: Elements): Elements => {
+  const getResult = () => {
+    const promise: Elements = new Promise((resolve, reject) => {
+      Promise.all([a, b])
+        .then(([a, b]) => {
+          const nextElements = { ...a, ...b };
+          delete nextElements._value;
+          promise.prev = a;
+          resolve(nextElements);
+        })
+        .catch((e) => {
+          a.then(
+            (a) => {
+              promise.prev = a;
+              reject(e);
+            },
+            () => {
+              promise.prev = a.prev;
+              reject(e);
+            },
+          );
+        });
+    });
+    return promise;
+  };
+  const cache2 = getCached(() => new WeakMap(), cache1, a);
+  return getCached(getResult, cache2, b);
+};
+
+type SetElements = (updater: (prev: Elements) => Elements) => void;
+type EnhanceCreateData = (
+  createData: (
+    responsePromise: Promise<Response>,
+  ) => Promise<Record<string, ReactNode>>,
+) => (responsePromise: Promise<Response>) => Promise<Record<string, ReactNode>>;
+
+const ENTRY = 'e';
+const SET_ELEMENTS = 's';
+const ENHANCE_CREATE_DATA = 'd';
+
+type FetchCache = {
+  [ENTRY]?: [rscPath: string, rscParams: unknown, elements: Elements];
+  [SET_ELEMENTS]?: SetElements;
+  [ENHANCE_CREATE_DATA]?: EnhanceCreateData | undefined;
+};
+
+const defaultFetchCache: FetchCache = {};
+
+/**
+ * callServer callback
+ * This is not a public API.
+ */
+export const callServerRsc = async (
+  funcId: string,
+  args: unknown[],
+  fetchCache = defaultFetchCache,
+) => {
+  const enhanceCreateData = fetchCache[ENHANCE_CREATE_DATA] || ((d) => d);
+  const createData = (responsePromise: Promise<Response>) =>
+    createFromFetch<Awaited<Elements>>(checkStatus(responsePromise), {
+      callServer: (funcId: string, args: unknown[]) =>
+        callServerRsc(funcId, args, fetchCache),
+    });
+  const url = BASE_PATH + encodeRscPath(encodeFuncId(funcId));
+  const responsePromise =
+    args.length === 1 && args[0] instanceof URLSearchParams
+      ? fetch(url + '?' + args[0])
+      : encodeReply(args).then((body) => fetch(url, { method: 'POST', body }));
+  const data = enhanceCreateData(createData)(responsePromise);
+  // FIXME this causes rerenders even if data is empty
+  fetchCache[SET_ELEMENTS]?.((prev) => mergeElements(prev, data));
+  return (await data)._value;
+};
+
+const prefetchedParams = new WeakMap<Promise<unknown>, unknown>();
+
+const fetchRscInternal = (url: string, rscParams: unknown) =>
+  rscParams === undefined
+    ? fetch(url)
+    : rscParams instanceof URLSearchParams
+      ? fetch(url + '?' + rscParams)
+      : encodeReply(rscParams).then((body) =>
+          fetch(url, { method: 'POST', body }),
+        );
+
+export const fetchRsc = (
+  rscPath: string,
+  rscParams?: unknown,
+  fetchCache = defaultFetchCache,
+): Elements => {
+  const entry = fetchCache[ENTRY];
+  if (entry && entry[0] === rscPath && entry[1] === rscParams) {
+    return entry[2];
+  }
+  const enhanceCreateData = fetchCache[ENHANCE_CREATE_DATA] || ((d) => d);
+  const createData = (responsePromise: Promise<Response>) =>
+    createFromFetch<Awaited<Elements>>(checkStatus(responsePromise), {
+      callServer: (funcId: string, args: unknown[]) =>
+        callServerRsc(funcId, args, fetchCache),
+    });
+  const prefetched = ((globalThis as any).__WAKU_PREFETCHED__ ||= {});
+  const url = BASE_PATH + encodeRscPath(rscPath);
+  const hasValidPrefetchedResponse =
+    !!prefetched[url] &&
+    // HACK .has() is for the initial hydration
+    // It's limited and may result in a wrong result. FIXME
+    (!prefetchedParams.has(prefetched[url]) ||
+      prefetchedParams.get(prefetched[url]) === rscParams);
+  const responsePromise = hasValidPrefetchedResponse
+    ? prefetched[url]
+    : fetchRscInternal(url, rscParams);
+  delete prefetched[url];
+  const data = enhanceCreateData(createData)(responsePromise);
+  // eslint-disable-next-line @typescript-eslint/no-floating-promises
+  fetchCache[ENTRY] = [rscPath, rscParams, data];
+  return data;
+};
+
+export const prefetchRsc = (rscPath: string, rscParams?: unknown): void => {
+  const prefetched = ((globalThis as any).__WAKU_PREFETCHED__ ||= {});
+  const url = BASE_PATH + encodeRscPath(rscPath);
+  if (!(url in prefetched)) {
+    prefetched[url] = fetchRscInternal(url, rscParams);
+    prefetchedParams.set(prefetched[url], rscParams);
+  }
+};
+
+const RefetchContext = createContext<
+  (rscPath: string, rscParams?: unknown) => void
+>(() => {
+  throw new Error('Missing Root component');
+});
+const ElementsContext = createContext<Elements | null>(null);
+
+export const Root = ({
+  initialRscPath,
+  initialRscParams,
+  fetchCache = defaultFetchCache,
+  unstable_enhanceCreateData,
+  children,
+}: {
+  initialRscPath?: string;
+  initialRscParams?: unknown;
+  fetchCache?: FetchCache;
+  unstable_enhanceCreateData?: EnhanceCreateData;
+  children: ReactNode;
+}) => {
+  fetchCache[ENHANCE_CREATE_DATA] = unstable_enhanceCreateData;
+  const [elements, setElements] = useState(() =>
+    fetchRsc(initialRscPath || '', initialRscParams, fetchCache),
+  );
+  useEffect(() => {
+    fetchCache[SET_ELEMENTS] = setElements;
+  }, [fetchCache, setElements]);
+  const refetch = useCallback(
+    (rscPath: string, rscParams?: unknown) => {
+      // clear cache entry before fetching
+      delete fetchCache[ENTRY];
+      const data = fetchRsc(rscPath, rscParams, fetchCache);
+      setElements((prev) => mergeElements(prev, data));
+    },
+    [fetchCache],
+  );
+  return createElement(
+    RefetchContext.Provider,
+    { value: refetch },
+    createElement(ElementsContext.Provider, { value: elements }, children),
+  );
+};
+
+export const useRefetch = () => use(RefetchContext);
+
+const ChildrenContext = createContext<ReactNode>(undefined);
+const ChildrenContextProvider = memo(ChildrenContext.Provider);
+
+type OuterSlotProps = {
+  elementsPromise: Elements;
+  shouldRenderPrev: ((err: unknown) => boolean) | undefined;
+  renderSlot: (elements: Record<string, ReactNode>) => ReactNode;
+  children?: ReactNode;
+};
+
+class OuterSlot extends Component<OuterSlotProps, { error?: unknown }> {
+  constructor(props: OuterSlotProps) {
+    super(props);
+    this.state = {};
+  }
+  static getDerivedStateFromError(error: unknown) {
+    return { error };
+  }
+  render() {
+    if ('error' in this.state) {
+      const e = this.state.error;
+      if (e instanceof Error && !('statusCode' in e)) {
+        // HACK we assume any error as Not Found,
+        // probably caused by history api fallback
+        (e as any).statusCode = 404;
+      }
+      if (this.props.shouldRenderPrev?.(e) && this.props.elementsPromise.prev) {
+        const elements = this.props.elementsPromise.prev;
+        return this.props.renderSlot(elements);
+      } else {
+        throw e;
+      }
+    }
+    return this.props.children;
+  }
+}
+
+const InnerSlot = ({
+  elementsPromise,
+  renderSlot,
+}: {
+  elementsPromise: Elements;
+  renderSlot: (elements: Record<string, ReactNode>) => ReactNode;
+}) => {
+  const elements = use(elementsPromise);
+  return renderSlot(elements);
+};
+
+/**
+ * Slot component
+ * This is used under the Root component.
+ * Slot id is the key of elements returned by the server.
+ *
+ * If the server returns this
+ * ```
+ *   { 'foo': <div>foo</div>, 'bar': <div>bar</div> }
+ * ```
+ * then you can use this component like this
+ * ```
+ *   <Root><Slot id="foo" /><Slot id="bar" /></Root>
+ * ```
+ */
+export const Slot = ({
+  id,
+  children,
+  fallback,
+  unstable_shouldRenderPrev,
+}: {
+  id: string;
+  children?: ReactNode;
+  fallback?: ReactNode;
+  unstable_shouldRenderPrev?: (err: unknown) => boolean;
+}) => {
+  const elementsPromise = use(ElementsContext);
+  if (!elementsPromise) {
+    throw new Error('Missing Root component');
+  }
+  const renderSlot = (elements: Record<string, ReactNode>) => {
+    if (!(id in elements)) {
+      if (fallback) {
+        return fallback;
+      }
+      throw new Error('Not found: ' + id);
+    }
+    return createElement(
+      ChildrenContextProvider,
+      { value: children },
+      elements[id],
+    );
+  };
+  return createElement(
+    OuterSlot,
+    {
+      elementsPromise,
+      shouldRenderPrev: unstable_shouldRenderPrev,
+      renderSlot,
+    },
+    createElement(InnerSlot, { elementsPromise, renderSlot }),
+  );
+};
+
+export const Children = () => use(ChildrenContext);
+
+/**
+ * ServerRoot for SSR
+ * This is not a public API.
+ */
+export const ServerRootInternal = ({
+  elements,
+  children,
+}: {
+  elements: Elements;
+  children: ReactNode;
+}) => createElement(ElementsContext.Provider, { value: elements }, children);
diff --git a/packages/waku/src/minimal/server.ts b/packages/waku/src/minimal/server.ts
new file mode 100644
index 00000000..05410d7e
--- /dev/null
+++ b/packages/waku/src/minimal/server.ts
@@ -0,0 +1,103 @@
+import type { ReactNode } from 'react';
+
+import type { Config } from '../config.js';
+import type { PathSpec } from '../lib/utils/path.js';
+// TODO move types somewhere
+import type { HandlerReq, HandlerRes } from '../lib/middleware/types.js';
+
+type Elements = Record<string, ReactNode>;
+
+export type BuildConfig = {
+  pathname: string | PathSpec; // TODO drop support for string?
+  isStatic?: boolean | undefined;
+  entries?: {
+    rscPath: string;
+    skipPrefetch?: boolean | undefined;
+    isStatic?: boolean | undefined;
+  }[];
+  context?: Record<string, unknown>; // TODO remove with new_defineEntries?
+  customCode?: string; // optional code to inject TODO hope to remove this
+}[];
+
+export type RenderEntries = (
+  rscPath: string,
+  options: {
+    rscParams: unknown | undefined;
+  },
+) => Promise<Elements | null>;
+
+export type GetBuildConfig = (
+  unstable_collectClientModules: (rscPath: string) => Promise<string[]>,
+) => Promise<BuildConfig>;
+
+export type GetSsrConfig = (
+  pathname: string,
+  options: {
+    searchParams: URLSearchParams;
+  },
+) => Promise<{
+  rscPath: string;
+  rscParams?: unknown;
+  html: ReactNode;
+} | null>;
+
+export function defineEntries(
+  renderEntries: RenderEntries,
+  getBuildConfig?: GetBuildConfig,
+  getSsrConfig?: GetSsrConfig,
+) {
+  return { renderEntries, getBuildConfig, getSsrConfig };
+}
+
+export type EntriesDev = {
+  default: ReturnType<typeof defineEntries>;
+};
+
+export type EntriesPrd = EntriesDev & {
+  loadConfig: () => Promise<Config>;
+  loadModule: (id: string) => Promise<unknown>;
+  dynamicHtmlPaths: [pathSpec: PathSpec, htmlHead: string][];
+  publicIndexHtml: string;
+  buildData?: Record<string, unknown>; // must be JSON serializable
+};
+
+// -----------------------------------------------------
+// new_defineEntries
+// Eventually replaces defineEntries
+// -----------------------------------------------------
+
+type HandleRequest = (
+  input: (
+    | { type: 'component'; rscPath: string; rscParams: unknown }
+    | {
+        type: 'function';
+        fn: (...args: unknown[]) => Promise<unknown>;
+        args: unknown[];
+      }
+    | { type: 'custom'; pathname: string }
+  ) & {
+    req: HandlerReq;
+  },
+  utils: {
+    renderRsc: (elements: Record<string, unknown>) => ReadableStream;
+    renderHtml: (
+      elements: Elements,
+      html: ReactNode,
+      rscPath: string,
+    ) => {
+      body: ReadableStream;
+      headers: Record<'content-type', string>;
+    };
+  },
+) => Promise<ReadableStream | HandlerRes | null | undefined>;
+
+type new_GetBuildConfig = (utils: {
+  unstable_collectClientModules: (elements: Elements) => Promise<string[]>;
+}) => Promise<BuildConfig>;
+
+export function new_defineEntries(fns: {
+  unstable_handleRequest: HandleRequest;
+  unstable_getBuildConfig: new_GetBuildConfig;
+}) {
+  return fns;
+}
diff --git a/packages/waku/src/router/client.ts b/packages/waku/src/router/client.ts
index cb3ea4af..5f6a154e 100644
--- a/packages/waku/src/router/client.ts
+++ b/packages/waku/src/router/client.ts
@@ -23,7 +23,13 @@ import type {
   MouseEvent,
 } from 'react';
 
-import { fetchRsc, prefetchRsc, Root, Slot, useRefetch } from '../client.js';
+import {
+  fetchRsc,
+  prefetchRsc,
+  Root,
+  Slot,
+  useRefetch,
+} from '../minimal/client.js';
 import {
   getComponentIds,
   encodeRoutePath,
diff --git a/packages/waku/src/router/define-router.ts b/packages/waku/src/router/define-router.ts
index 4e2b19a6..8516f964 100644
--- a/packages/waku/src/router/define-router.ts
+++ b/packages/waku/src/router/define-router.ts
@@ -11,8 +11,8 @@ import type {
   RenderEntries,
   GetBuildConfig,
   GetSsrConfig,
-} from '../server.js';
-import { Children, Slot } from '../client.js';
+} from '../minimal/server.js';
+import { Children, Slot } from '../minimal/client.js';
 import {
   getComponentIds,
   encodeRoutePath,
diff --git a/packages/waku/src/server.ts b/packages/waku/src/server.ts
index 73fe5866..24f9c43e 100644
--- a/packages/waku/src/server.ts
+++ b/packages/waku/src/server.ts
@@ -1,77 +1,54 @@
 import type { AsyncLocalStorage as AsyncLocalStorageType } from 'node:async_hooks';
-import type { ReactNode } from 'react';
 
-import type { Config } from './config.js';
-import type { PathSpec } from './lib/utils/path.js';
-import { REQUEST_HEADERS } from './lib/middleware/headers.js';
+// This can't be relative import
+import { getContext } from 'waku/middleware/context';
 
-type Elements = Record<string, ReactNode>;
+import { defineEntries as defineEntriesOrig } from './minimal/server.js';
+/** @deprecated */
+export const defineEntries = defineEntriesOrig;
 
-export type BuildConfig = {
-  pathname: string | PathSpec; // TODO drop support for string?
-  isStatic?: boolean | undefined;
-  entries?: {
-    rscPath: string;
-    skipPrefetch?: boolean | undefined;
-    isStatic?: boolean | undefined;
-  }[];
-  context?: Record<string, unknown>;
-  customCode?: string; // optional code to inject TODO hope to remove this
-}[];
-
-export type RenderEntries = (
-  rscPath: string,
-  options: {
-    rscParams: unknown | undefined;
-  },
-) => Promise<Elements | null>;
-
-export type GetBuildConfig = (
-  unstable_collectClientModules: (rscPath: string) => Promise<string[]>,
-) => Promise<BuildConfig>;
-
-export type GetSsrConfig = (
-  pathname: string,
-  options: {
-    searchParams: URLSearchParams;
-  },
-) => Promise<{
-  rscPath: string;
-  rscParams?: unknown;
-  html: ReactNode;
-} | null>;
+/**
+ * This is an internal function and not for public use.
+ */
+export function setAllEnvInternal(newEnv: Readonly<Record<string, string>>) {
+  (globalThis as any).__WAKU_SERVER_ENV__ = newEnv;
+}
 
-export function defineEntries(
-  renderEntries: RenderEntries,
-  getBuildConfig?: GetBuildConfig,
-  getSsrConfig?: GetSsrConfig,
-) {
-  return { renderEntries, getBuildConfig, getSsrConfig };
+export function getEnv(key: string): string | undefined {
+  return (globalThis as any).__WAKU_SERVER_ENV__?.[key];
 }
 
-export type EntriesDev = {
-  default: ReturnType<typeof defineEntries>;
-};
+export function unstable_getHeaders(): Readonly<Record<string, string>> {
+  return getContext().req.headers;
+}
 
-export type EntriesPrd = EntriesDev & {
-  loadConfig: () => Promise<Config>;
-  loadModule: (id: string) => Promise<unknown>;
-  dynamicHtmlPaths: [pathSpec: PathSpec, htmlHead: string][];
-  publicIndexHtml: string;
+type PlatformObject = {
   buildData?: Record<string, unknown>; // must be JSON serializable
-};
-
-let serverEnv: Readonly<Record<string, string>> = {};
+  buildOptions?: {
+    deploy?:
+      | 'vercel-static'
+      | 'vercel-serverless'
+      | 'netlify-static'
+      | 'netlify-functions'
+      | 'cloudflare'
+      | 'partykit'
+      | 'deno'
+      | 'aws-lambda'
+      | undefined;
+    unstable_phase?:
+      | 'analyzeEntries'
+      | 'buildServerBundle'
+      | 'buildSsrBundle'
+      | 'buildClientBundle'
+      | 'buildDeploy';
+  };
+} & Record<string, unknown>;
 
-/**
- * This is an internal function and not for public use.
- */
-export function setAllEnvInternal(newEnv: typeof serverEnv) {
-  serverEnv = newEnv;
-}
+(globalThis as any).__WAKU_PLATFORM_OBJECT__ ||= {};
 
-export function getEnv(key: string): string | undefined {
-  return serverEnv[key];
+// TODO tentative name
+export function unstable_getPlatformObject(): PlatformObject {
+  return (globalThis as any).__WAKU_PLATFORM_OBJECT__;
 }
 
 type RenderStore<> = {
@@ -81,15 +58,6 @@ type RenderStore<> = {
 
 let renderStorage: AsyncLocalStorageType<RenderStore> | undefined;
 
-// TODO top-level await doesn't work. Let's revisit after supporting "use server"
-// try {
-//   const { AsyncLocalStorage } = await import('node:async_hooks');
-//   renderStorage = new AsyncLocalStorage();
-// } catch (e) {
-//   console.warn(
-//     'AsyncLocalStorage is not available, rerender and getCustomContext are only available in sync.',
-//   );
-// }
 import('node:async_hooks')
   .then(({ AsyncLocalStorage }) => {
     renderStorage = new AsyncLocalStorage();
@@ -105,6 +73,7 @@ let currentRenderStore: RenderStore | undefined;
 
 /**
  * This is an internal function and not for public use.
+ * @deprecated
  */
 export const runWithRenderStoreInternal = <T>(
   renderStore: RenderStore,
@@ -122,6 +91,7 @@ export const runWithRenderStoreInternal = <T>(
   }
 };
 
+/** @deprecated use new_defineEntries */
 export function rerender(rscPath: string, rscParams?: unknown) {
   const renderStore = renderStorage?.getStore() ?? currentRenderStore;
   if (!renderStore) {
@@ -130,6 +100,7 @@ export function rerender(rscPath: string, rscParams?: unknown) {
   renderStore.rerender(rscPath, rscParams);
 }
 
+/** @deprecated use getContext from waku/middleware/context */
 export function unstable_getCustomContext<
   CustomContext extends Record<string, unknown> = Record<string, unknown>,
 >(): CustomContext {
@@ -139,39 +110,3 @@ export function unstable_getCustomContext<
   }
   return renderStore.context as CustomContext;
 }
-
-export function unstable_getHeaders(): Record<string, string> {
-  return (unstable_getCustomContext()[REQUEST_HEADERS] || {}) as Record<
-    string,
-    string
-  >;
-}
-
-type PlatformObject = {
-  buildData?: Record<string, unknown>; // must be JSON serializable
-  buildOptions?: {
-    deploy?:
-      | 'vercel-static'
-      | 'vercel-serverless'
-      | 'netlify-static'
-      | 'netlify-functions'
-      | 'cloudflare'
-      | 'partykit'
-      | 'deno'
-      | 'aws-lambda'
-      | undefined;
-    unstable_phase?:
-      | 'analyzeEntries'
-      | 'buildServerBundle'
-      | 'buildSsrBundle'
-      | 'buildClientBundle'
-      | 'buildDeploy';
-  };
-} & Record<string, unknown>;
-
-(globalThis as any).__WAKU_PLATFORM_OBJECT__ ||= {};
-
-// TODO tentative name
-export function unstable_getPlatformObject(): PlatformObject {
-  return (globalThis as any).__WAKU_PLATFORM_OBJECT__;
-}


Test patch:
diff --git a/e2e/fixtures/broken-links/waku.config.ts b/e2e/fixtures/broken-links/waku.config.ts
index 6babe53f..d05df803 100644
--- a/e2e/fixtures/broken-links/waku.config.ts
+++ b/e2e/fixtures/broken-links/waku.config.ts
@@ -1,9 +1,9 @@
 /** @type {import('waku/config').Config} */
 export default {
   middleware: () => [
+    import('waku/middleware/context'),
     import('./src/redirects.js'),
     import('waku/middleware/dev-server'),
-    import('waku/middleware/headers'),
     import('waku/middleware/rsc'),
     import('waku/middleware/ssr'),
   ],
diff --git a/packages/waku/tests/vite-plugin-rsc-transform-internals.test.ts b/packages/waku/tests/vite-plugin-rsc-transform-internals.test.ts
index d95bab6a..2efd9ac1 100644
--- a/packages/waku/tests/vite-plugin-rsc-transform-internals.test.ts
+++ b/packages/waku/tests/vite-plugin-rsc-transform-internals.test.ts
@@ -395,7 +395,7 @@ export default async function log4(mesg) {
     expect(await transform(code, '/src/func.ts')).toMatchInlineSnapshot(`
       "
       import { createServerReference } from 'react-server-dom-webpack/client';
-      import { callServerRsc } from 'waku/client';
+      import { callServerRsc } from 'waku/minimal/client';
 
       export const log1 = createServerReference('/src/func.ts#log1', callServerRsc);