nuxt-cloudflared-tunnel
v0.1.1
Published
Nuxt module that opens a Cloudflare Quick Tunnel to the dev server on startup
Maintainers
Readme
nuxt-cloudflared-tunnel
Nuxt module that starts a Cloudflare Quick Tunnel to the dev server on startup, with the URL available at runtime.
Features
- Starts a Cloudflare Quick Tunnel automatically on
nuxt dev, no Cloudflare account orcloudflaredlogin required. - Tunnel URL exposed at runtime via
useRuntimeConfig()and a$tunnelUrl/$isTunnelplugin. - Automatically allows the tunnel host through Vite's dev server host check.
- Tunnel additional local services (e.g. Storybook, an API) over their own Quick Tunnels via the
storybookshorthand or a generictunnelsarray. - No-op outside
nuxt dev. Nothing added to production builds. - Configurable port, and can be disabled entirely (e.g. in CI).
Requirements
This module requires:
- Nuxt
^4.0.0 - A network path to Cloudflare's edge (Quick Tunnels are created over the open internet, so they won't work fully offline or behind an egress-restricted proxy)
Quick setup
Install the module to your Nuxt application with one command:
npx nuxt module add nuxt-cloudflared-tunnelOr install manually:
# pnpm
pnpm add -D nuxt-cloudflared-tunnel
# yarn
yarn add -D nuxt-cloudflared-tunnel
# npm
npm install -D nuxt-cloudflared-tunnelThen add it to the modules section of nuxt.config.ts:
export default defineNuxtConfig({
modules: ['nuxt-cloudflared-tunnel'],
})That's it. Run nuxt dev and the tunnel starts automatically once the dev server is listening:
🚇 Starting Cloudflare tunnel for Nuxt (:3000)...
🌐 Nuxt tunnel ready at: https://<random-words>.trycloudflare.com
🔓 Allowed tunnel host: <random-words>.trycloudflare.comThe tunnel URL is exposed to the app at runtime via useRuntimeConfig().public.cloudflaredTunnelUrl, and $tunnelUrl / $isTunnel from the runtime plugin (src/runtime/plugin.ts).
Introduction
Nuxt has decent support for tunneling via nuxi dev --tunnel (also backed by Cloudflare Quick Tunnels), but using it as a one-off CLI flag has the same limitations as running cloudflared in a second terminal by hand:
- The tunnel isn't tied to the Nuxt config or committed to the repo, so every contributor has to know to pass the flag (or run
cloudflaredmanually) themselves. - The URL isn't exposed anywhere the app can read it. Building callback URLs (OAuth redirects, Stripe/GitHub webhooks) for dynamic Quick Tunnel URLs means manually copying the printed URL around.
- There's no way to disable it per-environment (e.g. CI) via config, only by remembering to drop the flag.
This module makes the tunnel a first-class part of the Nuxt config: modules: ['nuxt-cloudflared-tunnel'], committed once, and the URL is available at runtime via useRuntimeConfig() and $tunnelUrl/$isTunnel, for exactly the callback-URL use case above.
Configuration
Configure the module at the cloudflaredTunnel key in nuxt.config.ts:
export default defineNuxtConfig({
cloudflaredTunnel: {
enabled: true, // set false to disable the tunnel entirely
port: undefined, // override the port; defaults to the dev server's detected port (falls back to 3000)
log: true, // set false to silence the module's console output
storybook: false, // set true to also tunnel a Storybook dev server on :6006
tunnels: [], // arbitrary extra tunnel targets
},
})| Option | Type | Default | Description |
| ----------- | ---------------- | ----------- | ----------------------------------------------------------------------------------------------- |
| enabled | boolean | true | Disable to skip starting a tunnel (e.g. in CI or restricted networks). |
| port | number | undefined | Force a specific local port instead of auto-detecting the dev server's. |
| log | boolean | true | Log the tunnel URL and allowed host to the console. |
| storybook | boolean | false | Shorthand to tunnel a Storybook dev server on port 6006 (5s startup delay). |
| tunnels | TunnelTarget[] | [] | Extra services to tunnel. Each { port, label, delay? } opens a separate Quick Tunnel. |
Tunneling extra services
The storybook option is a convenience shorthand for the most common case. For anything else (API servers, Storybook on a non-default port, etc.), use the generic tunnels array:
export default defineNuxtConfig({
cloudflaredTunnel: {
tunnels: [
{ port: 6006, label: 'Storybook', delay: 5_000 },
{ port: 8080, label: 'API' },
],
},
})Each target opens a separate Quick Tunnel with its own *.trycloudflare.com URL. The delay field (in ms) defers the tunnel start - useful for secondary services that take longer to boot.
Note on tunneling Storybook
In Nuxt 3/4, Storybook runs as a completely separate process (via @storybook-vue/nuxt), not inside Nuxt. The module simply opens a tunnel to whatever is listening on the configured port - it doesn't manage Storybook's lifecycle.
Cloudflare Quick Tunnels use random *.trycloudflare.com hostnames. Vite (which powers Storybook's dev server) blocks requests from unrecognized hosts by default, so you must configure Storybook's Vite to allow all hosts:
// .storybook/main.ts
viteFinal: (config) => {
config.server ??= {}
config.server.allowedHosts = true
return config
},The module handles the Nuxt side automatically (vite.server.allowedHosts = true), but Storybook's Vite config is separate.
The playground ships a working Storybook setup (config under .storybook/, sample components and stories in playground/components/) wired up with cloudflaredTunnel: { storybook: true } - run pnpm dev:all to see both services tunneled.
How it works
On the Nuxt listen hook (fired once the dev server is accepting connections), the module:
- Resolves the port to tunnel:
options.portif set, otherwise the dev server's listening port, falling back to3000if the address can't be read (e.g. a Unix socket). - Calls
startTunnel()fromuntun, accepting Cloudflare's terms non-interactively. - Awaits
tunnel.getURL()for the publichttps://*.trycloudflare.comURL. - Sets
vite.server.allowedHosts = trueso Vite's dev server accepts requests with the tunnel'sHostheader (Vite blocks unrecognized hosts by default). - Exposes the URL via
runtimeConfig.public.cloudflaredTunnelUrl.
For each extra tunnel target (including storybook: true), steps 2-3 repeat with the target's port. Targets with a delay are started via setTimeout so the primary tunnel isn't blocked.
If untun fails to start a tunnel (no tunnel returned, or the call rejects, e.g. no network access to Cloudflare's edge), the error is logged with console.error and dev server startup continues unaffected.
When the dev server shuts down, all tracked tunnels are closed via tunnel.close() on the Nuxt close hook. SIGTERM and SIGHUP handlers are also registered (in addition to untun's built-in SIGINT handling) so that non-interactive teardowns (process supervisors, terminal close) don't orphan the cloudflared child process.
FAQ
Q: Does this work in production?
A: No, by design. The module only hooks the listen event, which only fires in nuxt dev. There is no code path that starts a tunnel in a built/production app.
Q: Why does the tunnel URL change every time I restart the dev server?
A: It's a Cloudflare Quick Tunnel, which is ephemeral by design: free, anonymous, and reassigned on every connection. See Roadmap for plans around stable, named tunnels.
Q: Can I use this with nuxi dev --tunnel at the same time?
A: No, both would try to tunnel the same port. Use one or the other; this module exists so the tunnel doesn't depend on remembering a CLI flag.
Roadmap
Today this module only wraps Cloudflare Quick Tunnels: ephemeral, free, no Cloudflare account config required, but the URL changes every time the dev server restarts. That's fine for ad-hoc sharing (phone testing, a one-off webhook test) but not for anything needing a stable URL across restarts (OAuth app settings, third-party webhook configs that don't support easy URL updates).
Possible future scope, not yet implemented:
- Named Tunnel support: a fixed hostname across restarts, via Cloudflare's authenticated tunnels (requires
cloudflaredlogin plus tunnel/DNS setup the module would need to manage or document). - Custom hostname config (
cloudflaredTunnel: { hostname: 'dev.example.com' }) on top of named tunnel support. - DevTools panel integration, QR code output for mobile testing.
- Tunnel health monitoring / automatic reconnection.
None of this is built. The module is intentionally a small, focused wrapper around untun's Quick Tunnel support today.
Testing
Unit tests live in test/module.test.ts and run with pnpm test. @nuxt/kit and untun are mocked so the tests exercise the module's setup() logic directly: port resolution, the listen hook, runtime config/Vite mutation, storybook shorthand, tunnels array handling, and tunnel-start failure handling, without booting a real Nuxt instance or network tunnel.
Contribution
# Install dependencies
pnpm install
# Generate type stubs
pnpm dev:prepare
# Develop with the playground
pnpm dev
# Build the playground
pnpm dev:build
# Run Storybook (on :6006) - tunneled via the module's `storybook` option
pnpm storybook
# Build Storybook as a static site
pnpm build-storybook
# Run ESLint
pnpm lint
# Run Vitest
pnpm test
pnpm test:watchBug reports and feature requests are welcome via GitHub Issues.
