# dTelecom — Complete Documentation for LLMs > dTelecom is a decentralized WebRTC video conferencing platform built on Solana. It is an API-compatible fork of LiveKit with decentralized SFU node discovery via Solana blockchain and Ed25519 JWT signing. Use it to build real-time audio/video applications with JavaScript/TypeScript. ## Key Differences from LiveKit | Aspect | LiveKit | dTelecom | |--------|---------|----------| | Server URL | Fixed (e.g. `wss://my-livekit.example.com`) | Dynamic — from `getWsUrl()` via Solana registry | | JWT signing | HMAC-SHA256 | Ed25519 (Solana keypair) | | API keys | Arbitrary strings | Solana Ed25519 keypair (public=API key, private=API secret) | | Node discovery | N/A | Solana blockchain registry | | Client SDK | `livekit-client` | `@dtelecom/livekit-client` (API-compatible) | | Server SDK | `livekit-server-sdk` | `@dtelecom/server-sdk-js` (adds `getWsUrl()`) | | React components | `@livekit/components-react` | `@dtelecom/components-react` (same API) | Component names like `LiveKitRoom` are kept intentionally for easy migration. --- ## NPM Packages ```bash # Server-side npm install @dtelecom/server-sdk-js # Client-side npm install @dtelecom/livekit-client @dtelecom/components-react @dtelecom/components-styles ``` | Package | Purpose | |---------|---------| | `@dtelecom/server-sdk-js` | Server (CommonJS): AccessToken, getWsUrl(), getApiUrl(), RoomServiceClient, WebhookReceiver, EgressClient, IngressClient | | `@dtelecom/livekit-client` | Client: Room, RoomEvent, Track, LocalParticipant, RemoteParticipant | | `@dtelecom/components-react` | React: LiveKitRoom, VideoConference, PreJoin, Chat, GridLayout, ParticipantTile, ControlBar, useTracks, useChat, useParticipants, useLocalParticipant, useRemoteParticipants, useRoomContext, useConnectionState | | `@dtelecom/components-styles` | Default CSS styles for React components | --- ## Environment Variables ```bash API_KEY= API_SECRET= SOLANA_CONTRACT_ADDRESS=E2FcHsC9STeB6FEtxBKGAwMTX7cbfYMyjSHKs4QbBAmh SOLANA_NETWORK_HOST_HTTP=https://api.mainnet-beta.solana.com SOLANA_REGISTRY_AUTHORITY=6KVRs6Yr2oYzddepFdtWrFmVq8sgELcXzbUy7apwuQX4 WEBHOOK_URL=https://your-public-url.com/api/webhook ``` - `API_KEY` / `API_SECRET`: Ed25519 keypair from cloud.dtelecom.org (project-specific) - The three `SOLANA_*` values are mainnet constants (same for all projects) - `WEBHOOK_URL`: Public URL where dTelecom SFU nodes send webhook events (set per-token via `webHookURL`). During dev, use cloudflared (`cloudflared tunnel --url http://localhost:3000`) since SFU nodes can't reach localhost. - The server SDK reads these from `process.env` automatically --- ## Connection Flow ``` 1. Client → Your Server: "I want to join room X" 2. Your Server: Create AccessToken with Ed25519 signing 3. Your Server: Call token.getWsUrl(clientIp) 4. getWsUrl() → Solana: Query the node registry on-chain 5. Solana → Your Server: Return the best SFU node URL 6. Your Server → Client: Return { token, wsUrl } 7. Client → SFU Node: Connect via WebSocket ``` --- ## Server SDK API ### AccessToken ```typescript import { AccessToken } from '@dtelecom/server-sdk-js'; const at = new AccessToken(process.env.API_KEY!, process.env.API_SECRET!, { identity: 'user-1', // unique participant identity name: 'Display Name', // optional display name metadata: '{"role":"host"}', // optional metadata string webHookURL: process.env.WEBHOOK_URL, // optional: SFU sends events to this URL }); // Grant permissions at.addGrant({ roomJoin: true, // can join the room room: 'my-room', // room name canPublish: true, // can publish audio/video canSubscribe: true, // can subscribe to others' tracks roomCreate: true, // can create rooms (for admin tokens) roomAdmin: true, // can moderate (for admin tokens) }); // Get the JWT string const token = at.toJwt(); // Get the WebSocket URL for the best SFU node (wss:// — for client connections) // clientIp should be the connecting user's IP (from x-forwarded-for header) const wsUrl = await at.getWsUrl(clientIp); // Get the API URL for server-side API calls (https:// — for RoomServiceClient) // No clientIp needed; returns the Twirp HTTP endpoint for the best node const apiUrl = await at.getApiUrl(); ``` ### Role-based metadata pattern Embed a role in participant metadata for host/guest differentiation: ```typescript // Server-side: set role in token const metadata = JSON.stringify({ role: 'host' }); // or 'guest' const at = new AccessToken(apiKey, apiSecret, { identity: 'user-1', name: 'Alice', metadata }); at.addGrant({ roomJoin: true, room: 'my-room', canPublish: true, canSubscribe: true, canPublishData: true }); ``` ```tsx // Client-side: read role from metadata import { useLocalParticipant } from '@dtelecom/components-react'; const { localParticipant } = useLocalParticipant(); const meta = JSON.parse(localParticipant.metadata || '{}'); const isHost = meta.role === 'host'; // Use isHost to conditionally render kick/mute buttons (UI only — verify server-side before acting) ``` ### RoomServiceClient ```typescript import { AccessToken, RoomServiceClient } from '@dtelecom/server-sdk-js'; // Use getApiUrl() to dynamically resolve the Twirp HTTP endpoint const at = new AccessToken(process.env.API_KEY!, process.env.API_SECRET!, { identity: 'server' }); const apiUrl = await at.getApiUrl(); const client = new RoomServiceClient(apiUrl, process.env.API_KEY!, process.env.API_SECRET!); // Create a room const room = await client.createRoom({ name: 'my-room', emptyTimeout: 600, // seconds before empty room closes maxParticipants: 20, metadata: JSON.stringify({ topic: 'standup' }), }); // List all rooms const rooms = await client.listRooms(); // List specific rooms const rooms = await client.listRooms(['room-1', 'room-2']); // Delete a room (disconnects all participants) await client.deleteRoom('my-room'); // List participants in a room const participants = await client.listParticipants('my-room'); // Get a specific participant const p = await client.getParticipant('my-room', 'user-1'); // Update participant metadata/name await client.updateParticipant('my-room', 'user-1', { metadata: JSON.stringify({ role: 'presenter' }), name: 'New Name', }); // Remove participant from room await client.removeParticipant('my-room', 'user-1'); // Update room metadata await client.updateRoomMetadata('my-room', JSON.stringify({ recording: true })); // Mute a track await client.mutePublishedTrack('my-room', 'user-1', 'TR_xxxxx', true); // Send data to participants const encoder = new TextEncoder(); const data = encoder.encode(JSON.stringify({ message: 'hello' })); await client.sendData('my-room', data, 0); // 0=RELIABLE, 1=LOSSY await client.sendData('my-room', data, 0, { destinationSids: ['PA_xxx'], topic: 'chat' }); ``` ### Decentralized API behavior - **Write operations** (RemoveParticipant, MutePublishedTrack, UpdateParticipant, SendData, CreateRoom, DeleteRoom) are **broadcast via P2P** — call any node and the action reaches the correct target. - **Read operations** (ListRooms, ListParticipants, GetParticipant) are **local to a single node's in-memory store**. They return only data from that node. A participant on node A won't appear in `getParticipant()` called on node B. ### Host controls pattern To let hosts kick or mute participants, your backend verifies the caller's role before executing. **IMPORTANT:** Do NOT use `getParticipant()` to verify the caller — it is a local read that only checks the current node. The caller may be on a different node, causing a 404. Instead, pass `callerRole` from the client (the role was set in the JWT by your server, so it is trustworthy). ```typescript // Backend API route (e.g. /api/kick) const { room, identity, callerRole } = await req.json(); // Verify the caller is a host (role was embedded in JWT by your server) if (callerRole !== 'host') { return NextResponse.json({ error: 'Only hosts can kick participants' }, { status: 403 }); } // Write operations broadcast via P2P — they work from any node const client = await getRoomService(); await client.removeParticipant(room, identity); // kick // or: await client.mutePublishedTrack(room, identity, trackSid, true); // mute ``` Never trust client-side role checks alone — always verify on the server. ### WebhookReceiver The webhook URL is set per-token via the `webHookURL` option on `AccessToken` (not in a dashboard). The SFU node sends events to that URL for any participant using that token. In dTelecom's decentralized network, webhooks are signed by the **SFU node** with its own Ed25519 key (not your app's key). `WebhookReceiver` automatically validates the node via the Solana registry, verifies the JWT signature, and checks the SHA-256 body hash. `receive()` is **async**. ```typescript import { WebhookReceiver } from '@dtelecom/server-sdk-js'; import { NextRequest, NextResponse } from 'next/server'; const receiver = new WebhookReceiver(process.env.API_KEY!, process.env.API_SECRET!); // Next.js App Router example export async function POST(req: NextRequest) { const body = await req.text(); // raw body needed for SHA-256 verification const authToken = req.headers.get('Authorization') ?? ''; const event = await receiver.receive(body, authToken); // event.event: 'room_started' | 'room_finished' | 'participant_joined' | 'participant_left' // event.room: Room object // event.participant: Participant object (for participant events) return NextResponse.json({ ok: true }); } ``` --- ## Client SDK API ### Room (vanilla JS/TS) ```typescript import { Room, RoomEvent, Track, VideoPresets, RemoteTrack, RemoteTrackPublication, RemoteParticipant, DataPacket_Kind, createLocalVideoTrack, createLocalAudioTrack, } from '@dtelecom/livekit-client'; // Create and connect const room = new Room(); await room.connect(wsUrl, token); // Enable camera and mic await room.localParticipant.setCameraEnabled(true); await room.localParticipant.setMicrophoneEnabled(true); // Screen share await room.localParticipant.setScreenShareEnabled(true); // Advanced: create tracks manually const videoTrack = await createLocalVideoTrack({ resolution: VideoPresets.hd }); const audioTrack = await createLocalAudioTrack({ echoCancellation: true, noiseSuppression: { ideal: true } }); await room.localParticipant.publishTrack(videoTrack); await room.localParticipant.publishTrack(audioTrack); // Subscribe to tracks room.on(RoomEvent.TrackSubscribed, (track: RemoteTrack, pub: RemoteTrackPublication, participant: RemoteParticipant) => { const element = track.attach(); // returns HTMLVideoElement or HTMLAudioElement document.getElementById('container')!.appendChild(element); }); // Get a specific track from a participant const micPub = room.localParticipant.getTrack(Track.Source.Microphone); // Returns TrackPublication | undefined // Properties: micPub?.trackSid, micPub?.isMuted // Reconnection events room.on(RoomEvent.Reconnecting, () => { /* show reconnecting UI */ }); room.on(RoomEvent.Reconnected, () => { /* hide reconnecting UI */ }); room.on(RoomEvent.Disconnected, (reason) => { /* handle disconnect */ }); // Data messages const encoder = new TextEncoder(); const data = encoder.encode(JSON.stringify({ message: 'hello' })); room.localParticipant.publishData(data, DataPacket_Kind.RELIABLE); room.on(RoomEvent.DataReceived, (payload: Uint8Array, participant, kind) => { const msg = new TextDecoder().decode(payload); }); // Disconnect room.disconnect(); ``` #### participant.getTrack() Look up a specific track publication by source: ```typescript import { Track } from '@dtelecom/livekit-client'; // Works on both LocalParticipant and RemoteParticipant const micPub = participant.getTrack(Track.Source.Microphone); // TrackPublication | undefined const camPub = participant.getTrack(Track.Source.Camera); // TrackPublication | undefined const screenPub = participant.getTrack(Track.Source.ScreenShare); // TrackPublication | undefined // Useful properties on TrackPublication micPub?.trackSid // unique track ID (string) micPub?.isMuted // whether the track is muted (boolean) micPub?.track // the underlying Track object (or undefined if not subscribed) ``` ### React Components #### LiveKitRoom and data-lk-theme ```tsx import { LiveKitRoom, VideoConference } from '@dtelecom/components-react'; import '@dtelecom/components-styles'; // Simple usage — full conference UI // LiveKitRoom automatically sets `data-lk-theme="default"` on its container element. // All child components inherit the theme. {}} onDisconnected={handleLeave} onError={(error) => console.error('Room error:', error)} onMediaDeviceFailure={(failure) => { // Camera/mic permission denied or device unavailable alert('Could not access camera or microphone.'); }} > ``` ##### data-lk-theme dTelecom component styles are scoped to the `data-lk-theme` attribute. `` sets this automatically on its wrapper element. If you render components **outside** of `` (e.g. ``), wrap them manually: ```tsx // Components outside LiveKitRoom need an explicit theme wrapper
``` Available themes: `"default"`, `"huddle"`. #### PreJoin component ```tsx import { PreJoin, LocalUserChoices } from '@dtelecom/components-react'; import '@dtelecom/components-styles'; // Pre-join page for camera/mic preview and device selection { // choices: { username, videoEnabled, audioEnabled, videoDeviceId, audioDeviceId } // Fetch token and navigate to room page }} onError={(err) => console.error(err.message)} defaults={{ username: 'Alice', videoEnabled: true, audioEnabled: true }} /> ``` `ParticipantTile` auto-shows a name placeholder when the camera is off — no extra code needed. #### Chat component ```tsx import { Chat } from '@dtelecom/components-react'; // Inside a — renders full chat UI (requires canPublishData: true in token grant) ``` ```tsx // Custom chat UI with useChat hook import { useChat } from '@dtelecom/components-react'; function CustomChat() { const { chatMessages, send } = useChat(); // NOTE: `send` may be undefined before the room is connected. // Use optional chaining: send?.('message') to avoid errors. return (
{chatMessages.map((msg) => (
{msg.from?.name}: {msg.message}
))}
{ e.preventDefault(); send?.('message'); }}>
); } ``` #### ControlBar ```tsx import { ControlBar } from '@dtelecom/components-react'; // Default — renders all controls with icons and labels // Variation controls the display style // icons only, no labels // icons + labels (default) // labels only, no icons // Selectively enable/disable individual controls ``` **ControlBar props:** - `variation`: `"minimal"` | `"verbose"` | `"textOnly"` — controls how buttons are rendered. - `controls`: `{ microphone?: boolean, camera?: boolean, chat?: boolean, screenShare?: boolean, leave?: boolean }` — toggle individual control buttons. #### Custom layout ```tsx import { GridLayout, ParticipantTile, useTracks, RoomAudioRenderer, ControlBar, useConnectionState } from '@dtelecom/components-react'; import { Track, ConnectionState } from '@dtelecom/livekit-client'; function MyConference() { const tracks = useTracks([ { source: Track.Source.Camera, withPlaceholder: true }, { source: Track.Source.ScreenShare, withPlaceholder: false }, ], { onlySubscribed: false }); const connectionState = useConnectionState(); return (
{connectionState === ConnectionState.Reconnecting &&
Reconnecting...
}
); } ``` #### Participant hooks ```tsx import { useParticipants, useRemoteParticipants, useLocalParticipant, useRoomContext } from '@dtelecom/components-react'; // All participants (local + remote) const participants = useParticipants(); // Only remote participants const remoteParticipants = useRemoteParticipants(); // Local participant info const { localParticipant } = useLocalParticipant(); // Access the Room object directly const room = useRoomContext(); ``` All hooks must be used inside a `` component. --- ## CSS Custom Properties dTelecom component styles use CSS custom properties (variables) prefixed with `--lk-`. Override them to customize the look and feel: ```css [data-lk-theme="default"] { --lk-bg: /* primary background color */ --lk-bg2: /* secondary/surface background color */ --lk-fg: /* primary foreground/text color */ --lk-accent-bg: /* accent color for active/highlighted elements */ --lk-danger: /* color for destructive actions (leave, errors) */ --lk-border-radius: /* border radius for cards, buttons, tiles */ --lk-grid-gap: /* gap between grid tiles */ --lk-control-bar-height: /* height of the bottom control bar */ } ``` Example override: ```css [data-lk-theme="default"] { --lk-bg: #1a1a2e; --lk-fg: #eaeaea; --lk-accent-bg: #0f3460; --lk-danger: #e94560; --lk-border-radius: 12px; } ``` --- ## Complete Conference App (Next.js) A minimal Google Meet-style conference app. The basic version is two files, ~80 lines total. For a full-featured app with PreJoin, Chat, host controls, and screen sharing, see the [Conference App Plan](https://github.com/dTelecom/docs/blob/main/CONFERENCE_APP_PLAN.md) — a complete blueprint with all code, architecture notes, and SDK mapping. ### Server: app/api/join/route.ts ```typescript import { AccessToken } from '@dtelecom/server-sdk-js'; import { NextRequest, NextResponse } from 'next/server'; export async function POST(req: NextRequest) { const { room, identity } = await req.json(); if (!room || !identity) { return NextResponse.json({ error: 'room and identity are required' }, { status: 400 }); } const at = new AccessToken( process.env.API_KEY!, process.env.API_SECRET!, { identity, name: identity, webHookURL: process.env.WEBHOOK_URL } ); at.addGrant({ roomJoin: true, room, canPublish: true, canSubscribe: true, canPublishData: true, // needed for chat }); const token = at.toJwt(); const clientIp = (req.headers.get('x-forwarded-for') ?? '127.0.0.1').split(',')[0].trim(); const wsUrl = await at.getWsUrl(clientIp); return NextResponse.json({ token, wsUrl }); } ``` ### Client: app/page.tsx ```tsx 'use client'; import { LiveKitRoom, VideoConference } from '@dtelecom/components-react'; import '@dtelecom/components-styles'; import { useState } from 'react'; export default function Home() { const [room, setRoom] = useState(''); const [identity, setIdentity] = useState(''); const [token, setToken] = useState(''); const [wsUrl, setWsUrl] = useState(''); async function joinRoom() { const res = await fetch('/api/join', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ room, identity }), }); const data = await res.json(); setToken(data.token); setWsUrl(data.wsUrl); } function leaveRoom() { setToken(''); setWsUrl(''); } if (!token || !wsUrl) { return (

