Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/outray-tunnel/outray/llms.txt

Use this file to discover all available pages before exploring further.

The @outray/express plugin hooks into app.listen() and starts an OutRay tunnel as soon as your server begins accepting connections — including when you use port 0 for a dynamically assigned port.
The tunnel only activates when NODE_ENV=development. It is a no-op otherwise.

Setup

1

Install the package

npm install @outray/express
2

Add the plugin to your app

Call outray(app) before app.listen():
index.js
import express from 'express'
import outray from '@outray/express'

const app = express()

outray(app)

app.listen(3000, () => {
  console.log('Server running on port 3000')
})
3

Start in development mode

Run your server with NODE_ENV=development. The tunnel URL appears in the terminal:
Server running on port 3000
  Tunnel:  https://abc123.outray.dev

How it works

The plugin patches app.listen() to intercept the listening event on the underlying HTTP server. Once the server is bound to a port — even a dynamically assigned one — the plugin reads the actual port from server.address() and opens the tunnel. No polling, no guessing.

Configuration

Pass options as the second argument to outray():
index.js
import express from 'express'
import outray from '@outray/express'

const app = express()

outray(app, {
  subdomain: 'my-api',
  apiKey: process.env.OUTRAY_API_KEY,
})

app.listen(3000)

Options

OptionTypeDefaultDescription
subdomainstringRequest a specific subdomain. Requires authentication.
customDomainstringUse a custom domain configured in the OutRay dashboard.
apiKeystringprocess.env.OUTRAY_API_KEYAPI key for authentication.
serverUrlstringwss://api.outray.dev/OutRay server WebSocket URL. Change this only for self-hosted instances.
enabledbooleanprocess.env.OUTRAY_ENABLED !== "false"Enable or disable the tunnel.
silentbooleanfalseSuppress all tunnel status logs.
localbooleanfalseEnable LAN access via mDNS (.local domain) for devices on the same network.
onTunnelReady(url: string) => voidCalled when the tunnel is established.
onLocalReady(info: LocalInfo) => voidCalled when LAN access is available.
onError(error: Error) => voidCalled when the tunnel encounters an error.
onClose() => voidCalled when the tunnel connection closes.
onReconnecting() => voidCalled when the tunnel is attempting to reconnect.

Environment variables

VariableDescription
NODE_ENVThe tunnel only starts when this is development.
OUTRAY_API_KEYAPI key for authentication — fallback for the apiKey option.
OUTRAY_SUBDOMAINSubdomain to request — fallback for the subdomain option.
OUTRAY_SERVER_URLServer WebSocket URL — fallback for the serverUrl option.
OUTRAY_ENABLEDSet to "false" to disable the tunnel without removing the plugin.

Examples

Custom subdomain

Reserve a consistent URL for your API:
index.js
import express from 'express'
import outray from '@outray/express'

const app = express()

outray(app, {
  subdomain: 'my-api',
  apiKey: process.env.OUTRAY_API_KEY,
})

app.get('/api/health', (req, res) => {
  res.json({ status: 'ok' })
})

app.listen(3000)

Custom domain

Use a domain you’ve configured in the OutRay dashboard:
index.js
import express from 'express'
import outray from '@outray/express'

const app = express()

outray(app, {
  customDomain: 'api.example.com',
  apiKey: process.env.OUTRAY_API_KEY,
})

app.listen(3000)

Dynamic port

Works with port 0 — the plugin reads the actual assigned port from server.address():
index.js
import express from 'express'
import outray from '@outray/express'

const app = express()

outray(app)

const server = app.listen(0, () => {
  const port = server.address().port
  console.log(`Server running on port ${port}`)
})

Callbacks

React to tunnel lifecycle events:
index.js
import express from 'express'
import outray from '@outray/express'

const app = express()

outray(app, {
  onTunnelReady: (url) => {
    console.log(`API accessible at: ${url}`)
  },
  onError: (error) => {
    console.error('Tunnel error:', error.message)
  },
  onReconnecting: () => {
    console.log('Connection lost, reconnecting...')
  },
})

app.listen(3000)

Silent mode

Suppress all console output and handle the URL yourself:
index.js
import express from 'express'
import outray from '@outray/express'

const app = express()

outray(app, {
  silent: true,
  onTunnelReady: (url) => {
    // store or forward url as needed
  },
})

app.listen(3000)

Webhook testing

Expose a specific endpoint for receiving webhooks:
index.js
import express from 'express'
import outray from '@outray/express'

const app = express()

outray(app, {
  subdomain: 'webhook-test',
  onTunnelReady: (url) => {
    console.log(`Webhook endpoint: ${url}/webhooks`)
  },
})

app.use(express.json())

app.post('/webhooks', (req, res) => {
  console.log('Received webhook:', req.body)
  res.json({ received: true })
})

app.listen(3000)

TypeScript

The package exports OutrayPluginOptions for typed configuration:
index.ts
import express, { Application } from 'express'
import outray, { OutrayPluginOptions } from '@outray/express'

const app: Application = express()

const options: OutrayPluginOptions = {
  subdomain: 'my-api',
  apiKey: process.env.OUTRAY_API_KEY,
  onTunnelReady: (url: string) => {
    console.log(`Tunnel ready: ${url}`)
  },
}

outray(app, options)

app.listen(3000)
The full OutrayPluginOptions interface:
interface OutrayPluginOptions {
  subdomain?: string
  customDomain?: string
  apiKey?: string
  serverUrl?: string
  enabled?: boolean
  silent?: boolean
  local?: boolean
  onTunnelReady?: (url: string) => void
  onLocalReady?: (info: {
    hostname: string
    ip: string
    httpUrl?: string
    httpsUrl?: string
  }) => void
  onError?: (error: Error) => void
  onReconnecting?: () => void
  onClose?: () => void
}

Middleware placement

The plugin does not add any middleware to your request stack. You can call outray(app) before or after registering routes and other middleware — it only patches app.listen().
index.js
import express from 'express'
import outray from '@outray/express'
import cors from 'cors'

const app = express()

outray(app) // can go anywhere before listen()

app.use(cors())
app.use(express.json())

app.listen(3000)

Compatibility

  • Express: 4.x and 5.x
  • Servers: TCP ports only — Unix domain sockets are not supported.

Troubleshooting

Tunnel not starting
  • Confirm NODE_ENV is set to development.
  • Check that OUTRAY_ENABLED is not set to "false".
  • Make sure your server is listening on a TCP port, not a Unix socket.
Not seeing the tunnel URL Set NODE_ENV before running your server:
NODE_ENV=development node app.js
Or add a dev script to package.json:
{
  "scripts": {
    "dev": "NODE_ENV=development node app.js"
  }
}
Authentication errors
  • Run outray login to authenticate, or set OUTRAY_API_KEY.
Connection issues
  • The plugin reconnects automatically on connection loss.
  • Confirm the server URL is correct (default: wss://api.outray.dev/).