Skip to main content

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 new scaffolds a static site to get started.

Status: serve, 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

Requires uv (it fetches Python 3.12+ for you if needed).

uv sync
uv run sidepage --help

Or activate the venv and call it directly: source .venv/bin/activate && sidepage --help.

Quickstart

# Serve a static site
uv run sidepage serve tests/fixtures/static-site --name demo

# Serve a Streamlit app, gated behind a token
uv run sidepage serve tests/fixtures/streamlit-app/app.py --name demo --auth token

# Serve a FastAPI app — /docs (Swagger UI) works automatically
uv run sidepage serve tests/fixtures/fastapi-app/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
uv run sidepage serve tests/fixtures/mcp-app/app.py --name demo

# Serve a Jupyter notebook — a full, editable Lab instance with a live
# kernel, reachable through the proxy like anything else
uv run sidepage serve tests/fixtures/notebook-app/notebook.ipynb --name demo

# Inject a secret and expose it over a real public tunnel
uv run sidepage secrets set MY_KEY
uv run sidepage serve some_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
uv run sidepage serve frontend.py --idle-timeout 1800 --peer api=backend

Every serve call blocks the terminal until Ctrl+C (or sidepage stop <app-name> from another terminal), tearing everything down immediately — no background/daemon mode.

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: --anon for a free, no-account *.trycloudflare.com URL, or --domain <domain> for your own Cloudflare domain (see Bring your own domain below). Without either flag, serve just listens on 127.0.0.1.

Commands

Command What it does
sidepage serve <target> Wrap and host a static dir, script, or app — see flags below.
sidepage stop <app-name> Tear down a running app.
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>]...
  • --type is usually inferred: code targets are auto-detected as Streamlit, FastAPI, or a Python MCP server (official mcp SDK or the third-party fastmcp package) and launched with their real launcher (streamlit run, uvicorn <module>:<app>, or uvicorn --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 calls mcp.run() (stdio, the default) still ends up served over real Streamable HTTP at /mcp, since that entrypoint is never executed.
  • --auth open|tokentoken gates the app behind a header, query param, or browser cookie set by a gate page. (network/oauth parse 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 / --domain are 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.

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).

Timeouts, lazy start, and peers

Auto-teardown. --timeout <seconds> stops the app once its total lifetime (from serve start) reaches the limit; --idle-timeout <seconds> stops it once that many seconds pass with no proxied HTTP request or WebSocket message — the timer resets on every one. Both are composable with each other and checked in the same blocking loop Ctrl+C already interrupts, so an auto-stop tears down exactly like sidepage stop would: immediately, no drain window.

sidepage serve demo.py --idle-timeout 1800   # stop after 30 idle minutes
sidepage serve demo.py --timeout 3600        # stop after 1 hour no matter what

Lazy start. For code/notebook targets, the wrapped process isn't launched at serve time — it launches on the first inbound request, behind the same "starting…" holding page a slow boot already shows. A serve call that nobody ever hits never spends the CPU/memory to boot the wrapped app at all. (static targets are already in-process and instant, so there's nothing to defer there.) This is automatic — no flag.

Peers. --peer <role>=<app-name> (repeatable) resolves another currently running served app's URL and injects it as SIDEPAGE_PEER_<ROLE>_URL in the wrapped process's environment — useful for a frontend that needs to reach a backend whose tunnel URL doesn't exist until it's actually served, and changes across --anon runs. Resolution fails loud (nonzero exit, clear message) if the named peer isn't running yet. The app can also re-resolve peers live, at any point, via GET /.sidepage/peers.json — gated by the app's own --auth tier like any other route — so a peer that restarts mid-session with a fresh URL is never stale the way the boot-time env var would be. code/ notebook targets only; there's no subprocess to inject into for a static target, so --peer on one is rejected up front.

sidepage serve backend.py --name backend
sidepage serve frontend.py --peer api=backend   # $SIDEPAGE_PEER_API_URL in frontend's env

Saved apps (the local registry)

Save a serve invocation under a short name and re-run it without retyping flags:

sidepage app register "abc.py --auth token" abc-app
sidepage serve abc-app

Any flag passed at serve time overrides the registered one for that one run only — the saved registration itself is never changed:

sidepage serve abc-app --scope web   # runs with --auth token (registered)
                                      # but --scope web for just this run

sidepage app show abc-app prints the saved config; add --with "<flags>" to preview the effective merged config before actually running it, e.g. sidepage app show abc-app --with "--scope web".

