Copy-paste prompt
Paste this into your assistant from the root of your project. It works for Flutter apps, web apps and React apps, with a Node or Go backend.
Add Gravix Cloud real-time voice and video to this project.
Read https://www.gravixcloud.com/llms-full.txt first and follow it exactly.
Rules:
1. Detect the stack. Flutter app: add the gravix_rtc package
(flutter pub add gravix_rtc). Web or React app: add gravix-rtc
(npm install gravix-rtc). Use only APIs shown in that file.
2. The App Certificate is a server secret. Never put it, or code that
signs tokens, in the Flutter app or the browser bundle.
3. On the backend, add POST /rtc/token. It takes the user from the app's
existing login, signs a short-lived token with gravix-rtc-token (Node)
or gravix-token-go (Go), and returns { token, url }. Read GRAVIX_APP_ID,
GRAVIX_APP_CERTIFICATE and GRAVIX_URL from the environment.
4. In the app, create GravixTokenProvider.endpoint(<that endpoint>) and
join with connectWithTokenProvider.
5. Add the microphone and camera permissions for each platform.
6. Leave the room (disconnect) when the screen closes.
7. Do not invent class names, options or events. If something is not in
the file, stop and ask me.Before you run it, have your App ID, App Certificate and region URL ready from your dashboard. Put them in your server's environment yourself. Do not paste the App Certificate into the chat.
Plain-text docs for assistants
Two files follow the llms.txt convention, so an assistant can read them without a browser.
/llms.txt: a short index with the key rules and links./llms-full.txt: the whole integration guide on one page, with all the code.
Both are made from the same samples as these docs, checked against gravix_rtc 0.4.13and gravix-rtc 0.6.6.
Packages
| Where | Package | Install |
|---|---|---|
| Flutter app (Android, iOS) | gravix_rtc on pub.dev | flutter pub add gravix_rtc |
| Web or React app | gravix-rtc on npm | npm install gravix-rtc |
| Node backend | gravix-rtc-token on npm | npm install gravix-rtc-token |
| Go backend | gravix-token-go | Ask us for access |
Android and iOS apps use the Flutter SDK. There is no separate native Kotlin or Swift SDK, so an assistant should not look for one.
Token contract
This is the part an assistant must not change.
- The App Certificate is a server secret. It never goes into app code, a mobile build or a web bundle.
- Your backend has one endpoint behind your existing login. It signs a short-lived token for that user and room.
- The server takes the identity from its own session and decides whether the user may publish. The request body is only a wish.
- The endpoint answers
{ token, url }. The app never hard-codes the region URL. - Pass the plain room name. The token library adds your App ID prefix.
// Request: what the SDK posts to your endpoint
POST /rtc/token
{ "room": "stream-42", "identity": "user-17", "name": "Asha", "can_publish": false }
// Response: what your endpoint must answer
{ "token": "<signed JWT>", "url": "wss://YOUR-REGION-URL" }Server code
The whole backend a join needs. Read the App ID, App Certificate and region URL from the environment.
// server.ts (your backend, Node 18+). This file never ships inside an app.
import express from 'express';
import { createToken } from 'gravix-rtc-token';
const APP_ID = process.env.GRAVIX_APP_ID!; // from your dashboard
const APP_CERTIFICATE = process.env.GRAVIX_APP_CERTIFICATE!; // server only, never in an app
const REGION_URL = process.env.GRAVIX_URL!; // wss://YOUR-REGION-URL
const app = express();
app.use(express.json());
// requireUser is your own login check: the identity comes from YOUR session,
// never from the request body.
app.post('/rtc/token', requireUser, async (req, res) => {
const room = req.body?.room;
if (typeof room !== 'string' || !/^[\w.-]{1,64}$/.test(room)) {
return res.status(400).json({ error: 'invalid room' });
}
const token = await createToken(APP_ID, APP_CERTIFICATE, {
room, // the library adds your App ID prefix
identity: req.user.id,
name: req.user.name,
canPublish: true, // false = listen only; decide it here, not in the app
ttlSeconds: 60 * 60, // short-lived: 1 hour
});
res.json({ token, url: REGION_URL });
});
app.listen(3000);// token.go (your backend, Go 1.21+). gravixtoken is the gravix-token-go package.
func tokenHandler(w http.ResponseWriter, r *http.Request) {
user, ok := currentUser(r) // your own login check
if !ok {
http.Error(w, "sign in first", http.StatusUnauthorized)
return
}
var body struct {
Room string `json:"room"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.Room == "" {
http.Error(w, "invalid room", http.StatusBadRequest)
return
}
// The App Certificate stays in the server's environment.
jwt, err := gravixtoken.New(os.Getenv("GRAVIX_APP_ID"), os.Getenv("GRAVIX_APP_CERTIFICATE")).
Room(body.Room).
Identity(user.ID).
Name(user.Name).
CanPublish(true). // false = listen only
TTL(time.Hour). // short-lived
ToJWT()
if err != nil {
http.Error(w, "could not sign", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"token": jwt,
"url": os.Getenv("GRAVIX_URL"), // wss://YOUR-REGION-URL
})
}Client code
The minimal join. The app points the SDK at your endpoint and never sees the App Certificate.
import 'package:gravix_rtc/gravix_rtc.dart';
// One provider per signed-in user. It caches each token until it is about to expire.
final tokens = GravixTokenProvider.endpoint(
Uri.parse('https://YOUR-BACKEND/rtc/token'),
headers: {'Authorization': 'Bearer $sessionJwt'}, // your app's own login session
);
final room = GravixRoomService();
Future<void> join(String roomId, String userId, String name) async {
room.onRemoteVideoTrack = (uid, track) {
// show it with VideoTrackRenderer(track)
};
final ok = await room.connectWithTokenProvider(
tokenProvider: tokens,
request: GravixTokenRequest(
room: roomId,
identity: userId,
name: name,
canPublish: true,
),
publishMic: true,
enableVideo: true,
);
if (!ok) {
// room.lastTokenError is set when your token endpoint failed
}
}
Future<void> leave() => room.disconnect();import { Room, RoomEvent, GravixTokenProvider, connectWithTokenProvider } from 'gravix-rtc';
// Calls YOUR backend with the user's session cookie. Tokens are cached until they expire.
const tokens = GravixTokenProvider.endpoint('https://YOUR-BACKEND/rtc/token', {
credentials: 'include',
});
const room = new Room();
const stage = document.getElementById('stage')!;
room
.on(RoomEvent.TrackSubscribed, (track) => {
stage.appendChild(track.attach()); // a <video> or <audio> element
})
.on(RoomEvent.TrackUnsubscribed, (track) => {
track.detach().forEach((el) => el.remove());
});
export async function join(roomId: string, userId: string, name: string) {
await connectWithTokenProvider(room, {
tokenProvider: tokens,
request: { room: roomId, identity: userId, name },
});
await room.localParticipant.setMicrophoneEnabled(true);
await room.localParticipant.setCameraEnabled(true);
}
export const leave = () => room.disconnect();- Flutter:
connectWithTokenProviderreturns false instead of throwing.room.lastTokenErrorsays why. - Web:
connectWithTokenProviderthrows when the token cannot be fetched. Catch it. - React: create the
RoominuseEffectand callroom.disconnect()in the cleanup. The live streaming sample shows a full component.
Platform setup
Flutter apps declare microphone and camera access, and ask for them at runtime before the user joins with them on.
<!-- android/app/src/main/AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<!-- Bluetooth headsets: Android 12+ needs BLUETOOTH_CONNECT -->
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<!-- ... your <application> ... -->
</manifest><!-- ios/Runner/Info.plist -->
<key>NSMicrophoneUsageDescription</key>
<string>Talk in calls</string>
<key>NSCameraUsageDescription</key>
<string>Video in calls</string>
<!-- Keep call audio playing with the screen locked or the app in the background -->
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>- Minimums: Android 7.0 (API 24), iOS 13, Flutter 3.38 and Dart 3.10.
- On Android 12 and later, request
BLUETOOTH_CONNECTat runtime so Bluetooth headsets are found. - Background audio on Android needs the SDK's foreground service, which is off by default. See Android.
- Web: nothing to declare, but the page must be served over HTTPS (a local development server on your own machine also works).
Common mistakes
- Signing tokens in the app, or putting the App Certificate in any client build or a browser-exposed env file.
- Trusting
identityorcan_publishfrom the request body. - Returning only the token. The endpoint must return
{ token, url }. - Using the deprecated
GravixCloudBackend, which sends a secret from the device. UseGravixTokenProvider. - Missing
NSMicrophoneUsageDescriptionorNSCameraUsageDescriptionon iOS, or the Android permissions. - Not leaving the room when the screen closes. Call
disconnect(), anddispose()on Flutter. - Inventing API. If a class, option or event is not in these docs, the assistant should ask you.
Done when
- The App Certificate exists only in your server's configuration.
- Two devices join the same room and see and hear each other.
- Permissions are declared and requested on every platform you ship.
- Leaving the screen leaves the room.
