cometchat-production
SkillSecurityProduction readiness for CometChat — server-side token auth, user management CRUD, environment hardening, and security checklist. Replaces dev-mode authKey with server-side tokens.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the cometchat-production skill
What this skill tells your AI
The instructions your AI receives, as published by cometchat/cometchat-skills in skills/cometchat-production/SKILL.md and read by ahel’s review.
Ground truth:
docs/fundamentals/user-auth+ per-platform UI Kit. Official docs: https://www.cometchat.com/docs/fundamentals/user-auth · Docs MCP:claude mcp add --transport http cometchat-docs https://www.cometchat.com/docs/mcp(or fetch the URL directly without MCP). Verify symbols against the installed package/source before relying on them.
Purpose
This skill teaches Claude how to harden a CometChat integration for production. It covers two critical areas:
- Token-based authentication — replacing client-side
authKeywith server-side token generation - User management — server-side CRUD for CometChat users (create on signup, update on profile change, delete on account deletion)
The cometchat-core skill's provider pattern supports both dev mode (login(uid)) and production mode (loginWithAuthToken(token)). This skill provides the server-side half: the token endpoint and user management endpoints.
1. Why production auth matters
In development mode, CometChatUIKit.login(uid) uses the authKey configured via UIKitSettingsBuilder.setAuthKey(). This key is embedded in your client-side JavaScript bundle. Anyone can open browser DevTools, find the auth key, and use it to log in as ANY user in your CometChat app. They can read private messages, send messages as other users, and access every conversation.
Production deployments MUST use server-side token generation. The auth key stays on your server. Clients receive short-lived tokens scoped to a single user. If a token leaks, the blast radius is one user session, not your entire app.
2. The token auth pattern
The production auth flow has four steps:
-
Client authenticates with YOUR auth system. The user logs into your app using your existing login flow (email/password, OAuth, magic link, etc.). This step has nothing to do with CometChat.
-
Your server calls the CometChat REST API. After verifying the user's identity, your server makes a POST request to CometChat's token endpoint using the REST API key (a server-only secret). CometChat returns an auth token for that specific user.
-
Client receives the token. Your server sends the auth token back to the client in the API response.
-
Client calls
CometChatUIKit.loginWithAuthToken(token). The CometChat SDK uses the token to establish a session. The auth key NEVER touches the browser.
┌─────────┐ 1. Login ┌──────────┐ 2. POST /v3/users/{uid}/auth_tokens ┌──────────────┐
│ Client │ ───────────────→ │ Your │ ──────────────────────────────────────→ │ CometChat │
│ (Browser)│ │ Server │ ←────────────────────────────────────── │ REST API │
│ │ ←─────────────── │ │ { authToken: "..." } │ │
│ │ 3. auth token │ │ │ │
│ │ └──────────┘ └──────────────┘
│ │
│ 4. CometChatUIKit.loginWithAuthToken(token)
└─────────┘
3. Server endpoint implementations
Each endpoint does the same thing:
- Receives a user UID (from the authenticated session, NOT from the request body in production)
- Validates that the caller is authenticated
- POSTs to
https://{APP_ID}.api-{REGION}.cometchat.io/v3/users/{uid}/auth_tokens - Returns the auth token to the client
The CometChat REST API requires two headers:
appId— your CometChat app IDapiKey— a REST API Key (NOT the Auth Key used in dev mode)
Auth Key vs REST API Key — these are different keys:
| Key type | Where to find | Purpose | Security |
|---|---|---|---|
Auth Key (authOnly scope) | Dashboard → Your App → API & Auth Keys → "Auth Keys" table | Client-side SDK CometChatUIKit.login(uid) in dev mode; server-side it can create users + mint auth tokens (POST /v3/users, POST /v3/users/{uid}/auth_tokens) but NOT update/delete users | Exposed in browser. Dev only. |
REST API Key (fullAccess scope) | Dashboard → Your App → API & Auth Keys → "Rest API Keys" table | Server-to-server: token generation, full user CRUD (incl. update/delete), message send | Server only. Never expose to client. |
Scope split (verified against
fundamentals/key-concepts.mdx+ the chat-apisapikeyscope enumfullAccess/authOnly): the Auth Key can create & login users and mint tokens, butPUT/DELETE /v3/users/{uid}require afullAccessREST API Key — anauthOnlyAuth Key is rejected. So a user-management endpoint that does update/delete MUST useCOMETCHAT_REST_API_KEY, not the Auth Key. (Heads-up: the CLI'sadd-user-mgmt/production-authscaffolds currently name the server varCOMETCHAT_AUTH_KEY; for full CRUD, populate it with — or rename it to — afullAccessREST API Key. Tracked as a CLI-alignment follow-up.)
The .env should have both for production:
# Client-side (prefixed for the framework)
VITE_COMETCHAT_APP_ID=your_app_id
VITE_COMETCHAT_REGION=us
# Server-side (no prefix — never exposed to the client)
COMETCHAT_APP_ID=your_app_id
COMETCHAT_REGION=us
COMETCHAT_REST_API_KEY=your_rest_api_key
If the user only has an Auth Key, tell them to create a REST API Key in the dashboard: API & Auth Keys → Rest API Keys → Add Key.
Next.js App Router
app/api/cometchat-token/route.ts
import { NextRequest, NextResponse } from "next/server";
const APP_ID = process.env.COMETCHAT_APP_ID!;
const REGION = process.env.COMETCHAT_REGION!;
const REST_API_KEY = process.env.COMETCHAT_REST_API_KEY!;
export async function POST(request: NextRequest) {
// TODO: Replace this with your real auth check.
// Example with NextAuth: const session = await getServerSession(authOptions);
// Example with Clerk: const { userId } = auth();
// If not authenticated, return 401.
const body = await request.json();
const uid = body.uid as string;
if (!uid || typeof uid !== "string") {
return NextResponse.json({ error: "Missing uid" }, { status: 400 });
}
// In production, derive UID from the authenticated session, not from
// the request body. The body approach is shown here as a starting point.
// Example: const uid = session.user.id;
const response = 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 (!response.ok) {
const error = await response.text();
console.error("CometChat token error:", error);
return NextResponse.json(
{ error: "Failed to generate auth token" },
{ status: response.status }
);
}
const data = await response.json();
return NextResponse.json({ authToken: data.data.authToken });
}
Next.js Pages Router
pages/api/cometchat-token.ts
import type { NextApiRequest, NextApiResponse } from "next";
const APP_ID = process.env.COMETCHAT_APP_ID!;
const REGION = process.env.COMETCHAT_REGION!;
const REST_API_KEY = process.env.COMETCHAT_REST_API_KEY!;
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== "POST") {
return res.status(405).json({ error: "Method not allowed" });
}
// TODO: Replace with your auth check (e.g., getServerSession, Clerk, JWT).
const { uid } = req.body;
if (!uid || typeof uid !== "string") {
return res.status(400).json({ error: "Missing uid" });
}
const response = 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 (!response.ok) {
const error = await response.text();
console.error("CometChat token error:", error);
return res.status(response.status).json({ error: "Failed to generate auth token" });
}
const data = await response.json();
return res.status(200).json({ authToken: data.data.authToken });
}
React Router v7 (framework mode)
In React Router framework mode, server logic lives in action functions within route modules. Create a resource route (no UI) for the token endpoint.
app/routes/api.cometchat-token.ts
import type { ActionFunctionArgs } from "react-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!;
export async function action({ request }: ActionFunctionArgs) {
if (request.method !== "POST") {
return new Response("Method not allowed", { status: 405 });
}
// TODO: Replace with your auth check (e.g., session cookie, JWT).
const body = await request.json();
const uid = body.uid as string;
if (!uid || typeof uid !== "string") {
return Response.json({ error: "Missing uid" }, { status: 400 });
}
const response = 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 (!response.ok) {
const error = await response.text();
console.error("CometChat token error:", error);
return Response.json(
{ error: "Failed to generate auth token" },
{ status: response.status }
);
}
const data = await response.json();
return Response.json({ authToken: data.data.authToken });
}
Register this route in your routes.ts (or app/routes.ts):
// Add to your route config:
route("api/cometchat-token", "routes/api.cometchat-token.ts"),
Express / Hono standalone (React + Vite projects)
React/Vite projects have no built-in server. You need a separate backend. Here are patterns for the two most common choices.
Express:
// server/index.ts (or server.js)
import express from "express";
import cors from "cors";
const app = express();
app.use(cors({ origin: "http://localhost:5173" })); // Your Vite dev server
app.use(express.json());
const APP_ID = process.env.COMETCHAT_APP_ID!;
const REGION = process.env.COMETCHAT_REGION!;
const REST_API_KEY = process.env.COMETCHAT_REST_API_KEY!;
app.post("/api/cometchat-token", async (req, res) => {
// TODO: Replace with your auth check.
const { uid } = req.body;
if (!uid || typeof uid !== "string") {
return res.status(400).json({ error: "Missing uid" });
}
const response = 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 (!response.ok) {
const error = await response.text();
console.error("CometChat token error:", error);
return res.status(response.status).json({ error: "Failed to generate auth token" });
}
const data = await response.json();
return res.json({ authToken: data.data.authToken });
});
app.listen(3001, () => console.log("Server running on :3001"));
Hono:
// server/index.ts
import { Hono } from "hono";
import { cors } from "hono/cors";
import { serve } from "@hono/node-server";
const app = new Hono();
app.use("/*", cors({ origin: "http://localhost:5173" }));
const APP_ID = process.env.COMETCHAT_APP_ID!;
const REGION = process.env.COMETCHAT_REGION!;
const REST_API_KEY = process.env.COMETCHAT_REST_API_KEY!;
app.post("/api/cometchat-token", async (c) => {
// TODO: Replace with your auth check.
const { uid } = await c.req.json();
if (!uid || typeof uid !== "string") {
return c.json({ error: "Missing uid" }, 400);
}
const response = 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 (!response.ok) {
const error = await response.text();
console.error("CometChat token error:", error);
return c.json({ error: "Failed to generate auth token" }, { status: response.status });
}
const data = await response.json();
return c.json({ authToken: data.data.authToken });
});
serve({ fetch: app.fetch, port: 3001 });
Astro
src/pages/api/cometchat-token.ts
Astro SSR endpoints work in hybrid or server mode. Make sure your astro.config.mjs has output: "server" or output: "hybrid".
import type { APIRoute } from "astro";
const APP_ID = import.meta.env.COMETCHAT_APP_ID;
const REGION = import.meta.env.COMETCHAT_REGION;
const REST_API_KEY = import.meta.env.COMETCHAT_REST_API_KEY;
export const POST: APIRoute = async ({ request }) => {
// TODO: Replace with your auth check (e.g., session cookie, Astro middleware).
const body = await request.json();
const uid = body.uid as string;
if (!uid || typeof uid !== "string") {
return new Response(JSON.stringify({ error: "Missing uid" }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}
const response = 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 (!response.ok) {
const error = await response.text();
console.error("CometChat token error:", error);
return new Response(
JSON.stringify({ error: "Failed to generate auth token" }),
{ status: response.status, headers: { "Content-Type": "application/json" } }
);
}
const data = await response.json();
return new Response(JSON.stringify({ authToken: data.data.authToken }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
};
4. Client-side changes
The cometchat-core skill's CometChatProvider already supports both authKey (dev) and authToken (production) props. To switch to production mode:
Step 1 — Create a hook to fetch the token
// hooks/useCometChatToken.ts
"use client"; // Required for Next.js App Router; harmless elsewhere
import { useState, useEffect } from "react";
/**
* Fetches a CometChat auth token from your server-side endpoint.
* Call this after the user is authenticated in your app.
*/
export function useCometChatToken(uid: string | null) {
const [token, setToken] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!uid) return;
let cancelled = false;
setLoading(true);
fetch("/api/cometchat-token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ uid }),
})
.then((res) => {
if (!res.ok) throw new Error(`Token request failed: ${res.status}`);
return res.json();
})
.then((data) => {
if (!cancelled) {
setToken(data.authToken);
setLoading(false);
}
})
.catch((err) => {
if (!cancelled) {
setError(String(err));
setLoading(false);
}
});
return () => {
cancelled = true;
};
}, [uid]);
return { token, error, loading };
}
Step 2 — Update the CometChatProvider usage
Before (dev mode):
<CometChatProvider
appId={import.meta.env.VITE_COMETCHAT_APP_ID}
region={import.meta.env.VITE_COMETCHAT_REGION}
authKey={import.meta.env.VITE_COMETCHAT_AUTH_KEY}
uid="cometchat-uid-1"
>
<ChatPage />
</CometChatProvider>
After (production mode):
function ChatWrapper() {
// Get the authenticated user's ID from your auth system
const { user } = useAuth(); // Your auth hook (NextAuth, Clerk, Supabase, etc.)
const { token, error, loading } = useCometChatToken(user?.id ?? null);
if (!user) return <LoginPage />;
if (loading) return <div>Connecting to chat...</div>;
if (error) return <div>Chat connection failed: {error}</div>;
return (
<CometChatProvider
appId={import.meta.env.VITE_COMETCHAT_APP_ID}
region={import.meta.env.VITE_COMETCHAT_REGION}
authToken={token!}
uid={user.id}
>
<ChatPage />
</CometChatProvider>
);
}
Key changes:
- Removed
authKeyprop entirely - Added
authTokenprop with the token from your server uidcomes from your auth system, not a hardcoded test user- The provider only renders after the token is fetched
Step 3 — Handle token refresh on 401
CometChat auth tokens expire. When a token expires, SDK calls will fail. Handle this in your provider:
// In your CometChatProvider or a wrapper:
import { CometChat } from "@cometchat/chat-sdk-javascript";
// Listen for auth errors
CometChat.addConnectionListener(
"auth-refresh-listener",
new CometChat.ConnectionListener({
onDisconnected: () => {
console.log("CometChat disconnected — token may have expired");
// Re-fetch token from your endpoint and call loginWithAuthToken again
},
})
);
A simpler approach: if any CometChat operation returns a 401 or auth error, re-fetch the token and call CometChatUIKit.loginWithAuthToken(newToken).
Guard refresh calls with the same concurrency pattern. If two components both see a 401 at the same time, two loginWithAuthToken calls race and the SDK throws "Please wait until the previous login request ends." Route token refresh through the same ensureLoggedIn(uid, authToken) helper defined in cometchat-core's provider pattern — the module-level loginInFlight promise dedupes concurrent refreshes automatically.
Concrete refresh handler — pair the helper from cometchat-core with the connection listener and a token-refetch path:
import { CometChat } from "@cometchat/chat-sdk-javascript";
import { CometChatUIKit } from "@cometchat/chat-uikit-react";
// Module-level — shared across every refresh attempt.
// Identical shape to the one in cometchat-core § 2.
let refreshInFlight: Promise<unknown> | null = null;
async function refreshSession(uid: string): Promise<void> {
if (refreshInFlight) {
// Another component already triggered a refresh — wait for it,
// don't fire a second loginWithAuthToken.
await refreshInFlight;
return;
}
refreshInFlight = (async () => {
// 1. Fetch a fresh token from your server endpoint.
// Your existing useCometChatToken hook (Step 1) calls /api/cometchat-token.
// Extract that fetch into a helper so the refresh path can reuse it.
const res = await fetch("/api/cometchat-token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ uid }),
});
if (!res.ok) throw new Error(`Token refresh failed: ${res.status}`);
const { token } = (await res.json()) as { token: string };
// 2. Re-authenticate with the new token. SDK swaps the in-memory
// session — no full reload needed.
await CometChatUIKit.loginWithAuthToken(token);
})();
try {
await refreshInFlight;
} finally {
refreshInFlight = null;
}
}
// Wire it into the connection listener so a disconnect triggers refresh.
CometChat.addConnectionListener(
"auth-refresh-listener",
new CometChat.ConnectionListener({
onDisconnected: async () => {
// Web kit method is getLoggedinUser() (lowercase "i") and is ASYNC —
// it returns a Promise, so you must await it (you can't chain ?.getUid()
// on the call directly). The synchronous capital-I form is the Angular kit.
const me = await CometChatUIKit.getLoggedinUser();
const uid = me?.getUid();
if (uid) {
refreshSession(uid).catch((e) => {
console.error("CometChat refresh failed; user may need to re-login", e);
});
}
},
}),
);
refreshInFlight mirrors loginInFlight from cometchat-core § 2 — same dedup pattern, separate promise so the initial-login and refresh paths don't accidentally serialize against each other. If the same component renders in StrictMode AND a 401 arrives during the second mount, the listener and the provider effect can both touch the SDK without colliding.
5. User management patterns
In production, you need to keep CometChat users in sync with your app's users. CometChat users are managed via the REST API using the REST API key (server-only).
Create user — on signup
When a user signs up for your app, create a corresponding CometChat user.
// Server-side utility function
async function createCometChatUser(uid: string, name: string, avatar?: string) {
const response = 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 ? { avatar } : {}),
}),
}
);
if (!response.ok) {
const error = await response.json();
// If user already exists (409), that's OK — just log it
if (response.status === 409) {
console.log(`CometChat user ${uid} already exists`);
return;
}
throw new Error(`Failed to create CometChat user: ${JSON.stringify(error)}`);
}
}
Update user — on profile change
When a user updates their name or avatar in your app, update the CometChat user.
async function updateCometChatUser(
uid: string,
updates: { name?: string; avatar?: string; metadata?: Record<string, unknown> }
) {
const response = 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 (!response.ok) {
const error = await response.json();
throw new Error(`Failed to update CometChat user: ${JSON.stringify(error)}`);
}
}
Delete user — on account deletion
When a user deletes their account, delete the CometChat user.
async function deleteCometChatUser(uid: string) {
const response = 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,
},
}
);
if (!response.ok) {
const error = await response.json();
throw new Error(`Failed to delete CometChat user: ${JSON.stringify(error)}`);
}
}
Where to hook user management into common auth providers
NextAuth (next-auth):
// app/api/auth/[...nextauth]/route.ts or pages/api/auth/[...nextauth].ts
import NextAuth from "next-auth";
export default NextAuth({
// ... your providers ...
events: {
createUser: async ({ user }) => {
await createCometChatUser(user.id, user.name ?? user.email ?? "User");
},
// Note: NextAuth doesn't have a deleteUser event by default.
// Handle deletion in your account deletion endpoint.
},
});
Clerk:
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 105
- Forks
- 2
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
cometchat-production- Source
- github.com/cometchat/cometchat-skills