Remix is a full-stack React framework built on web standards. Tunnel it to test actions, loaders, OAuth callbacks, and webhooks with real external services.
Quick Start
npm install -g mekong-cli
mekong auth YOUR_TOKEN# Terminal 1
npm run dev # Remix starts on http://localhost:5173 (Vite) or 3000 (classic)
# Terminal 2
mekong 5173
# → https://happy-tiger-a1b2c3d4.mekongtunnel.devRemix + Vite (default since v2.2): port
5173Remix Classic (older): port3000
Or together:
npx concurrently "remix dev" "mekong 5173"package.json Scripts
{
"scripts": {
"dev": "remix vite:dev",
"tunnel": "mekong 5173",
"dev:share": "concurrently \"remix vite:dev\" \"mekong 5173\""
}
}Custom Port
PORT=4000 remix vite:dev
mekong 4000Or in vite.config.ts:
import { vitePlugin as remix } from '@remix-run/dev'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [remix()],
server: { port: 4000 },
})Environment Variables
# .env
APP_URL=https://happy-tiger-a1b2c3d4.mekongtunnel.dev
SESSION_SECRET=your-session-secretAccess in loaders/actions:
// app/routes/_index.tsx
export async function loader() {
const appUrl = process.env.APP_URL
return { appUrl }
}Actions & Form Submissions Via Tunnel
Remix actions work through the tunnel exactly like a deployed app:
// app/routes/contact.tsx
import { ActionFunctionArgs, json } from '@remix-run/node'
import { Form, useActionData } from '@remix-run/react'
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData()
const email = formData.get('email')
// Process...
return json({ success: true })
}
export default function Contact() {
const data = useActionData<typeof action>()
return (
<Form method="post">
<input name="email" type="email" required />
<button type="submit">Submit</button>
{data?.success && <p>Submitted!</p>}
</Form>
)
}mekong 5173
# POST to https://tunnel.mekongtunnel.dev/contact → works perfectlyOAuth / Remix Auth
GITHUB_CLIENT_ID=...
GITHUB_CLIENT_SECRET=...
APP_URL=https://happy-tiger-a1b2c3d4.mekongtunnel.dev// app/services/auth.server.ts
import { Authenticator } from 'remix-auth'
import { GitHubStrategy } from 'remix-auth-github'
export const authenticator = new Authenticator(sessionStorage)
authenticator.use(
new GitHubStrategy(
{
clientID: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
callbackURL: `${process.env.APP_URL}/auth/github/callback`,
},
async ({ profile }) => profile
)
)Add to your GitHub OAuth app:
https://happy-tiger-a1b2c3d4.mekongtunnel.dev/auth/github/callbackWebhooks in Resource Routes
// app/routes/webhooks.stripe.tsx
import { ActionFunctionArgs } from '@remix-run/node'
import Stripe from 'stripe'
export async function action({ request }: ActionFunctionArgs) {
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
const body = await request.text()
const sig = request.headers.get('stripe-signature')!
const event = stripe.webhooks.constructEvent(
body, sig, process.env.STRIPE_WEBHOOK_SECRET!
)
// handle event...
return new Response('OK')
}mekong 5173
# Register webhook: https://happy-tiger-a1b2c3d4.mekongtunnel.dev/webhooks/stripeTips
- Remix's HMR/HDR dev features work locally; the tunnel proxies the served output
loaderandactionfunctions run on the server — all tunnel requests hit your local Node process- Cookie-based sessions work through the tunnel; ensure
SESSION_SECRETis set