How to Use LocalCan for React Native (Expo) Development
Published on February 5, 2026   •   10 min read

How to Use LocalCan for React Native (Expo) Development

Jarek CeborskiJarek Ceborski

If you've built anything non-trivial with React Native and Expo, you've probably hit the tunnel wall.

You're working on a feature that needs a real device — maybe camera access, push notifications, or just making sure your gestures feel right outside the simulator. You run npx expo start --tunnel, wait for it to connect... and it either takes forever, gives you a new URL (again), or just drops mid-session.

This isn't a niche complaint. Expo's tunnel has been a pain point for years, with open issues going back multiple SDK versions. The underlying problem: Expo wraps @expo/ngrok under the hood, which means you're depending on a third-party tunneling service that wasn't designed for the rapid reload cycles of React Native development.

Recently, developer @bidah shared on X what a lot of RN developers have been feeling:

ROFI
ROFI@bidah

I can't live without @LocalCanApp for React Native development. Expo tunnel? It gets regenerated every time you restart the server. Also, the tunnel gets dropped very often. It's inconsistent and not reliable.

8:05 PM · Feb 5, 2026

He's right. And there's a better setup. In this post, I'll show you how to replace Expo's built-in tunnel with LocalCan for a faster, more reliable React Native development workflow.

The Problem with Expo Tunnel

When you run npx expo start --tunnel, here's what happens behind the scenes:

  1. Expo CLI spawns an ngrok process via @expo/ngrok
  2. ngrok connects to its cloud servers and creates a public URL
  3. Expo serves your JavaScript bundle through that URL
  4. Your device (or Expo Go) connects to the ngrok URL to load the app

This works in theory. In practice, developers run into three recurring issues.

URLs regenerate on every restart

Every time you stop and restart your Expo dev server, you get a new tunnel URL. That means re-scanning the QR code, updating any OAuth callback URLs, and reconfiguring webhook endpoints. During active development, you might restart the server dozens of times a day.

Connections drop frequently

The ngrok tunnel can disconnect without warning, especially on unstable networks or during long development sessions. When it drops, your app loses its connection to the bundler. Hot reload stops working. You're staring at a red error screen, restarting the tunnel, and waiting for it to reconnect.

It's slow

Tunneling through ngrok's servers adds latency to every bundle request and hot reload. Compared to LAN or local connections, tunnel mode is noticeably slower — especially for initial bundle loads, which can take 30+ seconds on larger projects.

How LocalCan Solves This

LocalCan takes a different approach. Instead of relying on Expo's built-in ngrok wrapper, you create a persistent tunnel outside of Expo that points to your dev server's port. The key differences:

  • Persistent URLs. Your public URL stays the same across restarts, across days, across projects. Configure it once, use it forever.
  • Stable connections. LocalCan maintains its own tunnel independently from the Expo process. Restarting Expo doesn't kill the tunnel.
  • Faster. In our speed tests, LocalCan outperformed ngrok significantly — which means faster bundle loads and quicker hot reloads on your physical device.
  • One-time purchase. No monthly subscription. Pay once, use it for every project.

Setting Up LocalCan with Expo

Here's the full setup, step by step.

Step 1: Start Expo Without Tunnel

Instead of --tunnel, start Expo on LAN or localhost:

Bash
npx expo start --lan

By default, Expo serves on port 8081. You can verify this in the terminal output — look for the line showing your local URL.

If you need a specific port:

Bash
npx expo start --lan --port 8081

Step 2: Create a LocalCan Domain

Open LocalCan and create a new domain:

  1. Click Add Domain
  2. Set the target to localhost:8081 (or whichever port Expo is using)
  3. Enable Public URL

LocalCan gives you a persistent URL like https://my-rn-app.localcan.dev. This URL won't change when you restart Expo.

Step 3: Connect Your Device

Now you need to tell Expo Go (or your dev client) to load the bundle from your LocalCan URL instead of discovering it automatically.

Option A: Using the Expo dev tools URL bar

In the Expo dev tools (the terminal UI), press s to switch to Expo Go or your dev client, then manually enter the URL:

Text
https://my-rn-app.localcan.dev

Option B: Using environment variables

Set EXPO_PACKAGER_PROXY_URL to your LocalCan URL before starting Expo:

Bash
EXPO_PACKAGER_PROXY_URL=https://my-rn-app.localcan.dev npx expo start --lan

This tells the Expo development client to use your tunnel URL for bundle requests.

Option C: Open in device browser

On your phone, simply open your LocalCan URL in the browser:

Text
https://my-rn-app.localcan.dev

If Expo Go is installed, it will prompt to open the project. Alternatively, you can paste the URL directly into Expo Go's "Enter URL manually" field.

Step 4: Develop Normally

That's it. Your device connects through LocalCan's tunnel, hot reload works, and the connection survives Expo restarts. You don't need --tunnel anymore.

Your terminal workflow becomes:

Bash
# First time setup (once)
# → Create LocalCan domain pointing to localhost:8081

# Every time you develop
npx expo start --lan
# → Scan QR or open URL on device — same URL every time

Real Device Testing: Why This Matters

The simulator is great for layout work and basic interactions. But there's a class of features that only make sense on a real device:

Camera and sensors. The simulator doesn't have a real camera, accelerometer, or gyroscope. If your app uses expo-camera, expo-sensors, or AR features, you need a physical device.

Push notifications. The iOS Simulator doesn't receive push notifications from Expo Push Service or APNs. Testing your notification flow end-to-end requires a real device with a real push token.

Performance profiling. The simulator runs on your Mac's CPU, which is nothing like an actual phone. Janky animations and slow list rendering often only show up on real hardware — especially mid-range Android devices.

