Getting started
The realtime SDK has two halves:
- Your server decides who may listen to a channel, call whom, and see what, and signs it. On Node, that is this package's
@wordplus/realtime/server. On PHP and Laravel, it iswordplus/realtime-php, and on Python (Django, FastAPI)wordplus-realtime. All three answer the browser the same way. - The browser connects to WordPlus Cloud, asks your server for what it may do, and opens what arrives. That is this package's
@wordplus/realtime(and/react,/native), whatever your server speaks.
Where your server's code differs by language, each example below comes in Node, PHP and Python.
1. Your app in WordPlus Cloud
In your WordPlus Cloud account, go to Apps → Add app. Give the app a name and the addresses its pages are served from, such as https://app.example.com, plus http://localhost:3000 while you develop. Only pages on those addresses may connect.
- Capacitor: an app in Capacitor is
capacitor://localhost. - React Native: a React Native app sends no address, and is always let in.
You get the app's key, which is public because every page carries it, and its secret, which only your server has. Also make a local key of your own: it seals what goes through WordPlus Cloud, 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
- Node
- PHP
- Python
npm install @wordplus/realtime
composer require wordplus/realtime-php # PHP 8.1 or later, with sodium
npm install @wordplus/realtime
pip install 'wordplus-realtime[django]' # or [fastapi], or neither; uv add works the same
npm install @wordplus/realtime
The same packages are on WordPlus's own mirror, packages.wordplus.cloud, for a network that can't reach the registries:
- npm:
npm install https://packages.wordplus.cloud/npm/wordplus-realtime-1.0.0.tgz. Every version is listed in index.json. - Composer:
composer config repositories.wordplus composer https://packages.wordplus.cloud/composer, thencomposer requireas above. - pip and uv: add
--find-links https://packages.wordplus.cloud/pypi/index.html.
Then copy the browser's two files to where your app serves its static files:
npx wordplus-realtime copy public/ # realtime-worker.js and realtime-rooms.min.js
The worker lets every tab of a browser share one connection. Without it, each tab connects by itself and counts against your connections. Copy the files again after each update of the npm package.
3. Your server
Say who your users are, and who may listen to what:
- Node
- PHP
- Python
// lib/realtime.ts
import { createRealtime } from '@wordplus/realtime/server';
export const realtime = createRealtime( {
site: process.env.WORDPLUS_SITE!,
secret: process.env.WORDPLUS_SECRET!,
localKey: process.env.WORDPLUS_LOCAL_KEY!,
// Who is asking: their id, or null for nobody.
user: async ( request ) => ( await getSession( request ) )?.userId ?? null,
// What others see of them: in presence, calls and the directory.
profile: async ( user ) => ( { name: await nameOf( user ), avatar: await avatarOf( user ) } ),
} );
// Who may listen to an order's updates: its customer.
realtime.channel( 'private-orders-', async ( user, channel ) => user !== null && ( await ownerOf( channel.slice( 15 ) ) ) === user );
use WordPlus\Realtime\Realtime;
// In Laravel, the package makes this from config/wordplus-realtime.php: use the Realtime facade, and
// routes/channels.php for channels (see Laravel).
$realtime = Realtime::fromEnvironment( [
// What others see of a user: in presence, calls and the directory.
'profile' => fn ( $user ) => [ 'name' => name_of( $user ), 'avatar' => avatar_of( $user ) ],
] );
// Who may listen to an order's updates: its customer.
$realtime->channel( 'private-orders-', fn ( $user, $channel ) => null !== $user && owner_of( substr( $channel, 15 ) ) === $user );
# realtime.py
from wordplus_realtime import Realtime
realtime = Realtime.from_environment(
# What others see of a user: in presence, calls and the directory.
profile=lambda user: {"name": name_of(user), "avatar": avatar_of(user)},
)
# Who may listen to an order's updates: its customer.
realtime.channel("private-orders-", lambda user, channel: user is not None and owner_of(channel[15:]) == user)
The user is the signed-in user's id, or null (None) for nobody. Next, answer the browser at /api/realtime/auth, /calls and /keys:
- Node
- PHP
- Python
// Next.js: app/api/realtime/[action]/route.ts
import { realtime } from '@/lib/realtime';
export const POST = realtime.route;
// Express, or any Node server
app.use( '/api/realtime', express.json(), realtime.node( ( req ) => req.user?.id ?? null ) );
// Hono, Remix, SvelteKit, Bun, Deno, workers: any fetch handler
app.post( '/api/realtime/:action', ( c ) => realtime.route( c.req.raw ) );
// Laravel: nothing to add. The package's routes are /wordplus/realtime/auth, /calls and /keys, for the session's user.
// Plain PHP: public/api/realtime.php, with your web server sending /api/realtime/* here
$realtime->respond( basename( parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH ) ), current_user_id() );
# Django: urls.py, for the session's user
import wordplus_realtime.django as wr
urlpatterns = [path("api/realtime/", include(wr.urls(realtime)))]
# FastAPI and Starlette: current_user(request) gives the user's id, or None, and may be async
import wordplus_realtime.fastapi as wr
app.include_router(wr.router(realtime, current_user), prefix="/api/realtime")
# Flask, or any framework
@app.post("/api/realtime/<action>")
def realtime_route(action):
status, body = realtime.answer_request(request.method, request.content_type or "", action, request.get_data(), current_user_id())
return jsonify(body), status, {"Cache-Control": "no-store"}
The routes take JSON POSTs only. A page on another site can't send JSON without asking first, and it is never allowed to.
4. The browser
import { connect } from '@wordplus/realtime';
const rt = await connect( {
site: 's1500000007',
worker: '/realtime-worker.js',
rooms: '/realtime-rooms.min.js',
} );
const orders = rt.subscribe( 'private-orders-42' );
orders.on( 'updated', ( data ) => refresh( data ) );
orders.on( 'realtime:error', ( e ) => console.warn( e.code ) );
connect() first asks your keys route for the user's keys and identity. To skip that request, render your server's settings for the user into the page, and pass them as they are: connect( config ).
- Node
- PHP
- Python
const config = await realtime.config( userId ); // in a Next.js server component, then <RealtimeProvider options={ config }>
// Laravel: @wordplusRealtime in the layout writes window.wordplusRealtimeConfig, with the CSRF token.
<script>window.wordplusRealtimeConfig = <?= json_encode( $realtime->config( current_user_id() ), JSON_HEX_TAG | JSON_HEX_AMP ) ?>;</script>
config = realtime.config(current_user_id()) # into the page as JSON
config = wordplus_realtime.django.config_for(realtime, request) # Django: with the CSRF token; {{ config|json_script:"realtime" }}
5. Publish
- Node
- PHP
- Python
await realtime.publish( 'private-orders-42', 'updated', { status: 'paid' } );
$realtime->publish( 'private-orders-42', 'updated', [ 'status' => 'paid' ] ); // Laravel: broadcast( new OrderPaid( $order ) )
realtime.publish("private-orders-42", "updated", {"status": "paid"}) # await realtime.apublish(…) from async code
Every browser on the channel gets { status: 'paid' }, sealed on the way with the channel's key.
Next
- Channels, presence and publishing
- Calls, group calls and the call screen
- React and React Native
- Encryption and your users
- Laravel, Django and FastAPI
- The server references for Node, PHP and Python, and the browser reference
- Errors and limits