A registered app's target is resolved once, at registration time — so --type is stored as a concrete value (code, static, notebook), never "auto." sidepage app register refuses a literal --token <value>: auth tokens are per-process and regenerate on every serve call, so storing one would defeat the point of them being ephemeral. --env <SECRET_NAME> is fine to save — it's a reference to a vault entry, never the secret value itself.

sidepage app list
sidepage app unregister abc-app

Bring your own domain

Route apps through your own Cloudflare domain instead of *.trycloudflare.com. One-time setup:

  1. Create a Cloudflare API token (dashboard → My Profile → API Tokens) scoped to:
    • Account → Cloudflare Tunnel → Edit
    • Zone → DNS → Edit
    • Zone → Zone → Read
  2. Store it in the vault, then provision the domain:
    sidepage secrets set cf-api-token
    sidepage account domain set example.com --api-token-name cf-api-token
    
    This creates one Cloudflare Tunnel for the whole domain and stores its run-token in the vault automatically — the CLI prints the vault name it landed under (cf-tunnel-token::example.com), since it was never typed by you.
  3. Serve apps through it:
    sidepage serve app.py --domain example.com
    

Every app served under the same domain shares that one tunnel — no new Cloudflare resources or tokens per app. The shared cloudflared process starts with the first app on a domain and stops with the last.

Project layout

src/sidepage/
├── cli.py           Root Typer app
├── commands/        Argument parsing & help text — one module per command group
├── core/            The SDK: serve/tunnel/proxy orchestration, secrets vault, running-app registry, saved-app registry
└── config/          Local config paths (XDG-style, overridable via SIDEPAGE_HOME)

tests/
├── fixtures/        Real apps used as test targets (static site, Streamlit, FastAPI, MCP, notebook)
└── test_*.py        Unit and integration tests

docs/
├── CHECKLIST.md       Build status for every command and core module
├── OPEN_QUESTIONS.md  Design decisions — resolved and still-open
└── SPEC_V5_DRAFT.md   v5 proposals — timeout/lazy-start/--peer (built, this doc) plus still-parked ideas

Development

uv sync                 # install runtime + dev deps
uv run ruff check .     # lint
uv run pytest           # full suite (~4 min; mostly first-run dependency resolves)

Runtime dependencies are real, not stubs: Starlette, uvicorn, httpx, and websockets back the reverse proxy; cryptography backs the secrets vault. cloudflared and network access (for uv run to resolve wrapped apps' dependencies) are expected to be available wherever tests run.

Project status

Real and tested end to end: serve for static, code, and notebook targets (Streamlit/FastAPI/Python-MCP auto-detected, generic $PORT fallback, full Jupyter Lab for .ipynb), open/token auth, --env secret injection, --anon tunneling, BYO-domain tunneling (account domain set + serve --domain), secrets, stop/ls/status/usage, inspect for HTTP/static targets, the local app registry (app register|list|show|unregister + serve <app-name>, with real one-off override merging), --timeout/--idle-timeout auto-teardown, lazy start for code/notebook targets (subprocess deferred to the first request), and --peer <role>=<app-name> (boot-time env injection plus a live GET /.sidepage/peers.json).

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 the OS-keychain backend for the secrets vault (encrypted-file only for now).

See docs/CHECKLIST.md for the full per-feature breakdown, and docs/OPEN_QUESTIONS.md for design rationale behind what's resolved and what's still open.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

sidepage-0.1.0.tar.gz (74.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

sidepage-0.1.0-py3-none-any.whl (95.4 kB view details)

Uploaded Python 3

File details

Details for the file sidepage-0.1.0.tar.gz.

File metadata

  • Download URL: sidepage-0.1.0.tar.gz
  • Upload date:
  • Size: 74.1 kB
  • Tags: Source
  • Uploaded using 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}

File hashes

Hashes for sidepage-0.1.0.tar.gz
Algorithm Hash digest
SHA256 0a20e907e1e4cf92e8ac54f27ca38b79b6192e746267016383fab98b095da49b
MD5 79774edc0073baaeb86fbb626e9913d0
BLAKE2b-256 9a726c6611907659367c282228945cff419ac77af772f934eb342c6a68ce6696

See more details on using hashes here.

File details

Details for the file sidepage-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: sidepage-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 95.4 kB
  • Tags: Python 3
  • Uploaded using 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}

File hashes

Hashes for sidepage-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 6afa99c3f880da7e320681f1f1aa106abe577e4d471bcbc427f6944732725021
MD5 a7e6db1b22f01590aca4223fcfeaf2bb
BLAKE2b-256 cf808c8a1a0c89b73224e6f3a1218e32d97ff131594c087ce7b94ebc16c05941

See more details on using hashes here.

Release history Release notifications | RSS feed

0.2.0

2 files

This release

0.1.0 This release

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page