Gestures and haptics. Multi-finger gestures, Force Touch, and haptic feedback don't translate to the simulator. If your UX depends on these, you're flying blind without a device.

In all of these scenarios, you need a reliable connection between your dev server and your phone. That's where the stability of LocalCan's tunnel makes a real difference — a dropped connection during a camera test or notification debug session is more than annoying, it's a workflow killer.

OAuth is another area where Expo tunnel's regenerating URLs cause pain. OAuth providers require you to register redirect URIs in advance. When your tunnel URL changes every restart, you're constantly updating your Google, Apple, or GitHub OAuth app configuration.

With LocalCan's persistent URL, you register it once:

Text
https://my-rn-app.localcan.dev/auth/callback

And it works every time. Your OAuth flow looks like this:

  1. User taps "Sign in with Google" in your RN app
  2. App opens the browser to Google's OAuth consent screen
  3. User authorizes, Google redirects to https://my-rn-app.localcan.dev/auth/callback
  4. Your local Expo server handles the callback and completes the auth flow

Same story for deep links. If you're testing universal links or app links that route to specific screens, a stable URL means your link configuration stays valid across development sessions.

Testing Webhooks from Your RN Backend

Many React Native apps have a backend that receives webhooks — from Stripe, Firebase, Twilio, or other services. If you're running that backend locally alongside your Expo dev server, you need a public URL for webhook delivery too.

LocalCan handles this by letting you create multiple domains on different ports:

ServiceLocal PortLocalCan Domain
Expo dev server8081https://my-rn-app.localcan.dev
Backend API3000https://my-api.localcan.dev

Both URLs are persistent. Both survive restarts. You configure Stripe to send webhooks to https://my-api.localcan.dev/webhooks/stripe once, and it keeps working.

Using .local Domains for Same-Network Testing

Not every testing scenario needs a public URL. If your phone and Mac are on the same Wi-Fi network, LocalCan's .local domains give you a cleaner alternative to LAN mode.

Instead of connecting via an IP address like http://192.168.1.42:8081 (which changes when you switch networks), create a .local domain:

  1. In LocalCan, click Add Domain
  2. Set the domain to myapp.local
  3. Point it to localhost:8081
  4. HTTPS is enabled automatically

Now your device connects to https://myapp.local:8081 — a readable, consistent URL with HTTPS. This is particularly useful when you need secure contexts for features like the Geolocation API or service workers in your web view.

Expo vs. LocalCan Tunnel: Side-by-Side

Expo Tunnel (--tunnel)LocalCan
URL persistenceNew URL every restartSame URL always
Connection stabilityDrops frequentlyIndependent from Expo process
SpeedSlower (ngrok overhead)Faster (benchmark)
SetupOne flag (--tunnel)One-time domain setup
OAuth/webhooksReconfigure on every restartConfigure once
CostFree (with ngrok limits)$67-97 one-time
Multiple servicesOne tunnel for Expo onlyMultiple domains, any port
Traffic inspectionNoneBuilt-in request inspector

The one advantage of Expo's tunnel: zero setup. You add --tunnel and it works (when it works). But the trade-off in reliability and developer experience isn't worth it for anything beyond a quick demo.

Debugging Connection Issues

If your device can't connect through LocalCan, check these in order:

  1. Is Expo running? Make sure npx expo start --lan is active and shows the local URL.
  2. Is LocalCan pointing to the right port? Expo defaults to 8081, but check your terminal output.
  3. Is the tunnel active? Check LocalCan's status indicator — green means connected.
  4. Is your device using the right URL? Double-check you're not accidentally using an old Expo tunnel URL.
  5. Firewall issues? Make sure your Mac's firewall allows incoming connections on the Expo port.
  6. For .local domains: Both your Mac and phone must be on the same Wi-Fi network.

Conclusion

Expo's built-in tunnel was a good idea with a frustrating implementation. The URL regeneration, connection drops, and speed overhead make it unreliable for serious development — especially when you're testing on real devices, debugging OAuth flows, or integrating webhooks.

LocalCan replaces Expo's tunnel with persistent URLs, stable connections, and faster performance. Set it up once, and your React Native workflow becomes: start Expo, open your app, and develop. No QR code re-scanning, no URL reconfiguration, no waiting for ngrok to connect.

Your tunnel should be invisible infrastructure, not something you fight with every day.

FAQ

Do I still need the --tunnel flag with LocalCan?

No. Start Expo with --lan or just npx expo start. LocalCan handles the tunneling independently, so you don't need Expo's built-in tunnel at all.

Does this work with Expo Dev Client (custom builds)?

Yes. The dev client connects to whatever URL you provide, whether it's a LAN address or a LocalCan tunnel URL. The setup is the same.

Can I use LocalCan with EAS Build?

EAS Build runs in the cloud and doesn't need a tunnel to your local machine. LocalCan is for local development — when you're running expo start and connecting devices to your dev server. For EAS Update testing with a local server, LocalCan works great.

What about Android development? I thought LocalCan was macOS only.

LocalCan runs on macOS, but the tunnel is accessible from any device — including Android phones and tablets. You just need the Mac running as your dev machine, which is common in React Native development.

Is LAN mode (--lan) good enough? Why bother with a tunnel?

LAN mode works when your phone and Mac are on the same network. But it falls apart when you're on a corporate network with client isolation, when you're tethering, or when you need a public URL for OAuth, webhooks, or sharing with teammates. LocalCan gives you both options — .local for same-network and public tunnels for everything else.


For persistent local domains and public URLs for your React Native projects, check out LocalCan — replace Expo's unreliable tunnel with something you can depend on.