skills/cometchat/cometchat-skills/cometchat-native-production

cometchat-native-production

Installation
SKILL.md

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:

  1. Why the dev authKey can't ship to production
  2. Auth Key vs REST API Key — which lives where
  3. Server endpoint recipes (Express / Hono / Firebase Functions / Vercel)
  4. Client-side: CometChatUIKit.login({ authToken }) + token refresh
  5. User CRUD endpoints + auth-provider integration (Firebase Auth / Supabase / Clerk / Auth0)
  6. 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_KEY removed from client .env / app.json extra / any EXPO_PUBLIC_* var
  • Production provider uses authToken prop, not authKey
  • COMETCHAT_REST_API_KEY lives only on your backend (check with grep -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

  1. NEVER ship the REST API Key in an RN bundle. Not under any env-var name or prefix. Not in app.json extra. Not in EXPO_PUBLIC_*. Not in a .gitignored file the user commits by accident. If you see yourself writing an env var for REST_API_KEY in a client-side config, stop.

  2. 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.

  3. 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.

  4. Don't use login({ uid }) in production. uid mode requires an Auth Key on the UIKit settings. In production you should set neither authKey on the UIKitSettings nor call login({ uid }) — both are dev-only patterns.

  5. 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.

  6. Don't skip the security checklist. Production bugs in auth are catastrophic.

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

  1. Build a production-configuration version of the app (no Auth Key, only AppId + Region + token endpoint).
  2. Log in as a real user through your app's normal flow.
  3. In the RN debugger network tab, confirm POST /cometchat-token returns { authToken: "..." }.
  4. Confirm CometChatUIKit.login({ authToken }) resolves.
  5. Send a message — verify delivery.
  6. Force-close the app, reopen — token fetch + login should happen again on cold start.
  7. (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
Weekly Installs
1
GitHub Stars
7
First Seen
4 days ago