sidepage
Local-first hosting and tunneling for code, static sites, and notebooks.
sidepage serve wraps almost anything — a script, a static site, a
Streamlit or FastAPI app, a Python MCP server, a Jupyter notebook —
behind a local reverse proxy and hands you a URL. sidepage proxy does
the same for a service you already have running (npm run dev, a
container, anything already listening on a port) instead of one sidepage
launches itself. sidepage new scaffolds a static site to get started.
Status: serve, proxy, secrets, inspect, and bring-your-own-domain
tunneling are real and tested end to end. Features that need a Sidepage
cloud backend that doesn't exist yet (brokered tunneling, account login,
the directory beyond this machine) print a clear "not implemented"
message instead of failing silently or being left out of the CLI. See
Project status for the full breakdown.
Install
pip install sidepage
sidepage setup # installs cloudflared — needed for --anon/--domain tunneling
sidepage --help
sidepage itself just needs Python 3.12+. It still shells out to
uv to run whatever serve points at — the
wrapped app's own dependencies are always resolved through uv run,
regardless of how sidepage itself got installed — so make sure uv is on
PATH too. sidepage setup only installs cloudflared (the one
non-Python runtime dependency tunneling needs); it's explicit and
opt-in, never triggered silently from inside serve.
From source, for working on sidepage itself rather than just using it
— see docs/DEVELOPMENT.md:
uv sync
uv run sidepage --help # or: source .venv/bin/activate && sidepage --help
Quickstart
# Serve a static site
sidepage serve ./my-site --name demo
# Serve a Streamlit app, gated behind a token
sidepage serve app.py --name demo --auth token
# Serve a FastAPI app — /docs (Swagger UI) works automatically
sidepage serve app.py --name demo
# Serve a Python MCP server over real Streamable HTTP — even if its own
# __main__ only ever calls mcp.run() (stdio), sidepage never runs that
# entrypoint, so it's reachable at /mcp regardless
sidepage serve app.py --name demo
# Serve a Jupyter notebook — a full, editable Lab instance with a live
# kernel, reachable through the proxy like anything else
sidepage serve notebook.ipynb --name demo
# Inject a secret and expose it over a real public tunnel
sidepage secrets set MY_KEY
sidepage serve app.py --env MY_KEY --anon
# Auto-stop after 30 minutes of no traffic, and inject another running
# app's URL as SIDEPAGE_PEER_API_URL
sidepage serve frontend.py --idle-timeout 1800 --peer api=backend
# Proxy a service you already have running instead of one sidepage
# launches — npm run dev, a container, anything on a port
sidepage proxy --port 5173 --name my-vite-app
# Make an app installable to a phone home screen, with a terminal QR
# code to scan and install it — see PWA install and QR codes below
sidepage serve app.py --anon --pwa --qr
Installed from source instead? Prefix every command with uv run (uv run sidepage serve...) or activate the venv first — see
Install; the uv run prefix is only needed for that
source-checkout path.
--type is auto-detected from what you point at (code, static,
notebook; within code, Streamlit/FastAPI/MCP are each recognized by
import) — the four sidepage serve app.py lines above look identical
because the actual dispatch happens by inspecting app.py's content, not
its filename. Want to try these against something real without writing
your own app first? Clone this repo and swap in tests/fixtures/static- site, tests/fixtures/streamlit-app/app.py,
tests/fixtures/fastapi-app/app.py, tests/fixtures/mcp-app/app.py, or
tests/fixtures/notebook-app/notebook.ipynb.
Every serve/proxy call blocks the terminal until Ctrl+C (or sidepage stop <app-name> from another terminal) — no background/daemon mode.
serve tears down the process it launched too; proxy never launched
anything, so Ctrl+C/stop only tears down the proxy and tunnel — the
service you pointed it at keeps running (see Proxying an already-running
service).
How it works
Two things sit between "just run a script" and what serve does:
- A local reverse proxy runs on your machine in front of the app's
real port. It enforces
--auth, counts usage, shows a holding page while the app boots, and proxies HTTP + WebSockets — the wrapped app itself needs zero Sidepage-specific code. - A tunnel, chosen per call:
--anonfor a free, no-account*.trycloudflare.comURL, or--domain <domain>for your own Cloudflare domain (see Bring your own domain below). Without either flag,servejust listens on127.0.0.1.
Commands
| Command | What it does |
|---|---|
sidepage serve <target> |
Wrap and host a static dir, script, or app — see flags below. |
sidepage proxy --port <n> |
Wrap an already-running local service instead of one serve launches — see below. |
sidepage stop <app-name> |
Tear down a running app (serve or proxy). |
sidepage ls / sidepage status <app-name> |
List / check apps running on this machine. |
sidepage usage <app-name> |
Request and connection counts for an app. |
sidepage inspect [<app-name>] |
Interactive HTTP console against a running app. |
sidepage secrets set|list|remove |
Encrypted local vault for standing credentials. |
sidepage account domain set |
Provision a BYO Cloudflare domain — see below. |
sidepage new <name> |
Scaffold a static site. |
sidepage app register "<invocation>" <name> |
Save a serve invocation under a short name. |
sidepage app list / show <name> / unregister <name> |
Manage saved apps — see below. |
sidepage promote <app-name> |
Widen an app's discovery scope. Not yet meaningful — only local scope exists today. |
sidepage login / sidepage account status |
Not implemented — no Sidepage account backend to talk to yet. |
serve's main flags:
sidepage serve <target> [--type auto|code|static|notebook] [--name <app-name>]
[--auth open|token] [--anon | --domain <domain>]
[--token <value>] [--env <SECRET_NAME>]...
[--timeout <seconds>] [--idle-timeout <seconds>]
[--peer <role>=<app-name>]...
[--pwa [--pwa-*]...] [--qr]
--typeis usually inferred:codetargets are auto-detected as Streamlit, FastAPI, or a Python MCP server (officialmcpSDK or the third-partyfastmcppackage) and launched with their real launcher (streamlit run,uvicorn <module>:<app>, oruvicorn --factory <module>:<mcp-var>.<app-method>); anything else falls back to a generic$PORT-reading launch.notebook(.ipynb) targets get a full, editable Jupyter Lab instance with a live kernel. MCP servers are launched by bypassing their own entrypoint entirely (same trick as FastAPI) — a script whose__main__only callsmcp.run()(stdio, the default) still ends up served over real Streamable HTTP at/mcp, since that entrypoint is never executed.--auth open|token—tokengates the app behind a header, query param, or browser cookie set by a gate page. (network/oauthparse but aren't built.)--env <SECRET_NAME>— repeatable; injects a named vault secret into the wrapped process's environment. Fails loud if the name isn't stored.--anon/--domainare mutually exclusive — see How it works.--timeout <seconds>/--idle-timeout <seconds>— auto-teardown; see Timeouts, lazy start, and peers below.--peer <role>=<app-name>— repeatable; wire one served app to another's URL. Same section below.--pwa/--qr— make the app installable to a phone home screen, and/or print a terminal QR code for the URL. See PWA install and QR codes below.
Run sidepage <command> --help for the full flag list, including ones
that parse but aren't implemented yet (they report that clearly rather
than silently doing nothing).
Proxying an already-running service
sidepage proxy --port <n> wraps a service you already have running —
npm run dev, a container, anything already listening on a port — with
the same reverse proxy, auth, and tunnel stack serve uses, but never
launches or owns the process itself (Ctrl+C/stop only tears down the
proxy and tunnel, not your service).
sidepage proxy --port 5173 --anon # already running: npm run dev on 5173
Read the full guide before pointing this at anything public —
docs/guides/proxy.md covers the safety notes
(X-Forwarded-*, localhost-trust, OAuth/--anon), a one-line header fix
per framework, and a known Vite HMR limitation.
Timeouts, lazy start, and peers
--timeout <seconds> / --idle-timeout <seconds> auto-stop a served app
(total lifetime, or no-traffic window); code/notebook targets also
lazy-start their subprocess on the first inbound request instead of at
serve time; --peer <role>=<app-name> wires one served app to
another's URL via SIDEPAGE_PEER_<ROLE>_URL.
sidepage serve backend.py --name backend
sidepage serve frontend.py --idle-timeout 1800 --peer api=backend
Full detail (live peer re-resolution via GET /.sidepage/peers.json,
exactly when lazy start fires, why --peer is code/notebook-only) in
docs/guides/timeouts-and-peers.md.
PWA install and QR codes
--pwa makes any served app installable to a phone home screen —
manifest, service worker, and HTML injection all synthesized by the
reverse proxy, the wrapped app never touched on disk. --qr prints a
terminal QR code for the resulting URL, independent of --pwa.
sidepage serve app.py --anon --pwa --qr
Every --pwa-* flag, the ephemeral-vs-durable install distinction, and
icon validation are in docs/guides/pwa.md.
Saved apps (the local registry)
Save a serve invocation under a short name and re-run it without
retyping flags — any flag passed at serve time overrides the
registered one for that run only, the saved registration is never
changed.
sidepage app register "abc.py --auth token" abc-app
sidepage serve abc-app
Override/merge semantics, the --with preview, and why a literal
--token is refused at registration time are in
docs/guides/registry.md.
Bring your own domain
Route apps through your own Cloudflare domain instead of
*.trycloudflare.com:
sidepage secrets set cf-api-token
sidepage account domain set example.com --api-token-name cf-api-token
sidepage serve app.py --domain example.com
One-time setup needs a scoped Cloudflare API token; every app served
under the same domain then shares that one Cloudflare Tunnel — no new
resources or tokens per app. Token scopes and the shared-tunnel model are
in docs/guides/byo-domain.md.
For agents and harnesses
skills/sidepage-serve/ is a packaged Claude
Skill that teaches an
agent to drive sidepage serve/sidepage proxy safely — most importantly,
how to background them and get structured JSON back instead of hanging,
since neither command has a daemon mode and both block until Ctrl+C or
sidepage stop. Copy or symlink that directory into wherever your harness
looks for skills (e.g. ~/.claude/skills/).
Development
uv sync # install runtime + dev deps
uv run ruff check . # lint
uv run pytest # full suite (~4 min; mostly first-run dependency resolves)
Project layout, the full dependency list, and what's needed on the
machine running tests (cloudflared, Node.js/npm for the Vite fixture)
are in docs/DEVELOPMENT.md.
Project status
Real and tested end to end: serve/proxy for static, code, and
notebook targets, open/token auth, --env secret injection, --anon
tunneling, BYO-domain tunneling, secrets, stop/ls/status/usage,
inspect, the local app registry, --timeout/--idle-timeout/--peer,
and --pwa/--qr.
Not implemented, and reports that clearly rather than silently
no-op'ing: brokered (default) tunneling, login/account status, the
discovery directory beyond this machine, --guardrail, --auth network/oauth, MCP tool browsing in inspect, and an OS-keychain
backend for the secrets vault (encrypted-file only for now).
One known limitation, investigated not fixed: HMR/live-reload for a
Vite target proxied through --anon — see
docs/guides/proxy.md.
See docs/CHECKLIST.md for the full per-feature
breakdown.
See also
docs/guides/proxy.md,docs/guides/timeouts-and-peers.md,docs/guides/pwa.md,docs/guides/registry.md,docs/guides/byo-domain.md— per-feature deep dives.docs/DEVELOPMENT.md— project layout, contributor setup.docs/CHECKLIST.md— full per-feature build status.docs/OPEN_QUESTIONS.md— design rationale behind what's resolved and what's still open.
Metadata
Release files for sidepage 0.2.3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| sidepage-0.2.3.tar.gz | 106.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| sidepage-0.2.3-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 236.6 kB
Release files / sidepage-0.2.3.tar.gz
| Download URL | sidepage-0.2.3.tar.gz |
|---|---|
| Size | 106.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
bba72135375cab2961c78ad22a34d94bf241f8dbed1becc31f76651db821e197
|
|
BLAKE2b-256 checksum How to use checksums |
3a0e7129aa3ed0662dd6a87973f5560cf425f658d4f58f6c4146da456a0cbcc3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.21 {"installer":{"name":"uv","version":"0.9.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / sidepage-0.2.3-py3-none-any.whl
| Download URL | sidepage-0.2.3-py3-none-any.whl |
|---|---|
| Size | 130.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
47f0a0eee6eb310306315fd9e28e9e95fa31e95780fb22aeff350f0647544670
|
|
BLAKE2b-256 checksum How to use checksums |
fa820a49c4afeca117bcb1851673147e2f555ac46e01259d4968f1a23af4ea27
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.21 {"installer":{"name":"uv","version":"0.9.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|