Skip to content

Commit bb7a1d9

Browse files
committed
docs: examples/ for Gradio, Streamlit, FastAPI, and webhooks
Each example is a copy-paste flow: run app on a fixed local port -> add-route to get a permanent HTTPS URL -> curl to verify. Cross-linked from a new "Examples" section in the README. Covers the four most common asks: - Gradio: replace share=True - Streamlit: local dashboard with stable URL - FastAPI: personal API + path-based routing under one domain - Webhooks: GitHub / Stripe / Slack receivers on a laptop, including a path-based pattern for multiple providers on one domain
1 parent c27b67e commit bb7a1d9

5 files changed

Lines changed: 210 additions & 1 deletion

File tree

README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,20 @@ A hermetic test suite lives in `tests/run-tests.sh` (27 tests, no real cloudflar
341341
bash tests/run-tests.sh
342342
```
343343

344-
Covers input validation, idempotency, catch-all sanity checks, remove-route correctness, and `detect.sh` rejection of tampered configs.
344+
Covers input validation, idempotency, catch-all sanity checks, remove-route correctness, and `detect.sh` rejection of tampered configs. CI runs the same suite on every push and PR — see the **tests** badge at the top.
345+
346+
---
347+
348+
## Examples
349+
350+
End-to-end recipes for common stacks live in [`examples/`](./examples/):
351+
352+
- [Gradio](./examples/gradio.md) — replace `share=True` with a permanent URL
353+
- [Streamlit](./examples/streamlit.md) — local dashboard with stable HTTPS
354+
- [FastAPI](./examples/fastapi.md) — personal API server with optional path-based routing
355+
- [Webhooks](./examples/webhooks.md) — receive GitHub / Stripe / Slack webhooks on your laptop
356+
357+
Each one is a copy-pasteable `run app → add-route → curl` flow.
345358

346359
---
347360

examples/fastapi.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Expose a FastAPI service
2+
3+
Useful for shipping a quick API from your laptop / home server without renting cloud compute or dealing with Vercel/Fly cold starts.
4+
5+
## App
6+
7+
`main.py`:
8+
9+
```python
10+
from fastapi import FastAPI
11+
12+
app = FastAPI()
13+
14+
@app.get("/health")
15+
def health():
16+
return {"ok": True}
17+
18+
@app.get("/")
19+
def root():
20+
return {"message": "Hello from a permanent URL"}
21+
```
22+
23+
## Run + expose
24+
25+
```bash
26+
# 1. Start uvicorn, localhost-only
27+
uvicorn main:app --host 127.0.0.1 --port 3000 &
28+
29+
# 2. Tunnel route
30+
bash scripts/add-route.sh api.example.com http://127.0.0.1:3000 \
31+
--comment "Personal FastAPI service"
32+
33+
# 3. Verify
34+
curl -s https://api.example.com/health
35+
# {"ok":true}
36+
```
37+
38+
## Path-based routing on a single domain
39+
40+
You can host several APIs under one domain by adding paths:
41+
42+
```bash
43+
bash scripts/add-route.sh example.com 3000 --path "/api/v1.*" --comment "v1 API"
44+
bash scripts/add-route.sh example.com 3001 --path "/api/v2.*" --comment "v2 API"
45+
```
46+
47+
`cloudflared` matches first-hit, top-down — `add-route.sh` always inserts above the catch-all `404`, so order is preserved.
48+
49+
## Production hardening
50+
51+
A public URL with no auth is exactly that — public. For anything sensitive, add a [Cloudflare Access](https://developers.cloudflare.com/cloudflare-one/applications/configure-apps/self-hosted-apps/) policy on the hostname (SSO / IP allowlist / one-time PIN), and consider a [WAF rule](https://developers.cloudflare.com/waf/) for rate limiting.

examples/gradio.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Expose a Gradio app
2+
3+
Replace `share=True` (random ngrok-style URL that resets every restart) with a permanent Cloudflare Tunnel URL.
4+
5+
## App
6+
7+
`app.py`:
8+
9+
```python
10+
import gradio as gr
11+
12+
def greet(name: str) -> str:
13+
return f"Hello, {name}!"
14+
15+
# Bind to localhost on a fixed port - the tunnel handles public access.
16+
gr.Interface(fn=greet, inputs="text", outputs="text").launch(
17+
server_name="127.0.0.1",
18+
server_port=7860,
19+
)
20+
```
21+
22+
## Run + expose
23+
24+
```bash
25+
# 1. Start the app (foreground or systemd / pm2 / tmux - your choice)
26+
python app.py &
27+
28+
# 2. Wire up the tunnel route - one time per hostname
29+
bash scripts/add-route.sh demo.example.com 7860 \
30+
--comment "Gradio greeter demo"
31+
32+
# 3. Verify
33+
curl -I https://demo.example.com/
34+
# HTTP/2 200
35+
# ...
36+
```
37+
38+
The URL `https://demo.example.com/` is now permanent — restart the Python process whenever you want; the route stays.
39+
40+
## Tear-down
41+
42+
```bash
43+
bash scripts/remove-route.sh demo.example.com
44+
# then delete the DNS CNAME via the Cloudflare dashboard
45+
```

examples/streamlit.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Expose a Streamlit app
2+
3+
Streamlit's hosted "Streamlit Cloud" works for many cases, but a local tunnel is faster to iterate on and keeps your data local.
4+
5+
## App
6+
7+
`app.py`:
8+
9+
```python
10+
import streamlit as st
11+
12+
st.title("Local dashboard")
13+
name = st.text_input("Name", "World")
14+
st.write(f"Hello, {name}!")
15+
```
16+
17+
## Run + expose
18+
19+
```bash
20+
# 1. Start Streamlit on a fixed port, bound to localhost only
21+
streamlit run app.py \
22+
--server.address 127.0.0.1 \
23+
--server.port 8501 \
24+
--server.headless true &
25+
26+
# 2. Add the route
27+
bash scripts/add-route.sh dash.example.com 8501 \
28+
--comment "Streamlit local dashboard"
29+
30+
# 3. Verify
31+
curl -sSI https://dash.example.com/ | head -1
32+
# HTTP/2 200
33+
```
34+
35+
## Notes
36+
37+
- Streamlit websockets work over the tunnel — you don't need any extra config.
38+
- If you see "Please make sure your network connection is active" in the browser, check that `--server.address 127.0.0.1` is set. Binding to `0.0.0.0` is fine too, but the tunnel only needs `127.0.0.1`.
39+
- Use `--diff` to preview the change before writing:
40+
41+
```bash
42+
bash scripts/add-route.sh dash.example.com 8501 --diff
43+
```

examples/webhooks.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Receive webhooks on your laptop
2+
3+
Most webhook providers (GitHub, Stripe, Slack, Linear, Twilio…) need a public HTTPS URL they can `POST` to. A persistent Cloudflare Tunnel route is the cleanest way to receive them on a local dev machine without spinning up a server.
4+
5+
## Receiver
6+
7+
`webhook_server.py` (FastAPI; same idea works in any framework):
8+
9+
```python
10+
import json
11+
from fastapi import FastAPI, Request
12+
13+
app = FastAPI()
14+
15+
@app.post("/webhook/github")
16+
async def github(req: Request):
17+
payload = await req.json()
18+
print(json.dumps(payload, indent=2))
19+
return {"received": True}
20+
```
21+
22+
## Run + expose
23+
24+
```bash
25+
# 1. Start the receiver locally
26+
uvicorn webhook_server:app --host 127.0.0.1 --port 4000 &
27+
28+
# 2. Add a tunnel route - permanent URL even after laptop restarts
29+
bash scripts/add-route.sh hooks.example.com 4000 \
30+
--comment "Local webhook receiver"
31+
32+
# 3. Point GitHub / Stripe / Slack at:
33+
# https://hooks.example.com/webhook/github
34+
```
35+
36+
## Smoke test
37+
38+
Hit it from your phone or another machine to confirm it's reachable:
39+
40+
```bash
41+
curl -sS -X POST https://hooks.example.com/webhook/github \
42+
-H 'Content-Type: application/json' \
43+
-d '{"event":"ping"}'
44+
```
45+
46+
## Two providers, one domain
47+
48+
Use path-based routing so each provider gets a distinct URL but you only run one tunnel:
49+
50+
```bash
51+
bash scripts/add-route.sh hooks.example.com 4000 --path "/github.*" --comment "GitHub hooks"
52+
bash scripts/add-route.sh hooks.example.com 4001 --path "/stripe.*" --comment "Stripe hooks"
53+
```
54+
55+
## Verify provider signatures
56+
57+
Cloudflare terminates TLS at the edge, so the request body and headers reach your local service unchanged. That means signature verification (HMAC, JWT, etc.) works exactly as documented by the provider — no extra steps.

0 commit comments

Comments
 (0)