cometchat-native-production
Purpose
Teaches Claude how to move a React Native CometChat integration from dev-mode Auth Key to production-ready server-minted auth tokens + user CRUD. Covers:
- Why the dev
authKeycan't ship to production - Auth Key vs REST API Key — which lives where
- Server endpoint recipes (Express / Hono / Firebase Functions / Vercel)
- Client-side:
CometChatUIKit.login({ authToken })+ token refresh - User CRUD endpoints + auth-provider integration (Firebase Auth / Supabase / Clerk / Auth0)
- Security checklist + rate limits
Read cometchat-native-core first (init/login/wrapper chain) before this skill — production just swaps one prop on the provider, but understanding the provider lifecycle is the prerequisite.
Ground truth: docs/ui-kit/react-native/methods.mdx, and the cross-platform REST API at https://{APP_ID}.api-{REGION}.cometchat.io/v3/.
1. Why production auth matters
In dev mode, CometChatUIKit.login({ uid: "..." }) uses the authKey passed to CometChatUIKit.init({ authKey }). That key is embedded in your React Native bundle. For a signed iOS .ipa or Android .apk/.aab, anyone can extract it with standard reverse-engineering tools (unzip, strings, apktool, ReverseAPK) and use it to log in as ANY user in your CometChat app — read private messages, send as other users, access every conversation.
Production MUST use server-side token generation:
- Your server holds the REST API Key (a different key from the client Auth Key).
- On user login, your server calls CometChat's REST API with the REST API Key to mint a short-lived Auth Token for that specific UID.
- Your client receives the Auth Token and calls
CometChatUIKit.login({ authToken }). - If the token leaks, the blast radius is one user session, not your whole app.
Exactly the same threat model as JWTs for a REST API. If you've built a login flow before, this is that.
2. Auth Key vs REST API Key — two different keys
Easy to confuse. Both come from the CometChat Dashboard (your app → API & Auth Keys), but they live in different places and have different privileges.
| Key | Where in dashboard | Purpose | Where it lives |
|---|---|---|---|
| Auth Key | "Auth Keys" table | Client-side SDK login({ uid }) in dev mode |
Client bundle — dev only. Never in production builds. |
| REST API Key | "REST API Keys" table | Server-to-server: token generation, user CRUD, custom-message-send | Server only. Never in an RN bundle, app.json extra, EXPO_PUBLIC_* var, or git-committed file. |
If the project only has an Auth Key, the user needs to generate a REST API Key in the dashboard: API & Auth Keys → REST API Keys → Add Key. Pick "Full Access" for server-side use.
3. The token auth pattern (4 steps)
1. Client logs into YOUR auth (Firebase Auth / Supabase / Clerk / Auth0 / custom)
↓
2. Client asks YOUR backend for a CometChat auth token
↓ (POST /api/cometchat-token { uid })
3. Backend calls CometChat REST API → gets an Auth Token for that UID
↓ POST https://{APP_ID}.api-{REGION}.cometchat.io/v3/users/{uid}/auth_tokens
with header apiKey: <REST_API_KEY>
↓
4. Client calls CometChatUIKit.login({ authToken: "..." })
The RN client never sees the REST API Key. The server never ships a password or email to the client. The CometChat SDK holds the auth token, not a static key.
4. Server endpoint recipes
RN projects don't have Next.js-style API routes. You need a separate backend. Pick the one the user already has, or the simplest if they're starting fresh.
4a. Express (Node.js backend)
// server/routes/cometchat-token.ts
import { Router } from "express";
import { requireAuth } from "../middleware/auth"; // your existing auth
const router = Router();
const APP_ID = process.env.COMETCHAT_APP_ID!;
const REGION = process.env.COMETCHAT_REGION!;
const REST_API_KEY = process.env.COMETCHAT_REST_API_KEY!;
router.post("/cometchat-token", requireAuth, async (req, res) => {
// Derive UID from authenticated session — NOT from the request body in prod.
const uid = req.user.id;
const r = await fetch(
`https://${APP_ID}.api-${REGION}.cometchat.io/v3/users/${encodeURIComponent(uid)}/auth_tokens`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
appId: APP_ID,
apiKey: REST_API_KEY,
},
body: JSON.stringify({}),
},
);
if (!r.ok) {
const error = await r.text();
console.error("CometChat token error:", error);
return res.status(r.status).json({ error: "Failed to generate auth token" });
}
const data = await r.json();
return res.json({ authToken: data.data.authToken });
});
export default router;
4b. Hono (Cloudflare Workers / Bun / Node)
// server/cometchat-token.ts
import { Hono } from "hono";
const app = new Hono();
app.post("/api/cometchat-token", async (c) => {
const user = c.get("user"); // your middleware-resolved user
if (!user) return c.json({ error: "unauthorized" }, 401);
const APP_ID = c.env.COMETCHAT_APP_ID;
const REGION = c.env.COMETCHAT_REGION;
const REST_API_KEY = c.env.COMETCHAT_REST_API_KEY;
const r = await fetch(
`https://${APP_ID}.api-${REGION}.cometchat.io/v3/users/${encodeURIComponent(user.id)}/auth_tokens`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
appId: APP_ID,
apiKey: REST_API_KEY,
},
body: JSON.stringify({}),
},
);
if (!r.ok) return c.json({ error: "token mint failed" }, 502);
const data = await r.json();
return c.json({ authToken: data.data.authToken });
});
export default app;
4c. Firebase Cloud Functions
// functions/src/cometchat-token.ts
import { onCall, HttpsError } from "firebase-functions/v2/https";
export const getCometChatToken = onCall(
{ secrets: ["COMETCHAT_APP_ID", "COMETCHAT_REGION", "COMETCHAT_REST_API_KEY"] },
async (request) => {
if (!request.auth) throw new HttpsError("unauthenticated", "Sign in required");
const uid = request.auth.uid;
const APP_ID = process.env.COMETCHAT_APP_ID!;
const REGION = process.env.COMETCHAT_REGION!;
const REST_API_KEY = process.env.COMETCHAT_REST_API_KEY!;
const r = await fetch(
`https://${APP_ID}.api-${REGION}.cometchat.io/v3/users/${encodeURIComponent(uid)}/auth_tokens`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
appId: APP_ID,
apiKey: REST_API_KEY,
},
body: JSON.stringify({}),
},
);
if (!r.ok) throw new HttpsError("internal", "token mint failed");
const data = await r.json();
return { authToken: data.data.authToken };
},
);
Client call:
import functions from "@react-native-firebase/functions";
const result = await functions().httpsCallable("getCometChatToken")();
const authToken = result.data.authToken;
4d. Vercel Serverless / Next.js API Route
Even if the RN app isn't Next.js, the user's existing web app often is. Reuse the same backend:
// pages/api/cometchat-token.ts (or app/api/cometchat-token/route.ts)
import type { NextApiRequest, NextApiResponse } from "next";
import { getServerSession } from "next-auth";
import { authOptions } from "./auth/[...nextauth]";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") return res.status(405).end();
const session = await getServerSession(req, res, authOptions);
if (!session?.user) return res.status(401).json({ error: "unauthorized" });
const APP_ID = process.env.COMETCHAT_APP_ID!;
const REGION = process.env.COMETCHAT_REGION!;
const REST_API_KEY = process.env.COMETCHAT_REST_API_KEY!;
const uid = session.user.id;
const r = await fetch(
`https://${APP_ID}.api-${REGION}.cometchat.io/v3/users/${encodeURIComponent(uid)}/auth_tokens`,
{
method: "POST",
headers: { "Content-Type": "application/json", appId: APP_ID, apiKey: REST_API_KEY },
body: JSON.stringify({}),
},
);
if (!r.ok) return res.status(502).json({ error: "token mint failed" });
const data = await r.json();
return res.json({ authToken: data.data.authToken });
}
5. Client-side: CometChatUIKit.login({ authToken })
Once the server is serving tokens, update the RN client to fetch the token and use it. This is a change to the CometChatProvider (see cometchat-native-core § 6) — swap the uid prop for an authToken prop.
5a. Update the provider to support authToken
// CometChatProvider.tsx — production-aware version
import React, { createContext, useContext, useEffect, useState, type ReactNode } from "react";
import { CometChatUIKit } from "@cometchat/chat-uikit-react-native";
let initialized = false;
let loginInFlight: Promise<unknown> | null = null;
async function ensureLoggedIn(authToken?: string, uid?: string): Promise<void> {
const existing = await CometChatUIKit.getLoggedInUser();
if (existing) return;
if (loginInFlight) {
await loginInFlight;
return;
}
// Production — prefer authToken
if (authToken) {
loginInFlight = CometChatUIKit.login({ authToken });
} else if (uid) {
loginInFlight = CometChatUIKit.login({ uid }); // dev fallback
} else {
return; // nothing to log in with yet
}
try {
await loginInFlight;
} finally {
loginInFlight = null;
}
}
interface Props {
appId: string;
region: string;
authKey?: string; // dev only; omit in production
authToken?: string; // production — from your backend
uid?: string; // dev only
children: ReactNode;
}
export function CometChatProvider({ appId, region, authKey, authToken, uid, children }: Props) {
const [isReady, setIsReady] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function setup() {
try {
if (!initialized) {
initialized = true;
await CometChatUIKit.init({
appId,
region,
subscriptionType: "ALL_USERS",
...(authKey ? { authKey } : {}),
});
}
await ensureLoggedIn(authToken, uid);
setIsReady(true);
} catch (e) {
setError(String(e));
}
}
setup();
}, [appId, region, authKey, authToken, uid]);
if (!isReady) return null;
return <>{children}</>;
}
Push registration lands here — right after ensureLoggedIn resolves,
before setIsReady(true). The CometChat SDK scopes push tokens to the
logged-in user, so registering before login associates the token with
"anonymous" and the device won't receive pushes.
import { bootstrapPushAfterLogin } from "../push/bootstrap";
//...
await ensureLoggedIn(authToken, uid);
await bootstrapPushAfterLogin(); // registers FCM/APNs token with CometChat
setIsReady(true);
And unregister BEFORE CometChatUIKit.logout() — the SDK needs the user
context to dissociate the token. See cometchat-native-push § 7 for
the full bootstrapPushAfterLogin / unregisterPushTokenOnLogout helper
pair.
5b. Fetch the token from your backend
Typical app flow:
// App.tsx
import { useState, useEffect } from "react";
import { CometChatProvider } from "./src/providers/CometChatProvider";
import { useMyAppAuth } from "./src/hooks/useMyAppAuth"; // your existing auth
export default function App() {
const { user, isAuthenticated } = useMyAppAuth();
const [cometChatToken, setCometChatToken] = useState<string | null>(null);
useEffect(() => {
if (!isAuthenticated) {
setCometChatToken(null);
return;
}
// Fetch a CometChat auth token from your backend
fetch("https://api.yourapp.com/cometchat-token", {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${user.jwt}` },
})
.then((r) => r.json())
.then((data) => setCometChatToken(data.authToken))
.catch((e) => console.error("CometChat token fetch failed:", e));
}, [isAuthenticated, user?.jwt]);
if (!isAuthenticated) return <LoginScreen />;
if (!cometChatToken) return <LoadingScreen message="Connecting chat..." />;
return (
<CometChatProvider
appId={COMETCHAT_APP_ID}
region={COMETCHAT_REGION}
authToken={cometChatToken}
// no authKey prop in production
>
<AppNavigator />
</CometChatProvider>
);
}
5c. Handle token expiry / refresh
Auth tokens have a configurable TTL (default 24 hours). On token expiry, SDK calls start failing. Handle this by re-minting on 401:
useEffect(() => {
const LISTENER_ID = "TOKEN_EXPIRY_LISTENER";
CometChat.addConnectionListener(
LISTENER_ID,
new CometChat.ConnectionListener({
onDisconnected: async () => {
// Connection dropped. Token might be expired.
// Re-fetch and re-login.
const freshToken = await fetchCometChatToken(user.jwt);
setCometChatToken(freshToken);
await CometChatUIKit.login({ authToken: freshToken });
},
}),
);
return () => CometChat.removeConnectionListener(LISTENER_ID);
}, [user?.jwt]);
A simpler approach for apps that can tolerate a forced re-login: on any 401 from the SDK, log the user out and force them through your app's sign-in flow again.
6. User management CRUD
When someone signs up in your app, you need to create a matching CometChat user. Same for profile updates (name/avatar change) and deletion. These happen on your backend, using the REST API with the REST API Key.
6a. Create a user on signup
async function createCometChatUser(uid: string, name: string, avatarUrl?: string) {
const r = await fetch(
`https://${APP_ID}.api-${REGION}.cometchat.io/v3/users`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
appId: APP_ID,
apiKey: REST_API_KEY,
},
body: JSON.stringify({
uid,
name,
avatar: avatarUrl,
}),
},
);
if (!r.ok) throw new Error(`CometChat user create failed: ${await r.text()}`);
return r.json();
}
6b. Update a user on profile change
async function updateCometChatUser(uid: string, updates: Partial<{ name: string; avatar: string; metadata: any }>) {
const r = await fetch(
`https://${APP_ID}.api-${REGION}.cometchat.io/v3/users/${encodeURIComponent(uid)}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
appId: APP_ID,
apiKey: REST_API_KEY,
},
body: JSON.stringify(updates),
},
);
if (!r.ok) throw new Error(`CometChat user update failed: ${await r.text()}`);
return r.json();
}
6c. Delete a user on account deletion
async function deleteCometChatUser(uid: string) {
const r = await fetch(
`https://${APP_ID}.api-${REGION}.cometchat.io/v3/users/${encodeURIComponent(uid)}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
appId: APP_ID,
apiKey: REST_API_KEY,
},
body: JSON.stringify({ permanent: true }),
},
);
if (!r.ok) throw new Error(`CometChat user delete failed: ${await r.text()}`);
}
6d. Where to wire these calls
The CRUD functions live on your backend; you call them from your existing auth event handlers:
| Auth event | When to call | Function |
|---|---|---|
| User signs up | After your app's user creation succeeds | createCometChatUser(newUser.id, newUser.name, newUser.avatarUrl) |
| User updates name or avatar | After your app's profile update succeeds | updateCometChatUser(user.id, { name, avatar }) |
| User deletes account | Before/after your app's user deletion | deleteCometChatUser(user.id) |
7. Auth-provider integration recipes
Your RN app's auth layer typically comes from one of these SDKs. How to wire CometChat into each:
7a. Firebase Auth
Firebase issues a UID per user. Use that same UID in CometChat.
// Backend (Firebase Cloud Function)
import { onDocumentCreated } from "firebase-functions/v2/firestore";
import { onUserCreated, onUserDeleted } from "firebase-functions/v2/auth";
export const onSignup = onUserCreated(async (event) => {
const { uid, displayName, photoURL } = event.data;
await createCometChatUser(uid, displayName ?? "User", photoURL);
});
export const onAccountDelete = onUserDeleted(async (event) => {
await deleteCometChatUser(event.data.uid);
});
// Profile updates are app-level — hook into your profile-update handler
Client — get the Firebase ID token, send to your cometchat-token endpoint:
import auth from "@react-native-firebase/auth";
const idToken = await auth().currentUser!.getIdToken();
const r = await fetch("/api/cometchat-token", {
method: "POST",
headers: { Authorization: `Bearer ${idToken}` },
});
const { authToken } = await r.json();
7b. Supabase Auth
Supabase also issues a UID. Use it as the CometChat UID.
// Backend — Supabase Edge Function triggered on signup
Deno.serve(async (req) => {
const event = await req.json();
if (event.type === "INSERT" && event.table === "users") {
const { id, email } = event.record;
await createCometChatUser(id, email.split("@")[0]);
}
return new Response("ok");
});
Client:
import { supabase } from "./supabase";
const { data: { session } } = await supabase.auth.getSession();
const r = await fetch("/api/cometchat-token", {
method: "POST",
headers: { Authorization: `Bearer ${session!.access_token}` },
});
7c. Clerk Expo
Clerk is Expo-friendly and has its own webhooks for user lifecycle events.
// Client — React Native
import { useAuth } from "@clerk/clerk-expo";
const { getToken, userId } = useAuth();
const jwt = await getToken();
const r = await fetch("/api/cometchat-token", {
method: "POST",
headers: { Authorization: `Bearer ${jwt}` },
});
Backend — use a Clerk webhook to trigger CRUD on user lifecycle events.
7d. Auth0
import { useAuth0 } from "react-native-auth0";
const { getCredentials } = useAuth0();
const { accessToken } = await getCredentials();
const r = await fetch("/api/cometchat-token", {
method: "POST",
headers: { Authorization: `Bearer ${accessToken}` },
});
Backend — use Auth0 Actions or Rules to trigger CRUD webhooks.
7e. Custom JWT / bespoke auth
If the user's auth is custom (their own JWT), the pattern is the same: client includes Authorization: Bearer <jwt>, server validates + extracts UID + mints CometChat auth token.
8. Environment variables — split between client + server
| Variable | Location | Visibility |
|---|---|---|
COMETCHAT_APP_ID |
Client AND server | OK client-side |
COMETCHAT_REGION |
Client AND server | OK client-side |
COMETCHAT_AUTH_KEY |
Dev client only. Remove from production. | Should NEVER ship in a production RN bundle |
COMETCHAT_REST_API_KEY |
Server only. Your backend's env. | Never ships to client, ever |
COMETCHAT_TOKEN_ENDPOINT |
Client | Your backend URL (e.g. https://api.yourapp.com/cometchat-token) — safe in client bundle |
Production RN client .env (or app.json extra):
COMETCHAT_APP_ID=your_app_id
COMETCHAT_REGION=us
COMETCHAT_TOKEN_ENDPOINT=https://api.yourapp.com/cometchat-token
# No COMETCHAT_AUTH_KEY in production
# No COMETCHAT_REST_API_KEY — server-only
Server .env:
COMETCHAT_APP_ID=your_app_id
COMETCHAT_REGION=us
COMETCHAT_REST_API_KEY=your_rest_api_key
9. Security checklist
Before releasing to production, verify:
-
COMETCHAT_AUTH_KEYremoved from client.env/app.json extra/ anyEXPO_PUBLIC_*var - Production provider uses
authTokenprop, notauthKey -
COMETCHAT_REST_API_KEYlives only on your backend (check withgrep -r REST_API_KEY src/) - Token endpoint is behind auth — unauthenticated users can't mint a token for an arbitrary UID
- UID derivation on the token endpoint comes from the authenticated session, NOT from the request body (otherwise anyone can mint a token for anyone)
- Rate limit on the token endpoint (prevents abuse)
- HTTPS-only — no HTTP in production
- User CRUD endpoints are authenticated (or called from webhooks with signature verification)
- CometChat user deletion happens on account deletion (GDPR / privacy compliance)
10. Rate limits + retry
CometChat's REST API has rate limits per app. For the token endpoint:
- Default: 100 requests/minute per app
- Token generation is cheap — if you're hitting limits, you're likely minting too often (e.g. one mint per RN screen mount). Mint once per sign-in, cache client-side, reuse until expiry.
Retry policy:
- 5xx — retry with exponential backoff (1s, 2s, 4s, give up)
- 4xx — do NOT retry. Surface the error.
11. Anti-patterns
-
NEVER ship the REST API Key in an RN bundle. Not under any env-var name or prefix. Not in
app.json extra. Not inEXPO_PUBLIC_*. Not in a .gitignored file the user commits by accident. If you see yourself writing an env var forREST_API_KEYin a client-side config, stop. -
NEVER let the client specify the UID to mint a token for. The server must derive UID from the authenticated session. A
POST /cometchat-token { uid: "..." }that trusts the body is equivalent to no auth — anyone can impersonate anyone. -
Don't cache the auth token to disk forever. It expires. Either re-mint on every cold start or store with a short TTL and refresh on 401.
-
Don't use
login({ uid })in production.uidmode requires an Auth Key on the UIKit settings. In production you should set neitherauthKeyon the UIKitSettings nor calllogin({ uid })— both are dev-only patterns. -
Don't forget user CRUD. A user who signs up in your app but has no matching CometChat user will get "user does not exist" errors on
login({ authToken }). The token endpoint mints tokens, but the user must already exist in CometChat. -
Don't skip the security checklist. Production bugs in auth are catastrophic.
-
Don't retry 4xx errors. Token-endpoint 400s are config mistakes (wrong REST API Key, malformed UID, etc.). Retrying makes it worse.
12. Verifying production auth works
- Build a production-configuration version of the app (no Auth Key, only AppId + Region + token endpoint).
- Log in as a real user through your app's normal flow.
- In the RN debugger network tab, confirm
POST /cometchat-tokenreturns{ authToken: "..." }. - Confirm
CometChatUIKit.login({ authToken })resolves. - Send a message — verify delivery.
- Force-close the app, reopen — token fetch + login should happen again on cold start.
- (Optional) Wait out the token TTL (default 24hr), verify the 401-refresh path works.
If any step fails, see cometchat-native-troubleshooting § Auth / Token issues.
Skill routing reference
| Skill | When to route |
|---|---|
cometchat-native-core |
Init / login / provider wrapper chain — prerequisite |
cometchat-native-components |
The base component props (nothing production-specific there) |
cometchat-native-placement |
Where your chat UI goes (no change in production) |
cometchat-native-expo-patterns |
Expo-specific env var wiring (expo-constants vs EXPO_PUBLIC_*) |
cometchat-native-bare-patterns |
Bare RN env var wiring (react-native-config) |
cometchat-native-theming |
Theme customization (independent of auth) |
cometchat-native-features |
Feature flags (polls, extensions, etc. — all still work in prod) |
cometchat-native-customization |
If customization depends on server-side data (user tags, metadata) |
cometchat-native-production |
This skill — server tokens + user CRUD |
cometchat-native-troubleshooting |
401 on token fetch, "user does not exist" on login, token-endpoint rate limit |