Skip to content

Commit 7e6796e

Browse files
authored
Add Next proxy adapter foundation (#116)
* Add Next proxy adapter foundation - Adds createEmulateProxy with single-target and service-target forwarding, prefix handling, forwarded headers, and response rewriting for native runtimes. - Keeps embedded Next adapter behavior intact while adding focused proxy tests and adapter test wiring. - Updates docs and skills to distinguish embedded mode from native runtime proxy mode. * Fix Next proxy redirect handling * Fix Next proxy prefix rewriting * Harden Next proxy response rewriting
1 parent 2c707a6 commit 7e6796e

11 files changed

Lines changed: 942 additions & 34 deletions

File tree

README.md

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -713,7 +713,7 @@ The experimental native Go runtime serves the same current Resend routes, suppor
713713

714714
## Next.js Integration
715715

716-
Embed emulators directly in your Next.js app so they run on the same origin. This solves the Vercel preview deployment problem where OAuth callback URLs change with every deployment.
716+
Use `@emulators/adapter-next` to run emulator routes on the same origin as your Next.js app. Embedded mode runs JavaScript emulators directly in the app. Proxy mode forwards to a separately running native runtime.
717717

718718
### Install
719719

@@ -723,9 +723,9 @@ npm install @emulators/adapter-next @emulators/github @emulators/google
723723

724724
Only install the emulators you need. Each `@emulators/*` package is published independently.
725725

726-
### Route handler
726+
### Embedded route handler
727727

728-
Create a catch-all route that serves emulator traffic:
728+
Create a catch-all route that serves emulator traffic in-process:
729729

730730
```typescript
731731
// app/emulate/[...path]/route.ts
@@ -752,6 +752,48 @@ export const { GET, POST, PUT, PATCH, DELETE } = createEmulateHandler({
752752
})
753753
```
754754

755+
Embedded mode is the current zero-infra path for Vercel preview deployments. The emulator code runs in the Next.js function, so OAuth callback URLs can point at the preview origin.
756+
757+
### Native runtime proxy
758+
759+
Use `createEmulateProxy` when a native runtime is running separately and the Next.js route should forward requests to it:
760+
761+
```typescript
762+
// app/emulate/[...path]/route.ts
763+
import { createEmulateProxy } from '@emulators/adapter-next'
764+
765+
export const { GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS } = createEmulateProxy({
766+
targets: {
767+
resend: 'http://127.0.0.1:4018',
768+
aws: 'http://127.0.0.1:4020',
769+
},
770+
})
771+
```
772+
773+
With `targets`, the first path segment selects the service and is stripped before forwarding. `/emulate/resend/emails` forwards to `http://127.0.0.1:4018/emails`, while response `Location` headers and HTML links are rewritten back to `/emulate/resend/*`.
774+
775+
If multiple services share one native runtime URL, keep using `targets` and point each service at that runtime when the runtime expects service-local paths:
776+
777+
```typescript
778+
export const { GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS } = createEmulateProxy({
779+
targets: {
780+
resend: 'http://127.0.0.1:4000',
781+
aws: 'http://127.0.0.1:4000',
782+
},
783+
})
784+
```
785+
786+
Use single `target` only when the upstream expects every path segment after the public route prefix:
787+
788+
```typescript
789+
export const { GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS } = createEmulateProxy({
790+
routePrefix: '/emulate',
791+
target: 'http://127.0.0.1:4020',
792+
})
793+
```
794+
795+
Single target mode preserves every path segment. `/emulate/aws/sqs` forwards to `http://127.0.0.1:4020/aws/sqs`. For deployed previews, the target URL must be reachable from the Next.js serverless function.
796+
755797
### Auth.js / NextAuth configuration
756798

757799
Point your provider at the emulator paths on the same origin:

apps/web/app/api/docs-chat/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const DEFAULT_MODEL = "anthropic/claude-haiku-4.5";
1414

1515
const SYSTEM_PROMPT = `You are a helpful documentation assistant for emulate, a local drop-in replacement for Vercel, GitHub, Google, Slack, Apple, Microsoft, AWS, Okta, MongoDB Atlas, Resend, and Stripe APIs used in CI and no-network sandboxes.
1616
17-
emulate provides fully stateful, production-fidelity API emulation, not mocks. The CLI is installed as the "emulate" npm package and run via "npx emulate". It also supports a programmatic API via createEmulator and a Next.js adapter (@emulators/adapter-next) for embedding emulators in your app.
17+
emulate provides fully stateful, production-fidelity API emulation, not mocks. The CLI is installed as the "emulate" npm package and run via "npx emulate". It also supports a programmatic API via createEmulator and a Next.js adapter (@emulators/adapter-next) for embedded emulators or native runtime proxy routes in your app.
1818
1919
You have access to the full emulate documentation via the bash and readFile tools. The docs are available as markdown files in the /workspace/ directory.
2020

apps/web/app/docs/nextjs/page.mdx

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Next.js Integration
22

3-
The `@emulators/adapter-next` package embeds emulators directly into a Next.js app, running them on the same origin. This is particularly useful for Vercel preview deployments where OAuth callback URLs change with every deployment.
3+
The `@emulators/adapter-next` package supports two App Router modes. Embedded mode runs JavaScript emulators directly inside the Next.js app. Proxy mode exposes a separately running native runtime on the same origin.
44

55
## Install
66

@@ -10,9 +10,9 @@ npm install @emulators/adapter-next @emulators/github @emulators/google
1010

1111
Only install the emulators you need. Each `@emulators/*` package is published independently, keeping your serverless bundles small.
1212

13-
## Route Handler
13+
## Embedded Route Handler
1414

15-
Create a catch-all route that serves emulator traffic:
15+
Create a catch-all route that serves emulator traffic in-process:
1616

1717
```typescript
1818
// app/emulate/[...path]/route.ts
@@ -44,6 +44,50 @@ This creates the following routes:
4444
- `/emulate/github/**` serves the GitHub emulator
4545
- `/emulate/google/**` serves the Google emulator
4646

47+
Embedded mode is the current zero-infra path for Vercel preview deployments. The emulator code runs in the Next.js function, so OAuth callback URLs can point at the preview origin.
48+
49+
## Native Runtime Proxy
50+
51+
Use `createEmulateProxy` when a native runtime is running separately and the Next.js route should forward requests to it:
52+
53+
```typescript
54+
// app/emulate/[...path]/route.ts
55+
import { createEmulateProxy } from '@emulators/adapter-next'
56+
57+
export const { GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS } = createEmulateProxy({
58+
targets: {
59+
resend: 'http://127.0.0.1:4018',
60+
aws: 'http://127.0.0.1:4020',
61+
},
62+
})
63+
```
64+
65+
With `targets`, the first path segment selects the service and is stripped before forwarding. `/emulate/resend/emails` forwards to `http://127.0.0.1:4018/emails`, while response `Location` headers and HTML links are rewritten back to `/emulate/resend/*`.
66+
67+
If multiple services share one native runtime URL, keep using `targets` and point each service at that runtime when the runtime expects service-local paths:
68+
69+
```typescript
70+
export const { GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS } = createEmulateProxy({
71+
targets: {
72+
resend: 'http://127.0.0.1:4000',
73+
aws: 'http://127.0.0.1:4000',
74+
},
75+
})
76+
```
77+
78+
Use single `target` only when the upstream expects every path segment after the public route prefix:
79+
80+
```typescript
81+
export const { GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS } = createEmulateProxy({
82+
routePrefix: '/emulate',
83+
target: 'http://127.0.0.1:4020',
84+
})
85+
```
86+
87+
Single target mode preserves every path segment. `/emulate/aws/sqs` forwards to `http://127.0.0.1:4020/aws/sqs`.
88+
89+
The proxy adds `x-forwarded-host`, `x-forwarded-proto`, `x-forwarded-port` when known, `x-forwarded-prefix`, `x-emulate-proxy: next`, `x-emulate-original-path`, and `x-emulate-service` for service targets. For deployed previews, the target URL must be reachable from the Next.js serverless function.
90+
4791
## Auth.js / NextAuth Configuration
4892

4993
Point your provider at the emulator paths on the same origin:

apps/web/app/docs/page.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,4 @@ You can also use emulate as a library in your tests. See the [Programmatic API](
8585

8686
## Next.js Integration
8787

88-
Embed emulators directly in your Next.js app so they run on the same origin. See the [Next.js Integration](/docs/nextjs) page for setup instructions.
88+
Run emulator routes on the same origin as your Next.js app with embedded mode or native runtime proxy mode. See the [Next.js Integration](/docs/nextjs) page for setup instructions.

packages/@emulators/adapter-next/README.md

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# @emulators/adapter-next
22

3-
Next.js App Router integration for emulate. Embed emulators directly in your Next.js app so they run on the same origin, solving the Vercel preview deployment problem where OAuth callback URLs change with every deployment.
3+
Next.js App Router integration for emulate. Use embedded mode to run JavaScript emulators directly inside your app, or proxy mode to expose a separately running native runtime on the same origin.
44

55
Part of [emulate](https://github.com/vercel-labs/emulate) — local drop-in replacement services for CI and no-network sandboxes.
66

@@ -16,9 +16,9 @@ Only install the emulators you need alongside the adapter:
1616
npm install @emulators/adapter-next @emulators/github @emulators/google
1717
```
1818

19-
## Route handler
19+
## Embedded route handler
2020

21-
Create a catch-all route that serves emulator traffic:
21+
Create a catch-all route that serves emulator traffic in-process:
2222

2323
```typescript
2424
// app/emulate/[...path]/route.ts
@@ -45,6 +45,50 @@ export const { GET, POST, PUT, PATCH, DELETE } = createEmulateHandler({
4545
})
4646
```
4747

48+
Embedded mode is the current zero-infra path for Vercel preview deployments. The emulator code runs in the Next.js function, so OAuth callback URLs can point at the preview origin.
49+
50+
## Native runtime proxy
51+
52+
Use `createEmulateProxy` when a native runtime is running separately and the Next.js route should forward requests to it:
53+
54+
```typescript
55+
// app/emulate/[...path]/route.ts
56+
import { createEmulateProxy } from '@emulators/adapter-next'
57+
58+
export const { GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS } = createEmulateProxy({
59+
targets: {
60+
resend: 'http://127.0.0.1:4018',
61+
aws: 'http://127.0.0.1:4020',
62+
},
63+
})
64+
```
65+
66+
With `targets`, the first path segment selects the service and is stripped before forwarding. `/emulate/resend/emails` forwards to `http://127.0.0.1:4018/emails`, while response `Location` headers and HTML links are rewritten back to `/emulate/resend/*`.
67+
68+
If multiple services share one native runtime URL, keep using `targets` and point each service at that runtime when the runtime expects service-local paths:
69+
70+
```typescript
71+
export const { GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS } = createEmulateProxy({
72+
targets: {
73+
resend: 'http://127.0.0.1:4000',
74+
aws: 'http://127.0.0.1:4000',
75+
},
76+
})
77+
```
78+
79+
Use single `target` only when the upstream expects every path segment after the public route prefix:
80+
81+
```typescript
82+
export const { GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS } = createEmulateProxy({
83+
routePrefix: '/emulate',
84+
target: 'http://127.0.0.1:4020',
85+
})
86+
```
87+
88+
Single target mode preserves every path segment. `/emulate/aws/sqs` forwards to `http://127.0.0.1:4020/aws/sqs`.
89+
90+
The proxy adds `x-forwarded-host`, `x-forwarded-proto`, `x-forwarded-port` when known, `x-forwarded-prefix`, `x-emulate-proxy: next`, `x-emulate-original-path`, and `x-emulate-service` for service targets. For deployed previews, the target URL must be reachable from the Next.js serverless function.
91+
4892
## Auth.js / NextAuth configuration
4993

5094
Point your provider at the emulator paths on the same origin:

packages/@emulators/adapter-next/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"build": "tsup --clean",
3232
"dev": "tsup --watch",
3333
"clean": "rm -rf dist .turbo",
34+
"test": "vitest run",
3435
"type-check": "tsc --noEmit",
3536
"lint": "eslint src"
3637
},
@@ -39,6 +40,7 @@
3940
},
4041
"devDependencies": {
4142
"tsup": "^8",
42-
"typescript": "^5.7"
43+
"typescript": "^5.7",
44+
"vitest": "^4.1.6"
4345
}
4446
}

0 commit comments

Comments
 (0)