Join a Conference

setRoom(e.target.value)} /> setIdentity(e.target.value)} />
); } return ( ); } ``` ### Setup ```bash npx create-next-app@latest my-conference --app --typescript cd my-conference npm install @dtelecom/server-sdk-js @dtelecom/livekit-client @dtelecom/components-react @dtelecom/components-styles ``` Create `.env.local`: ``` API_KEY= API_SECRET= SOLANA_CONTRACT_ADDRESS=E2FcHsC9STeB6FEtxBKGAwMTX7cbfYMyjSHKs4QbBAmh SOLANA_NETWORK_HOST_HTTP=https://api.mainnet-beta.solana.com SOLANA_REGISTRY_AUTHORITY=6KVRs6Yr2oYzddepFdtWrFmVq8sgELcXzbUy7apwuQX4 ``` Run: `npm run dev` — open two tabs at http://localhost:3000, join the same room with different names. --- ## Room Permissions Reference | Field | Type | Description | |-------|------|-------------| | roomCreate | bool | Permission to create rooms | | roomJoin | bool | Permission to join a room | | roomAdmin | bool | Permission to moderate a room | | room | string | Name of the room (required if join or admin is set) | | canPublish | bool | Allow participant to publish tracks | | canSubscribe | bool | Allow participant to subscribe to tracks | | canPublishData | bool | Allow participant to publish data messages (required for chat) | --- ## Webhook Events The webhook URL is configured per-token via `webHookURL` in `AccessTokenOptions` (not in a dashboard). Webhooks are signed by SFU nodes with their own Ed25519 keys; `WebhookReceiver` validates node identity via the Solana registry. | Event | Fields | Description | |-------|--------|-------------| | `room_started` | room | A room was created | | `room_finished` | room | A room was closed | | `participant_joined` | room, participant | A participant joined | | `participant_left` | room, participant | A participant left | --- ## Source Code - Server SDK: https://github.com/dTelecom/server-sdk-js - Client SDK: https://github.com/dTelecom/client-sdk-js - React Components: https://github.com/dTelecom/components-js - Cloud Dashboard: https://cloud.dtelecom.org