Skip to content

Repository files navigation

python-peerjs-server

An asyncio-based Python port of the PeerJS signalling server, the WebSocket broker that PeerJS clients use to discover each other and exchange WebRTC offers/answers/candidates.

It's feature-complete and wire-compatible with the original: WebSocket session handling, reconnect, and the HTTP API are all implemented, plus a few deployment conveniences, like rate limiting, proxied-IP support, and bounded queues.

Features

  • WebSocket signalling at {path}/peerjs with id/token/key query-param auth
  • Reconnect support (same id + token re-attaches the socket and drains queued messages)
  • Per-peer message queueing for offline destinations, with periodic EXPIRE sweeps
  • Periodic dead-connection reaping based on heartbeat pings
  • HTTP API: GET {path}{key}/id (fresh peer id) and GET {path}{key}/peers (peer list, opt-in via allow_discovery)
  • CORS header support
  • Fixed-window per-IP rate limiting on the HTTP API
  • Optional X-Forwarded-For trust for real client IPs when running behind a reverse proxy

Requirements

  • Python 3.13+
  • websockets>=14 (the only runtime dependency, installed automatically below)

Installation

pip install python-peerjs-server

For local development (editable install with test/lint tooling):

pip install -e ".[dev]"

Usage

Run as a console script:

peerjs-server --port 9000 --key peerjs

or as a module:

python -m python_peerjs_server --port 9000 --key peerjs

CLI options

FlagEnv varDefaultDescription
--hostPEERJS_HOST::Bind host
--portPEERJS_PORT9000Bind port
--keyPEERJS_KEYpeerjsAccess key clients must supply
--pathPEERJS_PATH/Path prefix for HTTP/WS routes
--allow-discoveryPEERJS_ALLOW_DISCOVERYoffExpose GET /peers
--cors-originPEERJS_CORS_ORIGINunsetCORS origin for HTTP API responses
--log-levelPEERJS_LOG_LEVELINFOLogging level

Other server behaviour (timeouts, rate limits, queue caps, proxy trust) is configured via python_peerjs_server.config.Config when embedding the server programmatically; see src/python_peerjs_server/config.py for the full set of fields and their defaults.

Embedding in your own asyncio app

importasynciofrompython_peerjs_server.configimportConfigfrompython_peerjs_server.mainimportpeerjs_serveasyncio.run(peerjs_serve(Config(port=9000, key="peerjs")))

Logging

If you're running via the CLI, --log-level/PEERJS_LOG_LEVEL already configures this for you (it calls logging.basicConfig); the rest of this section is for embedding python_peerjs_server in your own app.

Every module logs under the python_peerjs_server namespace (logging.getLogger(__name__)), so that one logger name is the knob to turn:

importlogging# See python_peerjs_server logs at INFO+ on stderrlogging.getLogger("python_peerjs_server").setLevel(logging.INFO)
logging.getLogger("python_peerjs_server").addHandler(logging.StreamHandler())
# Or, if your app already configured the root logger, just set the level;# propagation does the restlogging.getLogger("python_peerjs_server").setLevel(logging.DEBUG)
# Silence a noisy sub-logger specificallylogging.getLogger("python_peerjs_server.services.web_socket_session").setLevel(logging.WARNING)

A NullHandler is attached by default, so the library stays silent unless logging is configured (either on python_peerjs_server or higher up).

Connecting a PeerJS client

This server only implements the signalling protocol; clients connect using the PeerJS client library (docs, GitHub):

constpeer=newPeer("some-id",{host: "localhost",port: 9000,path: "/",key: "peerjs",});

Runnable versions of the standalone and FastAPI embedding patterns live in examples/: standalone.py and fastapi_app.py.

CORS

The PeerJS client calls the HTTP API (GET {path}{key}/id) before it opens the WebSocket. If your page is served from a different origin than the broker (e.g. a Vite/CRA dev server on localhost:5173 talking to a broker on localhost:9000), the browser will block that request with a CORS error unless you set --cors-origin / PEERJS_CORS_ORIGIN / Config.cors_origin to the exact origin (scheme + host + port) serving the page:

peerjs-server --port 9000 --key peerjs --cors-origin http://localhost:5173

It's unset by default (no Access-Control-Allow-Origin header sent), which is fine for same-origin deployments.

Running alongside another web framework

peerjs_serve() is just a coroutine; you can run it inside whatever process already hosts your app instead of standing up a separate service. Pass handle_signals=False so it doesn't fight your app for SIGINT/SIGTERM handling, and pick a port distinct from your main app's.

Runnable versions of both patterns below live in examples/: tornado_app.py and flask_app.py.

Tornado

Tornado runs on the standard asyncio event loop, so the broker can simply be scheduled as another task on it:

importasyncioimporttornado.ioloopimporttornado.webfrompython_peerjs_server.configimportConfigfrompython_peerjs_server.mainimportpeerjs_serveclassMainHandler(tornado.web.RequestHandler):
defget(self) ->None:
self.write("hello from tornado")
asyncdefmain() ->None:
app=tornado.web.Application([(r"/", MainHandler)])
app.listen(8888)
asyncio.create_task(peerjs_serve(Config(port=9000, key="peerjs"), handle_signals=False))
awaitasyncio.Event().wait() # run foreverif__name__=="__main__":
asyncio.run(main())

Flask

Flask's dev server is sync/WSGI and blocks its own thread, so the broker has to run on an event loop in a separate thread:

importasyncioimportthreadingfromflaskimportFlaskfrompython_peerjs_server.configimportConfigfrompython_peerjs_server.mainimportpeerjs_serveapp=Flask(__name__)
@app.get("/")defindex() ->str:
return"hello from flask"defrun_python_peerjs_server() ->None:
asyncio.run(peerjs_serve(Config(port=9000, key="peerjs"), handle_signals=False))
if__name__=="__main__":
threading.Thread(target=run_python_peerjs_server, daemon=True).start()
app.run(port=8888)

For an ASGI app (FastAPI, Starlette) you can instead start it from a lifespan/startup hook with asyncio.create_task, the same way as the Tornado example: both share a loop with no extra thread needed.

Development

pip install -e ".[dev]"# install with dev/test/lint dependencies
pytest # run tests
ruff check src # lint
ruff format src # format
mypy src # type check
bandit -r src # security scan

License

MIT, see LICENSE.

About

Wire-compatible asyncio port of the PeerJS signalling server, standalone or embeddable.

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages