.cursor/skills/workos-convex-auth/SKILL.md
Set up and configure WorkOS AuthKit authentication with Convex backend. Use when integrating AuthKit, configuring JWT providers, setting up environment variables, or implementing sign in and sign out flows with React and Vite. Supports Netlify deployment.
npx skillsauth add get-convex/components-submissions-directory workos-convex-authInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Always check official docs for the latest information:
- Convex + WorkOS: https://docs.convex.dev/auth/authkit/
- WorkOS AuthKit: https://workos.com/docs/authkit
- AuthKit React SDK: https://workos.com/docs/sdks/authkit-react
- Auto provisioning: https://docs.convex.dev/auth/authkit/auto-provision
- Netlify Docs: https://docs.netlify.com/
Use this skill when you need to:
The authentication flow works as follows:
/callback with auth codectx.auth.getUserIdentity() returns user claims from JWTnpm install @workos-inc/authkit-react @convex-dev/workos
WorkOS issues JWTs from two different issuers. Configure both:
const clientId = process.env.WORKOS_CLIENT_ID;
export default {
providers: [
{
type: "customJwt",
issuer: "https://api.workos.com/",
algorithm: "RS256",
applicationID: clientId,
jwks: `https://api.workos.com/sso/jwks/${clientId}`,
},
{
type: "customJwt",
issuer: `https://api.workos.com/user_management/${clientId}`,
algorithm: "RS256",
jwks: `https://api.workos.com/sso/jwks/${clientId}`,
},
],
};
Key points:
applicationID is only needed for the SSO providerimport { AuthKitProvider, useAuth } from "@workos-inc/authkit-react";
import { ConvexProviderWithAuthKit } from "@convex-dev/workos";
import { ConvexReactClient } from "convex/react";
const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL);
const redirectUri = import.meta.env.VITE_WORKOS_REDIRECT_URI;
createRoot(document.getElementById("root")!).render(
<AuthKitProvider
clientId={import.meta.env.VITE_WORKOS_CLIENT_ID}
redirectUri={redirectUri}
>
<ConvexProviderWithAuthKit client={convex} useAuth={useAuth}>
<App />
</ConvexProviderWithAuthKit>
</AuthKitProvider>
);
VITE_CONVEX_URL=https://your-deployment.convex.cloud
VITE_WORKOS_CLIENT_ID=client_01XXXXXXXXXXXXXXXXXX
VITE_WORKOS_REDIRECT_URI=http://localhost:5173/callback
Set in Environment Variables section:
WORKOS_CLIENT_ID=client_01XXXXXXXXXXXXXXXXXX
Redirect URIs: Add callback URLs
http://localhost:5173/callback (development)https://yourdomain.netlify.app/components/callback (Netlify production)https://yourdomain.com/callback (custom domain production)CORS Origins: Add allowed origins
http://localhost:5173 (development)https://yourdomain.netlify.app (Netlify production)https://yourdomain.com (custom domain production)JWT Template: Configure email claim (see JWT claims section)
When deploying to Netlify, configure environment variables in Netlify Dashboard:
VITE_CONVEX_URL: Your Convex deployment URL (e.g., https://your-deployment.convex.cloud)VITE_WORKOS_CLIENT_ID: Your WorkOS Client IDVITE_WORKOS_REDIRECT_URI: https://yourdomain.netlify.app/components/callbackCreate a netlify.toml in your project root:
[build]
command = "npm run build"
publish = "dist"
[build.environment]
NODE_VERSION = "20"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
The SPA redirect rule ensures all routes serve index.html for client-side routing.
Use signIn() directly from the AuthKit hook. Do not use getSignInUrl() which is deprecated.
import { useAuth } from "@workos-inc/authkit-react";
function SignInButton() {
const { signIn } = useAuth();
return (
<button
onClick={() => {
localStorage.setItem("authReturnPath", window.location.pathname);
signIn();
}}
>
Sign In
</button>
);
}
import { useAuth } from "@workos-inc/authkit-react";
function SignOutButton() {
const { signOut } = useAuth();
return (
<button onClick={() => signOut()}>
Sign Out
</button>
);
}
Handle the callback with proper loading state checks:
function AuthCallback() {
const { isLoading, user, signIn } = useAuth();
const [authFailed, setAuthFailed] = useState(false);
const hasAuthCode = useMemo(
() => new URLSearchParams(window.location.search).has("code"),
[]
);
const returnPath = useMemo(() => {
const storedPath = localStorage.getItem("authReturnPath");
if (storedPath) {
localStorage.removeItem("authReturnPath");
return storedPath;
}
return "/";
}, []);
useEffect(() => {
if (isLoading) return;
if (user) {
window.location.replace(returnPath);
return;
}
if (hasAuthCode) {
setAuthFailed(true);
return;
}
window.location.replace(returnPath);
}, [hasAuthCode, isLoading, returnPath, user]);
return (
<div>
{authFailed ? (
<button onClick={() => signIn()}>Try Again</button>
) : (
<div>Finishing sign in...</div>
)}
</div>
);
}
Use useConvexAuth() for auth state (not useAuth()) to ensure the Convex backend has validated the token:
import { useConvexAuth, Authenticated, Unauthenticated } from "convex/react";
function MyComponent() {
const { isLoading, isAuthenticated } = useConvexAuth();
if (isLoading) return <div>Loading...</div>;
return (
<>
<Authenticated>
<AuthenticatedContent />
</Authenticated>
<Unauthenticated>
<SignInPrompt />
</Unauthenticated>
</>
);
}
import { query, QueryCtx } from "./_generated/server";
import { v } from "convex/values";
export const loggedInUser = query({
args: {},
returns: v.union(
v.object({
email: v.optional(v.string()),
name: v.optional(v.string()),
pictureUrl: v.optional(v.string()),
}),
v.null()
),
handler: async (ctx) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
return null;
}
return {
email: identity.email,
name: identity.name,
pictureUrl: identity.pictureUrl,
};
},
});
Check admin status based on email domain:
import { query, QueryCtx, MutationCtx, ActionCtx } from "./_generated/server";
import { v } from "convex/values";
type AuthContext = QueryCtx | MutationCtx | ActionCtx;
export async function requireAdminIdentity(ctx: AuthContext) {
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
throw new Error("Authentication required");
}
const email = identity.email;
if (!email?.endsWith("@yourdomain.com")) {
throw new Error("Admin access required");
}
return identity;
}
export async function getAdminIdentity(ctx: AuthContext) {
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
return null;
}
const email = identity.email;
if (!email?.endsWith("@yourdomain.com")) {
return null;
}
return identity;
}
export const isAdmin = query({
args: {},
returns: v.boolean(),
handler: async (ctx) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
return false;
}
const email = identity.email;
if (!email) {
return false;
}
return email.endsWith("@yourdomain.com");
},
});
WorkOS JWT templates do not include email by default. Configure in WorkOS Dashboard:
{
"email": "{{user.email}}",
"name": "{{user.first_name}} {{user.last_name}}",
"picture": "{{user.profile_picture_url}}"
}
After configuration, ctx.auth.getUserIdentity() returns:
{
tokenIdentifier: "https://api.workos.com/user_management/client_xxx|user_yyy",
subject: "user_yyy",
issuer: "https://api.workos.com/user_management/client_xxx",
email: "[email protected]",
name: "User Name",
pictureUrl: "https://..."
}
Use environment variables to switch between environments:
// convex/auth.config.ts
const clientId = process.env.WORKOS_CLIENT_ID;
Set different values in:
.env.local for local development.env.production for Netlify builds (or Netlify Dashboard environment variables)sk_test_...sk_live_...Both environments use: client_01XXXXXXXXXXXXXXXXXX
| Environment | Redirect URI | CORS Origin |
|-------------|--------------|-------------|
| Local Dev | http://localhost:5173/callback | http://localhost:5173 |
| Netlify | https://yourdomain.netlify.app/components/callback | https://yourdomain.netlify.app |
| Custom Domain | https://yourdomain.com/callback | https://yourdomain.com |
Note: When hosting on Netlify with a /components base path, ensure your callback URL includes the full path.
@workos-inc/authkit-react and @convex-dev/workosWORKOS_CLIENT_ID in Convex dashboard environment variablesVITE_WORKOS_CLIENT_ID in .env.localVITE_WORKOS_REDIRECT_URI in .env.localconvex/auth.config.tsnpx convex dev to sync auth configsignIn() directly, not getSignInUrl()netlify.toml with build command and SPA redirectdevelopment
Debug and troubleshoot WorkOS AuthKit authentication issues with Convex. Use when authentication fails, JWT validation errors occur, user identity returns null, email claims are missing, admin access checks fail, or sign in button does not work. Supports Netlify deployment.
documentation
# Update project docs Use this skill after completing any feature, fix, or migration to keep the three core project tracking files in sync. Activate with: `@update-project-docs` ## Step 1: Get real dates Run this first: ```bash git log --date=short -n 10 ``` Use actual commit dates. Never use placeholder dates or future months. ## Step 2: Update TASK.md Move completed items into `## Completed` with date and time: ```markdown - [x] Feature name (YYYY-MM-DD HH:mm UTC) - [x] Sub-task det
tools
# Create a PRD Use this skill before any multi-file feature, architectural decision, or complex bug fix. Activate with: `@create-prd` ## Location and naming - All PRDs live in `prds/` folder - File name: `prds/<feature-or-problem-slug>.md` - Extension is always `.md`, not `.prd` - Use kebab-case for the filename (e.g., `prds/adding-email-auth.md`) ## Template Copy and fill in this template: ```markdown # [Feature or problem name] Created: YYYY-MM-DD HH:mm UTC Last Updated: YYYY-MM-DD HH:
development
Patterns for scaling read-heavy Convex apps to millions of users. Use when optimizing bandwidth, reducing query costs, fixing slow queries, creating digest tables, replacing reactive subscriptions with one-shot fetches, adding compound indexes, debouncing writes, rate-controlling backfills, or running npx convex insights. Trigger when users mention "scale", "bandwidth", "performance", "optimize", "slow queries", "expensive queries", "digest table", "denormalize", or "thundering herd" in the context of Convex.