Edge Tunnel
Expose a local port to the internet through the Oblien edge. Traffic to either a managed subdomain or an exact verified custom hostname is forwarded through a multiplexed WebSocket tunnel to an agent running on your machine.
How it works
- Check the hostname if you want to use an exact custom domain
- Create a tunnel specifying a name, port, and optional slug/domain
- Issue a token — a short-lived JWT for the tunnel connection
- Connect an agent (SDK or CLI) using the token — traffic flows through the WebSocket tunnel to
localhost:port
The edge handles TLS termination. The agent authenticates with a JWT and maintains a persistent WebSocket connection to the broker.
For exact custom hostnames, certificate issuance is shared with the same custom-domain SSL flow used by pages and workspaces. Existing valid certificates are reused, and the scheduler auto-renews them before expiry.
Tunnels are independent of workspaces. They route traffic from the edge directly to a local port on whatever machine runs the agent.
Custom domain flow
There are two tunnel modes:
- Managed subdomain: pass a
slugand optional basedomain, for examplemy-app.preview.oblien.com - Exact custom hostname: pass
domain: "api.example.com"and leaveslugempty
Exact custom hostnames are gated by TXT ownership verification and global route collision checks. The create call re-checks ownership server-side, so the preflight check is advisory and cannot be used to bypass verification.
Check a custom tunnel domain
Use this before create when you want an exact custom hostname.
POST /edge/tunnels/check-domain
Content-Type: application/json
{
"domain": "api.example.com",
"port": 3000
}const check = await client.edgeTunnel.checkDomain({
domain: 'api.example.com',
port: 3000,
});
if (check.mode === 'custom_domain' && !check.ownership) {
console.log(check.required_records.txt);
}curl -X POST "https://api.oblien.com/edge/tunnels/check-domain" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"domain":"api.example.com","port":3000}'Check response
{
"success": true,
"mode": "custom_domain",
"domain": "api.example.com",
"available": true,
"needs_verification": true,
"verified": false,
"cname": false,
"ownership": false,
"reusable": false,
"existing_tunnel": null,
"required_records": {
"cname": { "host": "api.example.com", "target": "edge.oblien.com" },
"txt": { "host": "_oblien.api.example.com", "value": "verify=tunnel_abc123" }
},
"errors": [
"No matching TXT record found — add TXT \"verify=tunnel_abc123\" at _oblien.api.example.com"
]
}SDK Usage
One-liner: create + connect
import Oblien from 'oblien';
const client = new Oblien({
clientId: process.env.OBLIEN_CLIENT_ID,
clientSecret: process.env.OBLIEN_CLIENT_SECRET,
});
const tunnel = await client.edgeTunnel.connect({
name: 'dev-server',
port: 3000,
});
console.log(tunnel.url);
// → https://dev-server-x7k.preview.oblien.com
tunnel.on('request', (streamId, port) => {
console.log(`→ localhost:${port}`);
});
// Later…
tunnel.close();Two-sided: SaaS backend generates token, client connects
// ── Server side (your SaaS backend) ──
import Oblien from 'oblien';
const admin = new Oblien({ clientId, clientSecret });
// Create a tunnel for each user/project
const { tunnel } = await admin.edgeTunnel.create({
name: 'user-preview',
port: 3000,
slug: 'user-123-preview',
});
// Issue a token to hand to the client
const { token, connect_url } = await admin.edgeTunnel.issueToken(tunnel.id);
// Send token + connect_url to user's machine via your own API// ── Client side (user's machine) ──
import { TunnelClient } from 'oblien';
const tc = new TunnelClient({
connectUrl: connect_url, // from your backend
token: token, // from your backend
localPort: 3000,
});
tc.on('open', () => console.log('connected'));
tc.on('request', (id, port) => console.log(`→ localhost:${port}`));
tc.connect();Exact custom hostname
const check = await client.edgeTunnel.checkDomain({
domain: 'api.example.com',
});
if (!check.ownership) {
console.log('Add this TXT record first:', check.required_records.txt);
}
const { tunnel } = await client.edgeTunnel.create({
name: 'production-api',
port: 3000,
domain: 'api.example.com',
});
console.log(tunnel.url);
// → https://api.example.comList tunnels
GET /edge/tunnelscurl "https://api.oblien.com/edge/tunnels" \
-H "Authorization: Bearer $TOKEN"Response
{
"success": true,
"tunnels": [
{
"id": 1,
"name": "dev-server",
"slug": "dev-server-x7k",
"domain": "preview.oblien.com",
"tunnel_id": "a1b2c3d4e5f6...",
"port": 3000,
"url": "https://dev-server-x7k.preview.oblien.com",
"status": "active",
"created_at": "2026-04-13T10:00:00Z"
}
]
}Create a tunnel
POST /edge/tunnels
Content-Type: application/json
{
"name": "dev-server",
"port": 3000,
"slug": "my-app"
}curl -X POST "https://api.oblien.com/edge/tunnels" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"dev-server","port":3000}'Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Display name (max 100 chars) |
port | number | Yes | Target port on the agent machine (1–65535) |
slug | string | No | Subdomain slug. Leave empty when using an exact custom hostname. |
domain | string | No | Base domain for slug-based tunnels, or an exact custom hostname when slug is omitted. |
If you pass an exact custom hostname like api.example.com, the API requires TXT ownership verification and rejects the create call until verification succeeds.
If the same user already owns the hostname on the same port, the API can reuse the existing tunnel instead of creating a duplicate.
Response
{
"success": true,
"tunnel": {
"id": 1,
"name": "dev-server",
"slug": "dev-server-x7k",
"domain": "preview.oblien.com",
"hostname": "dev-server-x7k.preview.oblien.com",
"is_custom": false,
"tunnel_id": "a1b2c3d4e5f6...",
"port": 3000,
"url": "https://dev-server-x7k.preview.oblien.com",
"status": "active"
}
}For exact custom hostnames, tunnel objects also include a live ssl object:
{
"ssl": {
"status": "active",
"expiresAt": "2026-07-22",
"error": null
}
}Update a tunnel
Update name, slug, or port.
Exact custom-domain tunnels cannot change slug after creation.
PUT /edge/tunnels/:id
Content-Type: application/json
{ "name": "new-name", "port": 8080 }| Parameter | Type | Description |
|---|---|---|
name | string | New display name |
slug | string | New subdomain slug |
port | number | New target port |
Issue a connection token
Generate a short-lived JWT for agent authentication. The token is valid for 24 hours.
POST /edge/tunnels/:id/tokencurl -X POST "https://api.oblien.com/edge/tunnels/1/token" \
-H "Authorization: Bearer $TOKEN"Response
{
"success": true,
"token": "eyJhbGciOiJIUzI1NiIs...",
"tunnel_id": "a1b2c3d4e5f6...",
"port": 3000,
"expires_in": "24h",
"connect_url": "wss://edge.oblien.com/connect"
}Enable / Disable
Toggle a tunnel without deleting it. Disabling removes the edge route (traffic stops), enabling re-creates it.
POST /edge/tunnels/:id/enable
POST /edge/tunnels/:id/disableRenew tunnel SSL
Force certificate provisioning or renewal for a connected custom-domain tunnel.
POST /edge/tunnels/:id/ssl/renewconst result = await client.edgeTunnel.renewSSL(42);
console.log(result.ssl.status);
console.log(result.ssl.expiresAt);curl -X POST "https://api.oblien.com/edge/tunnels/42/ssl/renew" \
-H "Authorization: Bearer $TOKEN"Renew response
{
"success": true,
"domain": "api.example.com",
"ssl": {
"status": "active",
"expiresAt": "2026-07-22",
"error": null
},
"tunnel": {
"id": 42,
"url": "https://api.example.com"
}
}Delete a tunnel
Removes the edge route and DB record permanently.
DELETE /edge/tunnels/:id