---
sidebar_position: 1
---

# 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 is `wordplus/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.

```dotenv
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**

```bash
npm install @wordplus/realtime
```

**PHP**

```bash
composer require wordplus/realtime-php      # PHP 8.1 or later, with sodium
npm install @wordplus/realtime
```

**Python**

```bash
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](https://packages.wordplus.cloud/npm/index.json).
- **Composer:** `composer config repositories.wordplus composer https://packages.wordplus.cloud/composer`, then `composer require` as 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:

```bash
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**

```ts
// 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 );
```

**PHP**

```php
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 );
```

**Python**

```python
# 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**

```ts
// 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 ) );
```

**PHP**

```php
// 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() );
```

**Python**

```python
# 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

```ts
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**

```ts
const config = await realtime.config( userId );   // in a Next.js server component, then <RealtimeProvider options={ config }>
```

**PHP**

```php
// 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>
```

**Python**

```python
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**

```ts
await realtime.publish( 'private-orders-42', 'updated', { status: 'paid' } );
```

**PHP**

```php
$realtime->publish( 'private-orders-42', 'updated', [ 'status' => 'paid' ] );   // Laravel: broadcast( new OrderPaid( $order ) )
```

**Python**

```python
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.

![Two browsers side by side: in Ann’s, the shop’s staff page marks order 1042 as shipped; in Ben’s, his order page shows Shipped at once, without a reload](/img/apps/orders-live.webp)

## Next

- [Channels](channels.md), [presence](presence.md) and [publishing](publishing.md)
- [Calls, group calls and the call screen](calls.md)
- [React and React Native](react.md)
- [Encryption and your users](encryption.md)
- [Laravel](laravel.md), [Django](django.md) and [FastAPI](fastapi.md)
- The server references for [Node](server.md), [PHP](php-reference.md) and [Python](python-reference.md), and the [browser reference](browser.md)
- [Errors](errors.md) and [limits](limits.md)
