Serve a FastAPI App through rstream Tunnels

Serve a FastAPI application directly through a published rstream tunnel with the Python SDK, protect it with edge token auth, and consume the private variant over an rstream dial, all without a local HTTP server or an open port.


This guide serves a FastAPI application directly through the rstream Python SDK. The process opens an outbound connection to the engine and handles accepted HTTP streams in-process, without a Uvicorn listener or a local reverse proxy. The FastAPI application itself remains unchanged.

The walkthrough mirrors the C++ Boost.Beast guide for the Python ecosystem, in idiomatic async Python. It publishes the app, adds an edge token policy without touching application code, and finishes with the private variant, where the same app is reachable only through an rstream dial.

rstream examples / python-fastapi-rstream-tunnelOpen the FastAPI tunnel sample with the published, token-protected, and private modes.

What you will build

One Python process owns the whole flow. A FastAPI application defines the endpoints, the SDK creates a tunnel under the selected rstream project, and rstream.asgi.serve parses each accepted stream and dispatches it to the app in-process. There is no loopback hop and no listening socket on the machine; the only network activity is the outbound runtime session to the engine.

Prerequisites

Both the development machine and any machine that will dial privately need an rstream CLI configuration resolving to the same project, as covered in Installation and CLI Workflow.

rstream login
rstream project use <project-endpoint>

The sample needs Python 3.10 or newer and two packages.

pip install "rstreamlabs-rstream[asgi,api]" fastapi

The SDK package installs as rstreamlabs-rstream and is imported as rstream. The asgi extra brings the in-process HTTP bridge, and the api extra lets the client resolve the managed project endpoint, exactly like the CLI does. The broader SDK surface is documented in Python SDK.

Start from a FastAPI app

The application part of the sample is deliberately ordinary, three routes with type hints and nothing rstream-specific.

from fastapi import FastAPI
 
app = FastAPI(title="python-fastapi-rstream-tunnel")
 
 
@app.get("/")
async def root() -> dict[str, str]:
    return {"service": "python-fastapi-rstream-tunnel", "status": "ok"}
 
 
@app.get("/items/{item_id}")
async def read_item(item_id: int) -> dict[str, int | str]:
    return {"item_id": item_id, "source": "rstream tunnel"}

Anything that runs under Uvicorn runs here unchanged, including dependency injection, middleware, and routers. The deployment model changes; the framework does not.

Create the published tunnel

The runtime side reads the CLI-compatible configuration, opens a control channel, creates a published HTTP tunnel, and hands the app to the ASGI helper.

async with (
    rstream.Client.from_env() as client,
    await client.connect() as control,
):
    tunnel = await control.create_tunnel(
        name="python-fastapi-demo",
        protocol="http",
        http_version="http/1.1",
        publish=True,
        labels={"app": "python-fastapi-demo"},
    )
    print("Forwarding address:", tunnel.forwarding_address)
    await rstream.asgi.serve(app, tunnel)

Client.from_env resolves the same selected project as the CLI, so there are no credentials in the code. create_tunnel registers the endpoint under the project, and the printed forwarding address is the public URL. rstream.asgi.serve then iterates over accepted streams, parses HTTP/1.1, and dispatches each request to the app on the running event loop, one task per stream.

Run the sample

python main.py

The process prints the forwarding address once the tunnel is created. Requests from any HTTP client land directly in the FastAPI app.

curl https://<forwarding-address>/items/42
{"item_id":42,"source":"rstream tunnel"}

Stopping the process removes the tunnel; restarting it recreates the endpoint under the same name.

Add edge policy

Access policy belongs to the edge, not to application code. Running the sample with --token-auth passes auth=rstream.TunnelAuth(token=True) to create_tunnel, and the edge starts requiring a valid rstream token before any request reaches the app.

python main.py --token-auth
curl -s -o /dev/null -w '%{http_code}\n' https://<forwarding-address>/
401

The application did not change, and there is no auth middleware to maintain. Token formats, scopes, and the broader access model are covered in HTTP tunnel authentication and Fine-grained tokens.

Dial the private variant from Python

When the consumers are rstream-aware, the endpoint does not need to be published at all. With --private, the sample creates an unpublished tunnel, and the engine rejects public exposure options for it, so the protocol and HTTP version settings are simply omitted. The app is then reachable only by dialing the tunnel name.

async with (
    rstream.Client.from_env() as client,
    await client.dial("python-fastapi-demo") as stream,
):
    stream.write(b"GET /items/42 HTTP/1.1\r\nHost: python-fastapi-demo\r\nConnection: close\r\n\r\n")
    await stream.drain()
    print(await stream.read(4096))

The dialed stream is a plain bidirectional byte stream, which is why a minimal HTTP/1.1 exchange is enough. The sample's dial_client.py wraps exactly this. The distinction that matters is the client protocol, not the security model: published endpoints serve standard HTTP clients and can still be protected at the edge, while private tunnels serve rstream dials, as described in Private Tunnels.

Where to go next

The same SDK surface scales from one app to a distributed workload. Run Edge Vision Inference on Your Own Machines with rstream uses tunnel labels, live inventory, and real-time watches to coordinate Python processes across machines. The SDK entrypoint with the full capability table is Python SDK, and the equivalent native-server patterns exist for C++ and Next.js.