npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

expo-cloudflared

v4.2.1

Published

Cloudflare Tunnel for Expo — a drop-in replacement for `@expo/ngrok`. Powers `expo start --tunnel` using Cloudflare Tunnel instead of ngrok.

Readme

expo-cloudflared

npm TypeScript

Cloudflare Tunnel for Expo — a drop-in replacement for @expo/ngrok. Powers expo start --tunnel using Cloudflare Tunnel instead of ngrok.

  • Faster than ngrok — ngrok caps you around 1 MB/s, while Cloudflare Tunnel isn't rate-limited; on my connection it used my full 5 MB/s of bandwidth.
  • Stable URLs (named tunnels) — opt in with two env vars in .env.local if you own a domain on Cloudflare.
  • No ngrok account, no authtoken, no rate limits — quick tunnels are free with no sign-up.

How it works

Expo CLI's --tunnel flag looks for @expo/ngrok in your project's node_modules. expo-cloudflared uses your package manager's override / alias feature to install itself at the path node_modules/@expo/ngrok. Expo CLI finds it there, calls connect() and kill() — which this package implements with the same API shape — and gets a Cloudflare Tunnel URL back. Expo CLI never knows it isn't ngrok.

You do not need to install @expo/ngrok at all. If it's installed globally, the local override takes priority.


Setup for expo start --tunnel

Pick your package manager:

npm / bun

{
  "dependencies": {
    "expo-cloudflared": "^4.2.0"
  },
  "overrides": {
    "@expo/ngrok": "npm:expo-cloudflared@^4.2.0"
  }
}
npm install        # or: bun install
npx expo start --tunnel

yarn (v1 classic and v2+)

{
  "dependencies": {
    "expo-cloudflared": "^4.2.0"
  },
  "resolutions": {
    "@expo/ngrok": "npm:expo-cloudflared@^4.2.0"
  }
}
yarn install
yarn expo start --tunnel

pnpm

{
  "dependencies": {
    "expo-cloudflared": "^4.2.0"
  },
  "pnpm": {
    "overrides": {
      "@expo/ngrok": "npm:expo-cloudflared@^4.2.0"
    }
  }
}
pnpm install
pnpm expo start --tunnel

That's it — expo start --tunnel now prints an https://xxxx.trycloudflare.com URL.


Stable URLs with named tunnels

Quick tunnels get a random URL each session. If you own a domain on Cloudflare (free plan is fine), a named tunnel gives you the same URL every time — great for OAuth callbacks, webhooks, or sharing a dev build with teammates.

One-time setup:

npx expo-cloudflared setup

The wizard logs you into Cloudflare, creates the tunnel, routes a DNS hostname, and prints the two env vars to put in your Expo project's .env.local:

CLOUDFLARED_TUNNEL_NAME=my-expo-tunnel
CLOUDFLARED_TUNNEL_HOSTNAME=dev.yourdomain.com

Expo CLI loads .env.local automatically — the next expo start --tunnel serves your dev server at https://dev.yourdomain.com, with zero code changes.

Env vars reference

| Variable | Effect | | --- | --- | | CLOUDFLARED_TUNNEL_NAME | Run this named tunnel (name mode — needs the one-time setup/cloudflared tunnel login). Routes to the actual dev-server port automatically. | | CLOUDFLARED_TUNNEL_HOSTNAME | The public hostname to report as the tunnel URL (e.g. dev.yourdomain.com). | | CLOUDFLARED_TUNNEL_TOKEN | Run a dashboard-managed tunnel by token (token mode). Ingress is fixed in the Cloudflare dashboard — prefer name mode for Expo, where the port can change. | | CLOUDFLARED_VERSION | Pin the cloudflared binary release to download. Pinned versions are cached side by side, so projects pinning different versions never conflict. | | CLOUDFLARED_BIN | Use an existing cloudflared binary at this path instead of downloading. | | CLOUDFLARED_METRICS_URL | Override the cloudflared metrics server URL used by getApi(). |

Precedence: explicit connect() options → CLOUDFLARED_TUNNEL_NAMECLOUDFLARED_TUNNEL_TOKEN → quick tunnel.

Where the binary lives

The cloudflared binary (~40 MB) is cached once per device in Expo's user-level settings directory — ~/.expo/expo-cloudflared/ — not inside node_modules. Every project on the machine shares it, and it survives node_modules wipes, rm -rf .expo, and fresh installs. Pinned versions get version-suffixed filenames (cloudflared-2026.5.0) alongside the shared unpinned one.


