Clerk integration
Configure Clerk as an authentication provider for CloudSignal MQTT.
Configure Clerk as an authentication provider so your Clerk users can connect to CloudSignal MQTT with credentials derived from their Clerk session token. Clerk supports both development and production environments with different domains.
Prerequisites
- A Clerk application with authentication enabled
- A CloudSignal account with API keys
- Your Clerk Frontend API domains (development and/or production)
Understanding Clerk environments
Clerk uses different domains for development and production:
| Environment | Domain format | Example |
|---|---|---|
| Development | {instance}.clerk.accounts.dev | happy-fox-12.clerk.accounts.dev |
| Production | Custom domain | clerk.your-domain.com |
You can configure both environments simultaneously. CloudSignal will automatically match incoming tokens to the correct environment based on the JWT's issuer claim.
Required: configure custom claims
Important: Clerk's default JWT does not include the user's email address. You must configure custom claims for CloudSignal to identify users.
CloudSignal requires the user's email to create MQTT credentials. Configure custom claims in Clerk:
- Go to your Clerk Dashboard
- Navigate to Sessions → Customize session token
- Add the following claims:
{
"email": "{{user.primary_email_address}}",
"user_id": "{{user.id}}",
"name": "{{user.first_name}} {{user.last_name}}"
}- Click Save
Alternative: use a JWT template
If you prefer to use JWT templates (for API-specific tokens):
- Go to JWT Templates → Create template
- Add the same claims as above
- Use
getToken({ template: 'your-template-name' })when requesting tokens
Configuration
Find your Clerk frontend API domains
Development domain
- Go to your Clerk Dashboard
- Select your application
- Navigate to API Keys
- Find your Publishable Key (starts with
pk_test_) - The domain is encoded in the key. You can also find it in your Clerk Dashboard URL
Alternatively, decode from your publishable key:
// pk_test_ keys contain the Frontend API URL in base64
const pk = 'pk_test_aGFwcHktZm94LTEyLmNsZXJrLmFjY291bnRzLmRldiQ=';
const domain = atob(pk.replace('pk_test_', '').replace('$', ''));
// Result: happy-fox-12.clerk.accounts.devProduction domain
For production, you'll have either:
- A Clerk-provided domain from your
pk_live_key - A custom domain you've configured (for example,
clerk.your-domain.com)
Add Clerk provider in CloudSignal
- Go to CloudSignal Dashboard
- Navigate to Connections → Auth Providers
- Click on Clerk
- Enter your configuration:
| Field | Value |
|---|---|
| Development Frontend API | Your dev domain (for example, happy-fox-12.clerk.accounts.dev) |
| Production Frontend API | Your production domain (for example, clerk.your-domain.com) |
At least one domain is required. Configure both if you want to support tokens from both environments.
- Click Test Connection to verify both environments
- Click Save Changes
- Enable the provider using the toggle
Configure CORS (for browser apps)
If exchanging tokens from browser-based applications:
- Go to Settings → Security
- Add your application's domain to Allowed Origins
- Save changes
Test the integration
Use the CloudSignal API to test token exchange:
curl -X POST https://api.cloudsignal.io/v2/tokens/exchange \
-H "Content-Type: application/json" \
-d '{
"organization_id": "YOUR_ORG_ID",
"token": "YOUR_CLERK_SESSION_TOKEN"
}'Implementation
React/Next.js example
import { useAuth } from '@clerk/nextjs';
import mqtt from 'mqtt';
// Exchange Clerk token for MQTT credentials
async function getMqttCredentials(sessionToken: string) {
const response = await fetch('/api/mqtt-credentials', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ clerkToken: sessionToken })
});
return response.json();
}
// React component
function MqttConnection() {
const { getToken } = useAuth();
async function connect() {
// Get Clerk session token
const token = await getToken();
if (!token) {
throw new Error('User not authenticated');
}
// Exchange for MQTT credentials
const credentials = await getMqttCredentials(token);
// Connect to MQTT
const client = mqtt.connect('wss://connect.cloudsignal.app:18885/', {
username: credentials.mqtt_username,
password: credentials.mqtt_password
});
client.on('connect', () => {
console.log('Connected to MQTT');
});
return client;
}
return <button onClick={connect}>Connect to MQTT</button>;
}API route (Next.js)
// app/api/mqtt-credentials/route.ts
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
const { clerkToken } = await request.json();
const response = await fetch('https://api.cloudsignal.io/v2/tokens/exchange', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
organization_id: process.env.CLOUDSIGNAL_ORG_ID,
token: clerkToken
})
});
const credentials = await response.json();
return NextResponse.json(credentials);
}Direct browser exchange (with CORS)
If you've configured CORS, you can exchange tokens directly from the browser:
import { useAuth } from '@clerk/nextjs';
async function exchangeTokenDirectly() {
const { getToken } = useAuth();
const token = await getToken();
const response = await fetch('https://api.cloudsignal.io/v2/tokens/exchange', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
organization_id: 'YOUR_ORG_ID',
token: token
})
});
return response.json();
}Handling token refresh
Clerk session tokens expire. Handle reconnection with fresh credentials:
import { useAuth } from '@clerk/nextjs';
import mqtt from 'mqtt';
let mqttClient: mqtt.MqttClient | null = null;
function useMqttConnection() {
const { getToken } = useAuth();
async function reconnect() {
const token = await getToken();
if (!token) return;
const credentials = await getMqttCredentials(token);
if (mqttClient) {
mqttClient.end();
}
mqttClient = mqtt.connect('wss://connect.cloudsignal.app:18885/', {
username: credentials.mqtt_username,
password: credentials.mqtt_password
});
}
// Reconnect when MQTT connection is lost
useEffect(() => {
if (mqttClient) {
mqttClient.on('close', () => {
// Wait a moment then reconnect with fresh credentials
setTimeout(reconnect, 1000);
});
}
}, [mqttClient]);
return { reconnect, client: mqttClient };
}ACL rules for Clerk users
Create ACL rules that match Clerk user identifiers:
# User can only access their own topics
User Pattern: %
Topic: users/%u/#
Permission: pubsubThe MQTT username derived from Clerk will be the user's email (sanitized).
Troubleshooting
"Token issuer does not match" error
This means the JWT's iss claim doesn't match your configured domains.
| Check | What to verify |
|---|---|
| Token environment | Development tokens have issuer like https://happy-fox-12.clerk.accounts.dev |
| Domain configuration | Ensure you've configured the correct domain for that environment |
| Typos | Domain must match exactly (no trailing slashes) |
"Invalid token" error
| Check | What to verify |
|---|---|
| Token freshness | Clerk session tokens expire |
| Token source | Use getToken() from Clerk, not the publishable key |
| JWKS endpoint | Run "Test Connection" in the dashboard |
Test connection shows partial success
This means one environment connected but the other failed:
- Check that both domains are correctly configured
- Verify the failing domain is accessible
- Ensure your Clerk application has the correct environment set up
CORS errors in browser
| Check | What to verify |
|---|---|
| Allowed origins | Go to Settings → Security → Allowed Origins |
| Exact origin | Include protocol (for example, https://your-app.com) |
| Localhost development | Add http://localhost:3000 for local testing |
Next steps
- Supabase integration - Same pattern with JWT secret
- Firebase integration - Same pattern with project ID
- Token Exchange API reference - Full request and response details
- ACL rules guide - Scope topics to the authenticated user