Skip to main content
Markdown

Getting started with FastAPI

Live updates, chat, who is online, calls and group calls in a FastAPI or Starlette app, through WordPlus Cloud. Two packages:

  • Your server (wordplus-realtime, with wordplus_realtime.fastapi) declares your rooms once, says who may join them and call whom, and sends events to rooms and to users: the browser routes as a router, for the user your app finds.
  • The page (@wordplus/realtime, from npm) connects to WordPlus Cloud, joins rooms, and sends and gets events: emit, emitWithAck, volatile.

What goes through WordPlus Cloud is sealed with keys only your server and the browsers of your users hold. Ready-made parts come with it, all optional: the Better Messages chat, its list of who is online and its call screen. The example app is a whole shop built this way.

Your framework can run websockets of its own. Why WordPlus Cloud says what the cloud runs for you, and what it adds.

Try it live at fastapi.docs.wordplus.cloud: the example app of this guide as a demo, a shop of your own for a day, with Ben and Cara opening in windows of their own.

Two browsers side by side: Ann marks order 1042 as shipped on the staff page of the shop, and the order page of Ben shows Shipped at once, without a reload

1. Your app in WordPlus Cloud​

In your WordPlus Cloud account, go to Apps → Add app. Name it, and list the addresses its pages are served from: https://shop.example.com, and http://localhost:8000 while you develop. Only pages on those addresses may connect.

You get the app key, public because every page carries it, and its secret, for your server only. Make a local key of your own too: everything sealed is sealed with keys derived from it, and it never leaves your servers.

WORDPLUS_SITE=s1500000007
WORDPLUS_SECRET=…
WORDPLUS_LOCAL_KEY=… # openssl rand -hex 32; the same on every server of the app, kept as long as the app

2. Install​

pip install 'wordplus-realtime[fastapi]'    # uv add works the same
npm install @wordplus/realtime
npx wordplus-realtime copy static/ # the worker, the room client and the script of the components

The worker lets the tabs of a user share one connection, the room client runs group calls, and realtime-ui.min.js is the ready-made parts. Copy them again after every update of the npm package, in your build.

3. Your server​

# app.py
from fastapi import FastAPI, Request
from fastapi.staticfiles import StaticFiles
from starlette.middleware.sessions import SessionMiddleware
from wordplus_realtime import Realtime
import wordplus_realtime.fastapi as wr

realtime = Realtime.from_environment( # WORDPLUS_SITE, WORDPLUS_SECRET, WORDPLUS_LOCAL_KEY
# What others see of a user: in presence, calls and the directory.
profile=lambda user: {"name": name_of(user), "avatar": avatar_of(user)},
worker="/static/realtime-worker.js",
rooms="/static/realtime-rooms.min.js",
)

# The room of an order: its customer may join, and the last 20 updates are kept.
realtime.room("order:{id}", lambda user, p: user is not None and owner_of(p["id"]) == user, history=20)

async def current_user(request: Request):
"""The user id of the request, or None: your sign-in, here a session."""
return request.session.get("user")

app = FastAPI()
app.add_middleware(SessionMiddleware, secret_key=SESSION_SECRET)
app.include_router(wr.router(realtime, current_user), prefix="/api/realtime") # POST /auth, /calls, /keys, /webhook
app.mount("/static", StaticFiles(directory="static"), name="static")
  • current_user(request) gives the request user id, or None. It may be async: a token, a session, a dependency of your own.
  • The pattern of a room names its parts: order:{id} matches order:1042, and the rule gets {"id": "1042"}.
  • The rules (room(), calls(), kind(), profile) are plain functions. The router runs them on a worker thread, so a blocking database query in one does not hold up the event loop.
  • The routes take JSON POSTs only. A page on another site cannot send JSON without asking first, and it is never allowed to, so a session cookie needs no CSRF token here.

4. The page​

from fastapi.templating import Jinja2Templates

templates = Jinja2Templates(directory="templates")

@app.get("/orders/{pk}")
async def order_page(request: Request, pk: int):
config = wr.config_for(realtime, await current_user(request))
return templates.TemplateResponse(request, "order.html", {"realtime": config, "pk": pk})
<!-- templates/order.html: tojson escapes it for a <script> -->
<script type="application/json" id="realtime">{{ realtime|tojson }}</script>
<script type="module" src="/static/order.js"></script>
// order.js, through your bundler (esbuild, Vite, webpack)
import { connect } from '@wordplus/realtime';

const rt = await connect( JSON.parse( document.getElementById( 'realtime' ).textContent ) );

const order = rt.join( 'order:1042' );
order.on( 'updated', ( data ) => showStatus( data.status ) );
order.on( 'error', ( e ) => console.warn( e.code ) ); // forbidden, no_rule …

A single-page app can skip rendering, and connect with connect( { app: 's1500000007', worker: '/static/realtime-worker.js' } ), which asks /api/realtime/keys first.

5. Send​

@app.post("/orders/{pk}/ship")
async def ship(pk: int):
order = await mark_shipped(pk)
await realtime.to(f"order:{pk}").aemit("updated", {"status": order.status})
return {"ok": True}

Everyone in the room gets { status }, sealed with the room key on the way, and a page that reconnects later gets it from the room history. aemit() runs the request on a worker thread.

Next​