Direct usage (without Expo CLI)

const cloudflared = require('expo-cloudflared')

// Quick tunnel — no Cloudflare account needed
const url = await cloudflared.connect(8081)
// https://xxxx.trycloudflare.com

// With options
const url2 = await cloudflared.connect({
  addr: 8081,
  proto: 'http',
  onStatusChange: (status) => console.log('Tunnel:', status), // "connected" | "closed"
  onLogEvent: (line) => console.log('[cf]', line),
})

// Named tunnel — stable URL
const url3 = await cloudflared.connect({
  tunnelName: 'my-expo-tunnel',
  hostname: 'dev.yourdomain.com',
})

// Named tunnel via dashboard token
const url4 = await cloudflared.connect({ token: process.env.CLOUDFLARED_TUNNEL_TOKEN })

await cloudflared.kill()

CLI

npx expo-cloudflared <command>

Commands:
  install                  Download the cloudflared binary (happens lazily on first tunnel otherwise)
  install --force          Re-download even if already installed
  version                  Print the installed cloudflared version
  setup                    Guided named-tunnel setup (login → create → route DNS → env vars)
  tunnel [port]            Start a quick tunnel on [port] (default: 3000)
  tunnel --token <token>   Start a token-based named tunnel
  tunnel [port] --name <name> [--hostname <host>]
                           Start a locally-managed named tunnel

API Reference

connect(options?)

Returns Promise<string> — the public HTTPS URL.

| Option | Type | Default | Description | | --- | --- | --- | --- | | addr | number \| string | 3000 | Local port or address to expose | | port | number | — | Alias for addr (used by Expo CLI internally) | | proto | "http" \| "https" \| "tcp" \| "ssh" | "http" | Protocol of the local service | | token | string | — | Cloudflare Tunnel token (token mode) | | tunnelName | string | — | Named tunnel to run (name mode) | | hostname | string | — | Public hostname reported as the URL (named modes) | | logLevel | "debug" \| "info" \| "warn" \| "error" \| "fatal" | — | cloudflared log verbosity | | metricsUrl | string | auto | cloudflared metrics server URL (passed as --metrics) | | startTimeoutMs | number | 30000 | How long to wait for the tunnel URL | | onLogEvent | (line: string) => void | — | Called for every cloudflared log line | | onStatusChange | (status) => void | — | "connected" | "closed" (closed only fires on unexpected crashes) |

@expo/ngrok fields that don't apply to Cloudflare Tunnel are accepted but ignored: authtoken, configPath, subdomain, region (and Expo's *.exp.direct hostname).

Other exports

| Export | Description | | --- | --- | | disconnect(url?) / kill() | Stop the tunnel (the url arg is ngrok-compat, ignored) | | getUrl() | Active public URL, or null | | getVersion() | Installed cloudflared binary version string | | getActiveProcess() | The cloudflared ChildProcess, or null | | getApi() | CloudflaredClient for the local metrics server (auto-discovered), or null | | ensureBinary(options?) / isInstalled() | Manage the binary programmatically | | authtoken() | No-op ngrok-compat stub | | CloudflaredError | Error class with a code (e.g. ERR_CERT_MISSING, ERR_NO_HOSTNAME) |

const api = cloudflared.getApi()
await api.healthcheck() // boolean
await api.ready()       // boolean
await api.getMetrics()  // Prometheus-format string

Two tunnel modes

| | Quick Tunnel | Named Tunnel | |-----------------------------|----------------------------------|----------------------------------------| | Cloudflare account required | No | Yes (free) + a domain | | URL stability | Random each session | Permanent | | Setup | None | One-time npx expo-cloudflared setup | | How to use with Expo | expo start --tunnel | + two env vars in .env.local | | Example URL | https://xxxx.trycloudflare.com | https://dev.yourdomain.com |


Versioning

Expo CLI only accepts an @expo/ngrok replacement whose version satisfies ^4.1.0. This package therefore stays on 4.x forever — breaking changes ship as minor bumps within 4.x, never as a 5.0.0.

Requirements

Node.js ≥ 18.17. macOS (x64/arm64), Linux (x64/arm64/arm), Windows (x64/arm64).