John
John2y ago

WorkOS Authkit as Custom Auth Integration with Convex

Currently having some trouble figuring out how to use WorkOS Authkit as Custom Auth with Convex. - there doesn't seem to be an equivalent of an AuthProviderXReactProvider - not sure how to update the useAuthFromProviderX reference to use with ConvexProviderWithAuth so that ctx.auth.getUserIdentity() works correctly - not sure what to provide as the domain and applicationID in the auth.config.js file I've tried referencing this next-authkit-example, the convex clerk example, and the nextauth example by Web Dev Cody, but still not sure how to get it working with convex. The WorkOS docs mention that:
In order to persist the authenticated state of the user in the application, we need to store and access a session. WorkOS User Management does not currently offer a session management feature, this must instead be handled by the application. For illustration purposes we’ll be using a JSON Web Token (JWT) to store the authenticated user in a short lived cookie, though your approach may differ depending on the application's specific requirements.
Which seems to refer to this file in their example. Not sure if that affects the setup for Convex.
User Management – WorkOS Docs
Easy to use authentication APIs designed to provide a flexible, secure, and fast integration.
Custom Auth Integration | Convex Developer Hub
Convex can be integrated with any identity provider supporting the
User Management – WorkOS Docs
Easy to use authentication APIs designed to provide a flexible, secure, and fast integration.
GitHub
next-authkit-example/src/app/callback/route.ts at main · workos/nex...
Example application demonstrating how to authenticate users with AuthKit and the WorkOS Node SDK. - workos/next-authkit-example
223 Replies
Michal Srb
Michal Srb2y ago
Hey @John, from cursory look, I think you'd have to initiate a JWT on the server: https://workos.com/docs/user-management/3-handle-the-user-session/issue-a-jwt Then pass that to your client and pass that to Convex via the Custom Auth Integration. Does that help? Any reason you can't use Clerk or Auth0?
User Management – WorkOS Docs
Easy to use authentication APIs designed to provide a flexible, secure, and fast integration.
John
JohnOP2y ago
was mainly curious about workos since it says 1million free users going to just using clerk, the templates/doc and web dev cody yt videos were great references!
Abhishek
Abhishek2y ago
@John if you find any solution do share it here . I am also transitioning from clerk auth to some other solution cause clerk auth is having too many bugs and their support is also slow. Thanks
Michal Srb
Michal Srb17mo ago
@John you might be able to use WorkOS through NextAuth: https://stack.convex.dev/nextauth
Convex with Auth.js (NextAuth)
Learn how to use Auth.js with your Next.js server and Convex backend to build a full-featured authentication system.
Matt Luo
Matt Luo14mo ago
@Michal Srb - When you say work with WorkOS through NextAuth, I think you are saying to use WorkOS as a provider. I don't think that's going to help many Convex projects because businesses don't typically store their employee user info in WorkOS. Can I request a feature so that WorkOS is an officially supported third-party authentication provider for a Convex project? In the meantime, I'll try to setup WorkOS through the Custom Auth Integration approach.
https://docs.convex.dev/auth/advanced/custom-auth
Custom Auth Integration | Convex Developer Hub
Note: This is an advanced feature! We recommend sticking with the
Matt Luo
Matt Luo14mo ago
Actually, let me do some more research before going so far as to request a feature for Convex's official WorkOS support.
imad
imad4mo ago
it seems at the moment the first blocker I am facing is, workOS ISS=https://api.workos.com while the actual issuer is something like this https://xxxxxxxxxxxxx-staging.authkit.app so when I enter the correct one in convex, convex tries to match it to iss filed and we get ""error":"No auth provider found matching the given token"" I even tried to tamper with the JWT by changing the iss and signing it again, but obviously this wont work as WorkOS will not be able to verify the original sig @Matt Luo convex has to solve it by changing how they match issuer, or making it configurable in ConvexProviderWithAuth I spoke to workOS, they are very reluctant to do anything about it as it would be a breaking change for them
erquhart
erquhart4mo ago
Would they be open to setting up a redirect for the oidc endpoints from api.workos.com to the actual issuer? If their JWT's aren't compatible with oidc that feels like something worth addressing on their end. Redirect would be non-breaking.
Bruce
Bruce4mo ago
We are also trying to get workOS authkit to work and can't do it either! * We are able to decode the JWT token, but Convex’s “custom JWT” provider accepts only Content-Type: application/json WorkOS returns application/jwk-set+json
erquhart
erquhart4mo ago
Support for this content type is on the way
ahmed
ahmed4mo ago
@imad @erquhart so there is no way to use workos with convex right now? I was going to setup that
erquhart
erquhart4mo ago
I believe support for the content type mentioned by Bruce is now accepted, so it should work if that's all that was blocking.
Matt Luo
Matt Luo4mo ago
If you have a working WorkOS implementation with Convex, please share what you learned!
ahmed
ahmed4mo ago
I am still getting the same error With this config
export default {
providers: [
{
type: "customJwt",
applicationID: "convex-workos",
issuer: `https://auth.workos.com/sso/${process.env.WORKOS_CLIENT_ID}`,
jwks: `https://auth.workos.com/sso/jwks/${process.env.WORKOS_CLIENT_ID}`,
algorithm: "RS256",
},
],
} as const;
export default {
providers: [
{
type: "customJwt",
applicationID: "convex-workos",
issuer: `https://auth.workos.com/sso/${process.env.WORKOS_CLIENT_ID}`,
jwks: `https://auth.workos.com/sso/jwks/${process.env.WORKOS_CLIENT_ID}`,
algorithm: "RS256",
},
],
} as const;
ahmed
ahmed4mo ago
Custom JWT Provider | Convex Developer Hub
Note: This is an advanced feature! We recommend sticking with the
ahmed
ahmed4mo ago
Error when trying to authenticate:
{"type":"AuthError","error":"No auth provider found matching the given token","baseVersion":0,"authUpdateAttempted":true}
{"type":"AuthError","error":"No auth provider found matching the given token","baseVersion":0,"authUpdateAttempted":true}
erquhart
erquhart4mo ago
Ah the issuer not matching the issuer in the token would still be a problem Actually, you'll want to determine what the issuer is in the token (you can parse it at jwt.io), and use that for the issuer field. Then you need to determine what the actual jwks endpoint is and provide that under jwks field. @imad mentioned it's something like https://xxxxxxxxxxxxx-staging.authkit.app/
ahmed
ahmed4mo ago
this is the decoded payload
{
"aud": "convex-workos",
"iss": "https://api.workos.com",
"sub": "user_01JV2874P7YR4JJKGRZ2ZY4G4J",
"sid": "session_01JVFTEDF79MANVY44Z9M27NMG",
"jti": "01JVMNE7SHR7NBY48C79XKSRJG",
"exp": 1747671643,
"iat": 1747671523
}
{
"aud": "convex-workos",
"iss": "https://api.workos.com",
"sub": "user_01JV2874P7YR4JJKGRZ2ZY4G4J",
"sid": "session_01JVFTEDF79MANVY44Z9M27NMG",
"jti": "01JVMNE7SHR7NBY48C79XKSRJG",
"exp": 1747671643,
"iat": 1747671523
}
erquhart
erquhart4mo ago
With custom jwt no oidc discovery is done, so the issuer is just being used to validate the token. Gotcha, so the iss claim is what you want to use for issuer And then you need to determine where the actual jwks endpoint is and provide that under jwks
ahmed
ahmed4mo ago
I am not using authkit from workos and I tried with this URL
https://api.workos.com/sss/jwks/{CLIENT_ID}
https://api.workos.com/sss/jwks/{CLIENT_ID}
But now getting this error
{"type":"AuthError","error":"Invalid Content-Type when fetching JWKS","baseVersion":0,"authUpdateAttempted":true}
{"type":"AuthError","error":"Invalid Content-Type when fetching JWKS","baseVersion":0,"authUpdateAttempted":true}
ahmed
ahmed4mo ago
I tried with this online debugger and it is able to verify the token with same jwks url
No description
gizmo_jake_04110
Hey guys - I have a working Convex / WorkOS Authkit implementation so figured I'd share how I solved these issues: Issue #1 - "No auth provider found matching the given token" Use the customJwt format in your Convex auth.config.ts instead of the OIDC Provider format. Set issuer to https://api.workos.com or whatever custom domain you use. Issue #2 - missing aud claim For this you can simply go into the WorkOS Dashboard -> Authentication -> Custom JWT claims and add "aud". Call it whatever you want and make sure the value matches the issuer property from your Convex auth config. Issue #3 - "Invalid Content-Type when fetching" This is really something WorkOS should fix on their side, but there's an easy workaround. Just create your own API route and use that for the JWKS endpoint. For example, here is a simple NextJS Route Handler exposed at /api/jwks:
export async function GET(request: NextRequest) {
const jwks =
process.env.NODE_ENV === 'development'
? await fetch(
"https://api.workos.com/sso/jwks/{clientId}",
)

: await fetch(
"https://api.workos.com/sso/jwks/{clientId}",
);

const data = await jwks.json();

console.log('Fetched JWKS: ', jwks);

return NextResponse.json(data, { status: 200 });
}
export async function GET(request: NextRequest) {
const jwks =
process.env.NODE_ENV === 'development'
? await fetch(
"https://api.workos.com/sso/jwks/{clientId}",
)

: await fetch(
"https://api.workos.com/sso/jwks/{clientId}",
);

const data = await jwks.json();

console.log('Fetched JWKS: ', jwks);

return NextResponse.json(data, { status: 200 });
}
Then you can use your own URL in the auth.config. Here is what mine looks like: export default { providers: [ { type: 'customJwt', applicationID: 'gizmo', issuer: 'https://api.workos.com', jwks: 'https://your-app-url.com/api/jwks', algorithm: 'RS256', }, ], }; Hope this helps!
Bruce
Bruce4mo ago
Wow! amazing! thank you!
erquhart
erquhart4mo ago
Thanks for sharing this!! The content type should be supported, are you running self hosted by chance?
gizmo_jake_04110
Nope. And I'm on the latest version of Convex but it was still throwing an error with application/jwk-set+json being returned by WorkOS in the header. Also, another issue is when you use the method I posted above and install any Component, your push will fail because of a Zod Error for "appAuth.domain" being undefined. Even if you add domain: '', as long as the type is still customJwt it throws. So, I had to do an even more complex custom OIDC implementation in my own backend to get all of this working. Just a heads up in case that's something the team can address!
erquhart
erquhart4mo ago
Not sure on the Zod error but the content-type support was deployed yesterday, should work now Ah, there it is: https://github.com/get-convex/convex-js/blob/061c176c2fbe98eaf16dbd003d9e106277f61ac4/src/cli/lib/deployApi/types.ts#L7-L10
Robert Kirk
Robert Kirk3mo ago
I'm having some trouble getting this to work. Would appreciate any help figuring out what's going wrong. My auth.config.ts:
export default {
providers: [
{
type: "customJwt",
applicationID: "via-auth",
issuer: "https://api.workos.com",
jwks: `https://api.workos.com/sso/jwks/${process.env.WORKOS_CLIENT_ID}`,
algorithm: "RS256",

},
],
};
export default {
providers: [
{
type: "customJwt",
applicationID: "via-auth",
issuer: "https://api.workos.com",
jwks: `https://api.workos.com/sso/jwks/${process.env.WORKOS_CLIENT_ID}`,
algorithm: "RS256",

},
],
};
Robert Kirk
Robert Kirk3mo ago
The token that I manually verified (being sent down by the websocket)
No description
erquhart
erquhart3mo ago
was just going to ask for that hmm aud needs to match your applicationID
Robert Kirk
Robert Kirk3mo ago
That worked! I swore I tried that but I guess in all the combinations I didn't. Thanks for your help
sbkl
sbkl3mo ago
Hello, Would you have an example with the front end? I am banging my head to be able to plug it. for nextjs I mean
Rishad B
Rishad B3mo ago
I have this auth.config.js:
export default {
providers: [
{
type: 'customJwt',
applicationID: 'my-app',
issuer: 'https://api.workos.com',
jwks: `http://localhost:3000/api/auth/jwks`,
algorithm: 'RS256',
},
],
};
export default {
providers: [
{
type: 'customJwt',
applicationID: 'my-app',
issuer: 'https://api.workos.com',
jwks: `http://localhost:3000/api/auth/jwks`,
algorithm: 'RS256',
},
],
};
WorkOS JWT Template page:
{
"aud": "my-app"
}
{
"aud": "my-app"
}
/api/auth/jwks:
import { NextResponse } from 'next/server';

export async function GET() {
const clientId = process.env.WORKOS_CLIENT_ID;
if (!clientId) {
return NextResponse.json({ error: 'Missing WorkOS client ID' }, { status: 500 });
}

const jwks =
process.env.NODE_ENV === 'development'
? await fetch(`https://api.workos.com/sso/jwks/${clientId}`)
: await fetch(`https://api.workos.com/sso/jwks/${clientId}`);

const data = await jwks.json();

console.log('Fetched JWKS: ', jwks);

return NextResponse.json(data, { status: 200 });
}
import { NextResponse } from 'next/server';

export async function GET() {
const clientId = process.env.WORKOS_CLIENT_ID;
if (!clientId) {
return NextResponse.json({ error: 'Missing WorkOS client ID' }, { status: 500 });
}

const jwks =
process.env.NODE_ENV === 'development'
? await fetch(`https://api.workos.com/sso/jwks/${clientId}`)
: await fetch(`https://api.workos.com/sso/jwks/${clientId}`);

const data = await jwks.json();

console.log('Fetched JWKS: ', jwks);

return NextResponse.json(data, { status: 200 });
}
But when I try to access this from a query:
const identity = await ctx.auth.getUserIdentity();
console.log('identity', identity);
const identity = await ctx.auth.getUserIdentity();
console.log('identity', identity);
it always returns null
sbkl
sbkl3mo ago
Trying to summarise my implementation so far Client providers
"use client";

import * as React from "react";

import { ConvexProviderWithAuth, ConvexReactClient } from "convex/react";
import {
AuthKitProvider,
useAccessToken,
useAuth,
} from "@workos-inc/authkit-nextjs/components";

const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);

export function Providers({ children }: { children: React.ReactNode }) {
return (
<AuthKitProvider>
<ConvexProviderWithAuth client={convex} useAuth={useWorkosConvexAuth}>
{children}
</ConvexProviderWithAuth>
</AuthKitProvider>
);
}

function useWorkosConvexAuth() {
const {
accessToken,
loading: accessTokenLoading,
refresh,
} = useAccessToken();
const { user, loading } = useAuth();

const fetchAccessToken = React.useCallback(
async ({ forceRefreshToken }: { forceRefreshToken: boolean }) => {
if (accessTokenLoading || loading) return null;
if (!accessToken && forceRefreshToken) {
return (await refresh()) ?? null;
}
return accessToken ?? null;
},
[accessToken, accessTokenLoading, loading]
);

return React.useMemo(
() => ({
isLoading: loading || accessTokenLoading,
isAuthenticated: Boolean(user),
fetchAccessToken,
}),
[loading, accessTokenLoading, user, fetchAccessToken]
);
}
"use client";

import * as React from "react";

import { ConvexProviderWithAuth, ConvexReactClient } from "convex/react";
import {
AuthKitProvider,
useAccessToken,
useAuth,
} from "@workos-inc/authkit-nextjs/components";

const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);

export function Providers({ children }: { children: React.ReactNode }) {
return (
<AuthKitProvider>
<ConvexProviderWithAuth client={convex} useAuth={useWorkosConvexAuth}>
{children}
</ConvexProviderWithAuth>
</AuthKitProvider>
);
}

function useWorkosConvexAuth() {
const {
accessToken,
loading: accessTokenLoading,
refresh,
} = useAccessToken();
const { user, loading } = useAuth();

const fetchAccessToken = React.useCallback(
async ({ forceRefreshToken }: { forceRefreshToken: boolean }) => {
if (accessTokenLoading || loading) return null;
if (!accessToken && forceRefreshToken) {
return (await refresh()) ?? null;
}
return accessToken ?? null;
},
[accessToken, accessTokenLoading, loading]
);

return React.useMemo(
() => ({
isLoading: loading || accessTokenLoading,
isAuthenticated: Boolean(user),
fetchAccessToken,
}),
[loading, accessTokenLoading, user, fetchAccessToken]
);
}
Nexjs routes Request magic auth code
import { WorkOS } from "@workos-inc/node";

import { NextRequest, NextResponse } from "next/server";

export async function POST(req: NextRequest) {
const workos = new WorkOS(process.env.WORKOS_API_KEY);
const { email } = await req.json();

await workos.userManagement.createMagicAuth({
email,
});

return NextResponse.json({ ok: true });
}
import { WorkOS } from "@workos-inc/node";

import { NextRequest, NextResponse } from "next/server";

export async function POST(req: NextRequest) {
const workos = new WorkOS(process.env.WORKOS_API_KEY);
const { email } = await req.json();

await workos.userManagement.createMagicAuth({
email,
});

return NextResponse.json({ ok: true });
}
Sign in route and storing session as cookie under "wos-session". I can see the cookie properly setup in the browser.
import { saveSession } from "@workos-inc/authkit-nextjs";
import { WorkOS } from "@workos-inc/node";

import { NextRequest, NextResponse } from "next/server";

export async function POST(req: NextRequest) {
const workos = new WorkOS(process.env.WORKOS_API_KEY);
const { email, code } = await req.json();

const session = await workos.userManagement.authenticateWithMagicAuth({
clientId: process.env.WORKOS_CLIENT_ID!,
code,
email,
});

await saveSession(session, req);

return NextResponse.json({ ok: true });
}
import { saveSession } from "@workos-inc/authkit-nextjs";
import { WorkOS } from "@workos-inc/node";

import { NextRequest, NextResponse } from "next/server";

export async function POST(req: NextRequest) {
const workos = new WorkOS(process.env.WORKOS_API_KEY);
const { email, code } = await req.json();

const session = await workos.userManagement.authenticateWithMagicAuth({
clientId: process.env.WORKOS_CLIENT_ID!,
code,
email,
});

await saveSession(session, req);

return NextResponse.json({ ok: true });
}
jwks route. I can see convex calling this endpoint and the data being returned.
import { NextRequest, NextResponse } from "next/server";

export async function GET(request: NextRequest) {
const jwks = await fetch(
`https://api.workos.com/sso/jwks/${process.env.WORKOS_CLIENT_ID}`
);

const data = await jwks.json();

console.log("Fetched JWKS: ", data);

return NextResponse.json(data, { status: 200 });
}
import { NextRequest, NextResponse } from "next/server";

export async function GET(request: NextRequest) {
const jwks = await fetch(
`https://api.workos.com/sso/jwks/${process.env.WORKOS_CLIENT_ID}`
);

const data = await jwks.json();

console.log("Fetched JWKS: ", data);

return NextResponse.json(data, { status: 200 });
}
convex auth.config. I have deployed the repo to vercel to get the jwks url live so convex can access it but using the same workos client id than development.
import { env } from "./env";

export default {
providers: [
{
type: "customJwt",
applicationID: "convex",
issuer: "https://api.workos.com",
jwks: `https://api.workos.com/sso/jwks/${env.WORKOS_CLIENT_ID}`,
algorithm: "RS256",
},
],
};
import { env } from "./env";

export default {
providers: [
{
type: "customJwt",
applicationID: "convex",
issuer: "https://api.workos.com",
jwks: `https://api.workos.com/sso/jwks/${env.WORKOS_CLIENT_ID}`,
algorithm: "RS256",
},
],
};
And of course the JWT template in workos
{
"aud": "convex",
"name": "{{ user.first_name }} {{ user.last_name }}",
"email": {{user.email}},
"picture": {{user.profile_picture_url}},
"given_name": {{user.first_name}},
"updated_at": {{user.updated_at}},
"family_name": {{user.last_name}},
"email_verified": {{user.email_verified}}
}
{
"aud": "convex",
"name": "{{ user.first_name }} {{ user.last_name }}",
"email": {{user.email}},
"picture": {{user.profile_picture_url}},
"given_name": {{user.first_name}},
"updated_at": {{user.updated_at}},
"family_name": {{user.last_name}},
"email_verified": {{user.email_verified}}
}
lol.... the trailing slash on the issuer of the convex auth.config was the issue. I edited the code above and now you got a working custom integration of Authkit with convex for otp via email! for completion sake, below the methods calling the nextjs api routes you can plug with your form.
// Request for the POST method using workos.userManagement.createMagicAuth
export async function requestSignIn(email: string) {
return await fetch(`/api/auth/request-sign-in`, {
method: "POST",
body: JSON.stringify({ email }),
});
}
// Request for the POST method using workos.userManagement.authenticateWithMagicAuth and saving the session from workos
export async function signIn(email: string, code: string) {
return await fetch(`/api/auth/sign-in`, {
method: "POST",
body: JSON.stringify({ email, code }),
});
}
// Request for the POST method using workos.userManagement.createMagicAuth
export async function requestSignIn(email: string) {
return await fetch(`/api/auth/request-sign-in`, {
method: "POST",
body: JSON.stringify({ email }),
});
}
// Request for the POST method using workos.userManagement.authenticateWithMagicAuth and saving the session from workos
export async function signIn(email: string, code: string) {
return await fetch(`/api/auth/sign-in`, {
method: "POST",
body: JSON.stringify({ email, code }),
});
}
I also removed the nextjs endpoint for jwks which is not necessary anymore.
import { env } from "./env";

export default {
providers: [
{
type: "customJwt",
applicationID: "convex",
issuer: "https://api.workos.com",
jwks: `https://api.workos.com/sso/jwks/${env.WORKOS_CLIENT_ID}`,
algorithm: "RS256",
},
],
};
import { env } from "./env";

export default {
providers: [
{
type: "customJwt",
applicationID: "convex",
issuer: "https://api.workos.com",
jwks: `https://api.workos.com/sso/jwks/${env.WORKOS_CLIENT_ID}`,
algorithm: "RS256",
},
],
};
And on your auth form, don't forget to call the refreshAuth method below to update the AuthKit provider after the cookie has been set by the server api route via the saveSession helper on the browser. Otherwise, a hard reload of the page is necessary for the convex client to pickup the authentication state.
"use client";

import { requestSignIn, signIn } from "@/lib/workos";
import { useAuth } from "@workos-inc/authkit-nextjs/components";
import { useRouter } from "next/navigation";

export function AuthForm() {
const { refreshAuth } = useAuth();
const router = useRouter();

// Request sign in magic code here
// ...

async function handleSubmit(email: string, code: string) {
await signIn(email, code);
await refreshAuth();
router.push("/");
}

// form implementation here
// ...
}
"use client";

import { requestSignIn, signIn } from "@/lib/workos";
import { useAuth } from "@workos-inc/authkit-nextjs/components";
import { useRouter } from "next/navigation";

export function AuthForm() {
const { refreshAuth } = useAuth();
const router = useRouter();

// Request sign in magic code here
// ...

async function handleSubmit(email: string, code: string) {
await signIn(email, code);
await refreshAuth();
router.push("/");
}

// form implementation here
// ...
}
sbkl
sbkl3mo ago
here is a repo I put together with detailed explanations for the setup authkit-convex repo
GitHub
GitHub - sbkl/authkit-convex
Contribute to sbkl/authkit-convex development by creating an account on GitHub.
Rishad B
Rishad B3mo ago
I'm using magic auth too, thanks for sharing this! I'll give it a shot. It looks like I'd have to use authkit and ditch my custom designed login pages if I wanted this to work in my setup
sbkl
sbkl3mo ago
this example is made especially for custom designed login pages. Not using workos urls.
Rishad B
Rishad B3mo ago
Ah my bad I missed the AuthForm!
sbkl
sbkl3mo ago
Just updated the repo to add google oauth example. Make sure to add "authentication.oauth_succeeded" to your webhook.
ballingt
ballingt3mo ago
We can fix this, we already normalize some other trailing slashes because some providers tell you to use one when they shouldn't great work @sbkl, just catching up here this kind of thing https://github.com/get-convex/convex-backend/blob/35faa2b8e8b18b1814846b0ec2e003c5599ce2c3/crates/common/src/auth.rs#L64-L79 huh, this should already be normalizing slashes? need to look into it later
Perfect
Perfect3mo ago
@ballingt Do you think workos will become one of the officially supported 3rd party solutions in the future for auth or probably not?
ballingt
ballingt3mo ago
Sure! We've chatted with them back when we only had OIDC support about what to do, decided to go wtih custom JWT "Official" here would mean - mention them in docs? yeah definitely - have a quickstart in the docs, include it in npm create convex@latest wizard? sure, need to get around to it - include custom code for them in the core client? we stopped adding auth providers to the core package years ago so they'd (or we could in a separate package) manage the client code, but there's not much of it; in e.g. React, it's just "create a convex auth provider and pass in a useAuth hook;" but we do need to at least show that code somewhere
Perfect
Perfect3mo ago
Yeah even if not "official" maybe some mention in the docs like you said and a quick example/overview of the steps. About to start using convex for first time, going to try with workos
ballingt
ballingt3mo ago
we also need to buff up the auth error mesages, well done everyone in this thread debugging but it's hard when it just says "no auth provider found," or "can't validate;" we dont' want to reveal info inadvertently about a deployment's auth config but we could at least show extra info in the dashboard If someone can repro the missing slash issue I'd appreciate it, happy to normalize that, just need to find it
sbkl
sbkl3mo ago
And just integrated SSO in the example. Just tested with the Test organisation included in your workos stage env. For sure not perfect but a good base to start from. If you fork, the repo I shared and add the trialing slash on the issuer, you should be able to reproduce it.
sbkl
sbkl3mo ago
Sharing some pics with the form. Repo now includes magic auth with email OTP, google auth and SSO.
No description
No description
ballingt
ballingt3mo ago
Got it, I haven't cloned the repo yet but repro'd it in our internal tests of customJwt behavior.
Rishad B
Rishad B3mo ago
For some reason, my token seems to just expire after a few minutes of being logged in and I get Uncaught ConvexError: Authentication required - no identity found Ok I resolved it by calling await refreshAuth(); and also setting up the query client as per here - https://github.com/sbkl/authkit-convex/blob/main/src/components/providers.tsx#L20-L34 Hmm it still appears once or twice but not as bad as it was before Ok I see what it is. I need to adopt this pattern:
export function useTaskList() {
const { isAuthenticated, isLoading } = useConvexAuth();
const query = useQuery({
...convexQuery(
api.tasks.query.list,
isLoading || !isAuthenticated ? "skip" : {}
),
placeholderData: (prev) => prev,
});
return query;
}
export function useTaskList() {
const { isAuthenticated, isLoading } = useConvexAuth();
const query = useQuery({
...convexQuery(
api.tasks.query.list,
isLoading || !isAuthenticated ? "skip" : {}
),
placeholderData: (prev) => prev,
});
return query;
}
This seems to get around a race condition in the auth.
ballingt
ballingt3mo ago
Other versions of this include <Authenticated> component which doesnt render it's children until the connection is authenticated and const { isLoading, isAuthenticated } = useConvexAuth();
Rishad B
Rishad B3mo ago
True. This is not the best DX because for me part of the point of convex was to not have to do this stuff manually with react query. I'd personally love a WorkOS component or a more first-class integration. @sbkl's implementation is a great start though, I have some way of implementing protected queries/mutations now. Ok looks like that didn't help either. I notice if I hard refresh it goes away for a while. I'm wondering if I need to implement refreshAuth like this: https://discord.com/channels/1019350475847499849/1202520608672317471/1387692134878023710 Maybe I need to use this at the root level of my authenticated part of the app Ok I made a custom auth guard using useConvexAuth and then a custom loader to display instead of the blank screen and I think that should do it. Thank you!
ballingt
ballingt3mo ago
I think this could be better, both a WorkOS guide and protecting authenticated queries. The gist is - some convex queries requires auth, some don't - the client doesn't know which are which - the client sometimes (it's a race) ends up sending in requests to subscribe to queries before sending in auth lots of potential fixes, one would be having an "authed mode" for the client where it doesn't subscribe to anything until after auth has been sent in
Rishad B
Rishad B3mo ago
That gist is spot on. Also, I'm happy to help you guys in any way on my own time to make this WorkOS experience easier. Just let me know. I was planning to boilerplate some of this for my own projects anyway. I'll have a lot more need for WorkOS in the near future. So anything officially recommended by Convex would really help me and I can put in the time for it.
sbkl
sbkl3mo ago
lol exactly my thought. Started to work on a cli command like shadcn where it provides the working setup and all the code. And you change it as you please. 1- Install latest nextjs 2- Initialise shadcn 3- Install the necessary dependencies for the authkit nextjs convex integration to work for authentication (sso, oauth, magic auth) and webhooks to sync users and organisations tables (hono available as opt in or regular convex http router). 4- Copy the templates for all necessary config, functions, provider for nextjs and a custom form using shadcn components, tanstack form etc... Still early but looking good so far to start a brand new project with all the auth bit setup easily Video demo attached. One question @ballingt , npx convex dev is the only way to create a new convex project right? Currently the setup I have requires to have all env variables setup (WorkOS keys, redirect paths...) under the convex dashboard of the project otherwise zod will fail the deployment. Is there a command where I can just create a convex project then have the cli ask for the mandatory api keys so I can set them up with npx convex env set? Or I am thinking to make the project creation in the convex dashboard as a prerequisit and then ask for the convex slug before initiating the cli.
sbkl
sbkl3mo ago
Basically a command that would just init the project in your convex account and return the convex slug without deployment and code gen so I can continue requiring the keys and set them up. Once completed, you just cd into the project and run npx convex dev and you’re good to go
ballingt
ballingt3mo ago
You might find this script that sets up some Convex Auth stuff interesting, for how it gets through some of this https://github.com/get-convex/convex-auth/blob/main/src/cli/index.ts this looks great! would this flow work? 1. create (provision + write .env.local) + run codegen (just creating the convex/_generated/* files), but don't push code 2. read values from .env.local 3. push code What you're asking for does exist (you can provision call the same endpoint that the convex CLI does) but would rather make it a convex CLI command
sbkl
sbkl3mo ago
looks right! if this is possible then perfect. Would just need to run that command. thank you for pointing me on this. Going to steal some stuff from it.
ballingt
ballingt3mo ago
Here's something you can do today:
tomb@Thomass-MacBook-Pro-2 tutorial % npx convex dev --once; npx convex env set FOO=1; npx convex dev --once --run-sh 'echo do other stuff after a successful push'
✔ Provisioned a dev deployment and saved its name as CONVEX_DEPLOYMENT to .env.local

Write your Convex functions in convex/
Give us feedback at https://convex.dev/community or support@convex.dev
View the Convex dashboard at https://dashboard.convex.dev/d/first-bandicoot-876

✖ Error: Unable to push deployment config to https://first-bandicoot-876.convex.cloud
✖ Error fetching POST https://first-bandicoot-876.convex.cloud/api/push_config 400 Bad Request: InvalidModules: Hit an error while pushing:
Loading the pushed modules encountered the following
error:
Failed to analyze messages.js: Uncaught Error: please set FOO!
at <anonymous> (../convex/messages.ts:8:20)
✔ Successfully set FOO to 1 (on dev deployment first-bandicoot-876)
✔ Provisioned a dev deployment and saved its name as CONVEX_DEPLOYMENT to .env.local

Write your Convex functions in convex/
Give us feedback at https://convex.dev/community or support@convex.dev
View the Convex dashboard at https://dashboard.convex.dev/d/first-bandicoot-876

✔ 19:21:20 Convex functions ready! (1.57s)
do other stuff after a successful push
tomb@Thomass-MacBook-Pro-2 tutorial % npx convex dev --once; npx convex env set FOO=1; npx convex dev --once --run-sh 'echo do other stuff after a successful push'
✔ Provisioned a dev deployment and saved its name as CONVEX_DEPLOYMENT to .env.local

Write your Convex functions in convex/
Give us feedback at https://convex.dev/community or support@convex.dev
View the Convex dashboard at https://dashboard.convex.dev/d/first-bandicoot-876

✖ Error: Unable to push deployment config to https://first-bandicoot-876.convex.cloud
✖ Error fetching POST https://first-bandicoot-876.convex.cloud/api/push_config 400 Bad Request: InvalidModules: Hit an error while pushing:
Loading the pushed modules encountered the following
error:
Failed to analyze messages.js: Uncaught Error: please set FOO!
at <anonymous> (../convex/messages.ts:8:20)
✔ Successfully set FOO to 1 (on dev deployment first-bandicoot-876)
✔ Provisioned a dev deployment and saved its name as CONVEX_DEPLOYMENT to .env.local

Write your Convex functions in convex/
Give us feedback at https://convex.dev/community or support@convex.dev
View the Convex dashboard at https://dashboard.convex.dev/d/first-bandicoot-876

✔ 19:21:20 Convex functions ready! (1.57s)
do other stuff after a successful push
sbkl
sbkl3mo ago
Works like a charm!
ballingt
ballingt3mo ago
There's some related work coming up around being able to deploy multiple dev deployments per person, will roll looking into a "create without pushing" flow into that. One option is (yet) another flag like npx convex dev --no-push, which would just be a npx convex codegen if you already have a deployment but would create one if you don't.
sbkl
sbkl3mo ago
If possible to include a single command to set multiple env variables as well. It is marked as a todo on the convex-auth package. I am almost done with a first draft. Just reviewing webhooks because you cannot possibly have a webhook secret ready when initialising a project since you need the convex slug for it. So thinking on how to sequence it since having users/organisations synced between workos and the convex database is critical to me.
adam
adam3mo ago
@sbkl thank you for your repo, it's truly a gem! I wanted to know whether the following is possible I plan to run two webapps for my project. One is the admin dashboard panel And the other is the main site looking at how workos is implemented through the repo It seems like this wouldn't exactly work unless I'm wrong? I'm desperate to switch out from clerk to workos
Rishad B
Rishad B3mo ago
Working on a project right now that has a turborepo monorepo with a web app, mobile app and backend package with convex. The backend package is used across both apps. You could do the same with your use case of two web apps.
adam
adam3mo ago
Ah yes thank you! This would be perfect actually I'll start changing my repo for this
Rishad B
Rishad B3mo ago
Keep your workos code as separate as you can from your regular convex functions. For the backend package, you could just use what @sbkl has as a base.
adam
adam3mo ago
wait bit more on this as im going to be using workos for both main app and admin app how do i sync the webhook url?
Rishad B
Rishad B3mo ago
In http.ts. I just mean whatever workos stuff needs use node keep those actions in separate files. Don't mix them with queries and mutations.
sbkl
sbkl3mo ago
Released a first draft of the command. Try npx @sbkl/stack init. Let me know what you think. Instructions in the repo
GitHub
GitHub - sbkl/stack
Contribute to sbkl/stack development by creating an account on GitHub.
sonandmjy
sonandmjy3mo ago
I actually got it working in react router as well partly thanks to @sbkl 's example in nextjs if anyone is interested but the only thing I am not sure about is this part about revalidating the token, not sure if i am doing it correctly 🤔 there is no useAccessToken hook in the react-router package for authkit https://github.com/developerdanwu/ai-storefront/blob/4a9eb19a3a752b8b626bdc99176c91e058dbc4f0/app/components/auth/auth-provider.tsx#L65-L95
Rishad B
Rishad B3mo ago
@sbkl Is there a way to get your setup working with react native? I'm also getting page refreshes in my nextjs app when isAuthenticated is briefly false. Displaying a loading indicator, but it's random and hard to get around. I figure if I render null the screen will just go blank.
Rishad B
Rishad B3mo ago
You might be able to use this https://workos.com/docs/reference/user-management/authentication/refresh-token inside of a convex action
API Reference – WorkOS Docs
Code snippets and type definitions for the WorkOS client libraries.
Rishad B
Rishad B3mo ago
Now that I think of it, I might be able to do the same for react native
sonandmjy
sonandmjy3mo ago
@Rishad B thanks for suggestion but i think i cannot directly get the refresh token in the FE 🤔 can u? revalidate the route data actually seems to work for me rn haven’t run into issues yet
sbkl
sbkl3mo ago
authkit doesn't support react-native at the moment unfortunately. Got it working with convex-auth with their storage prop (and storageNamespace prop) secure-store.ts
import * as SecureStore from "expo-secure-store";

export const secureStore = {
getItem: (key: string) => SecureStore.getItem(key),
removeItem: (key: string) => SecureStore.deleteItemAsync(key),
setItem: (key: string, value: string) => SecureStore.setItem(key, value),
};
import * as SecureStore from "expo-secure-store";

export const secureStore = {
getItem: (key: string) => SecureStore.getItem(key),
removeItem: (key: string) => SecureStore.deleteItemAsync(key),
setItem: (key: string, value: string) => SecureStore.setItem(key, value),
};
providers.tsx
import * as React from "react";
import { RootSiblingParent } from "react-native-root-siblings";
import { env } from "@/lib/env";
import { secureStore } from "@/lib/secure-store";
import { ConvexAuthProvider } from "@convex-dev/auth/react";
import { ConvexQueryClient } from "@convex-dev/react-query";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ConvexReactClient } from "convex/react";

const convex = new ConvexReactClient(env.EXPO_PUBLIC_CONVEX_URL!, {
unsavedChangesWarning: false,
});
const convexQueryClient = new ConvexQueryClient(convex);
const queryClient = new QueryClient({
defaultOptions: {
queries: {
queryKeyHashFn: convexQueryClient.hashFn(),
queryFn: convexQueryClient.queryFn(),
},
},
});
convexQueryClient.connect(queryClient);

export function Providers({ children }: { children: React.ReactNode }) {
return (
<ConvexAuthProvider
client={convex}
storage={secureStore}
storageNamespace="session_token"
>
<QueryClientProvider client={queryClient}>
<RootSiblingParent>
{children}
</RootSiblingParent>
</QueryClientProvider>
</ConvexAuthProvider>
);
}
import * as React from "react";
import { RootSiblingParent } from "react-native-root-siblings";
import { env } from "@/lib/env";
import { secureStore } from "@/lib/secure-store";
import { ConvexAuthProvider } from "@convex-dev/auth/react";
import { ConvexQueryClient } from "@convex-dev/react-query";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ConvexReactClient } from "convex/react";

const convex = new ConvexReactClient(env.EXPO_PUBLIC_CONVEX_URL!, {
unsavedChangesWarning: false,
});
const convexQueryClient = new ConvexQueryClient(convex);
const queryClient = new QueryClient({
defaultOptions: {
queries: {
queryKeyHashFn: convexQueryClient.hashFn(),
queryFn: convexQueryClient.queryFn(),
},
},
});
convexQueryClient.connect(queryClient);

export function Providers({ children }: { children: React.ReactNode }) {
return (
<ConvexAuthProvider
client={convex}
storage={secureStore}
storageNamespace="session_token"
>
<QueryClientProvider client={queryClient}>
<RootSiblingParent>
{children}
</RootSiblingParent>
</QueryClientProvider>
</ConvexAuthProvider>
);
}
So you can imagine building something similar using the user management api.
Rishad B
Rishad B3mo ago
You could probably get it via @workos-inc/node inside a convex action and then return that value to the frontend Awesome I will try this What would the token setting side of things look like after login? I think I'm missing the full picture here
sbkl
sbkl3mo ago
authkit-js is the base package. It only supports browser based api which is not available to react-native. Therefore, it needs to allow for an optional storage props that would be used instead of the browser solution if it is provided. Then you have authkit-react where there is a need for the authkit-provider to accept an optional storage props as well and link it to the authkit-js usage there. Didn't look all the details but trying to make an implementation to make it working on my local machine and would submit a PR to both packages if they allow for it.
GitHub
GitHub - workos/authkit-js: Vanilla JS AuthKit SDK
Vanilla JS AuthKit SDK. Contribute to workos/authkit-js development by creating an account on GitHub.
GitHub
GitHub - workos/authkit-react: React SDK for AuthKit
React SDK for AuthKit. Contribute to workos/authkit-react development by creating an account on GitHub.
sonandmjy
sonandmjy3mo ago
Is anyone getting some errors like this at times? I believe it is when the token refreshes and then the screen seems to blank for 1 sec while the token refreshes (My error message is custom but it basically means it can't find a authenticated user in convex BE)
No description
Rishad B
Rishad B3mo ago
Yeah I got something like that earlier in the thread. I left some comments around what worked for me. Every detail in this setup is important to avoid race conditions.
sonandmjy
sonandmjy3mo ago
@Rishad B are you referring to this? but under the hood Authenticated already uses useConvexAuth, I don't see how that helps
No description
sbkl
sbkl3mo ago
wonder if it is not because of the hot reload
sonandmjy
sonandmjy3mo ago
I think I found the error message in the sync websocket but idk how to fix it hmm
No description
Perfect
Perfect3mo ago
Anyone have a good summary of this thread right now? I want to use workOS with convex but it seems like it is issues?
sbkl
sbkl3mo ago
@Rishad B made it work with expo. Using expo-secure-store, expo-auth-session and expo-web-browser. storage.ts
import * as SecureStore from "expo-secure-store";

export const storage = {
getItem: (key: string) => SecureStore.getItemAsync(key),
removeItem: (key: string) => SecureStore.deleteItemAsync(key),
setItem: (key: string, value: string) => SecureStore.setItemAsync(key, value),
};
import * as SecureStore from "expo-secure-store";

export const storage = {
getItem: (key: string) => SecureStore.getItemAsync(key),
removeItem: (key: string) => SecureStore.deleteItemAsync(key),
setItem: (key: string, value: string) => SecureStore.setItemAsync(key, value),
};
providers.tsx
import * as React from "react";

import { storage } from "@/lib/storage";
import { ConvexQueryClient } from "@convex-dev/react-query";

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ConvexProviderWithAuth, ConvexReactClient } from "convex/react";

interface ProvidersProps {
children: React.ReactNode;
}

const convex = new ConvexReactClient(process.env.EXPO_PUBLIC_CONVEX_URL!, {
unsavedChangesWarning: false,
});
const convexQueryClient = new ConvexQueryClient(convex);
const queryClient = new QueryClient({
defaultOptions: {
queries: {
queryKeyHashFn: convexQueryClient.hashFn(),
queryFn: convexQueryClient.queryFn(),
},
},
});
convexQueryClient.connect(queryClient);

export function Providers({ children }: ProvidersProps) {
return (
<ConvexProviderWithAuth client={convex} useAuth={useWorkosConvexAuth}>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</ConvexProviderWithAuth>
);
}

function useWorkosConvexAuth() {
const [isLoading, setIsLoading] = React.useState(true);
const [isAuthenticated, setIsAuthenticated] = React.useState(false);

React.useEffect(() => {
async function init() {
const accessToken = await storage.getItem("accessToken");
if (accessToken) {
setIsAuthenticated(true);
}
setIsLoading(false);
}
init();
}, []);

const fetchAccessToken = React.useCallback(
async ({
forceRefreshToken: _forceRefreshToken,
}: {
forceRefreshToken: boolean;
}) => {
// TODO: handle refresh token
return storage.getItem("accessToken");
},
[]
);

return React.useMemo(
() => ({
isLoading,
isAuthenticated,
fetchAccessToken,
}),
[isLoading, isAuthenticated, fetchAccessToken]
);
}
import * as React from "react";

import { storage } from "@/lib/storage";
import { ConvexQueryClient } from "@convex-dev/react-query";

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ConvexProviderWithAuth, ConvexReactClient } from "convex/react";

interface ProvidersProps {
children: React.ReactNode;
}

const convex = new ConvexReactClient(process.env.EXPO_PUBLIC_CONVEX_URL!, {
unsavedChangesWarning: false,
});
const convexQueryClient = new ConvexQueryClient(convex);
const queryClient = new QueryClient({
defaultOptions: {
queries: {
queryKeyHashFn: convexQueryClient.hashFn(),
queryFn: convexQueryClient.queryFn(),
},
},
});
convexQueryClient.connect(queryClient);

export function Providers({ children }: ProvidersProps) {
return (
<ConvexProviderWithAuth client={convex} useAuth={useWorkosConvexAuth}>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</ConvexProviderWithAuth>
);
}

function useWorkosConvexAuth() {
const [isLoading, setIsLoading] = React.useState(true);
const [isAuthenticated, setIsAuthenticated] = React.useState(false);

React.useEffect(() => {
async function init() {
const accessToken = await storage.getItem("accessToken");
if (accessToken) {
setIsAuthenticated(true);
}
setIsLoading(false);
}
init();
}, []);

const fetchAccessToken = React.useCallback(
async ({
forceRefreshToken: _forceRefreshToken,
}: {
forceRefreshToken: boolean;
}) => {
// TODO: handle refresh token
return storage.getItem("accessToken");
},
[]
);

return React.useMemo(
() => ({
isLoading,
isAuthenticated,
fetchAccessToken,
}),
[isLoading, isAuthenticated, fetchAccessToken]
);
}
auth-form.tsx
import { Button } from "@/components/ui/button";
import { api } from "@/convex/_generated/api";
import { storage } from "@/lib/storage";
import { useAction } from "convex/react";
import * as AuthSession from "expo-auth-session";
import * as WebBrowser from "expo-web-browser";
import { View } from "react-native";

export function AuthForm() {
const getAuthorisationUrl = useAction(api.workos.action.getAuthorisationUrl);
const authenticateWithCode = useAction(
api.workos.action.authenticateWithCode
);

async function handleGoogleLogin() {
// The redirectUri must be added to the sign-in callback in the workos dashboard
const redirectUri = AuthSession.makeRedirectUri().toString();

const authorisationUrl = await getAuthorisationUrl({
provider: "GoogleOAuth",
redirectUri,
});

const result = await WebBrowser.openAuthSessionAsync(
authorisationUrl,
redirectUri
);

if (result.type !== "success" || !result.url)
throw new Error("Authentication cancelled or failed");

const params = new URL(result.url).searchParams;

const code = params.get("code");

if (!code) throw new Error("No code returned");

const { accessToken, refreshToken, user } = await authenticateWithCode({
code,
});

await storage.setItem("refreshToken", refreshToken);
await storage.setItem("accessToken", accessToken);
await storage.setItem("user", JSON.stringify(user));
}

return (
<View className="flex-1 items-center justify-center">
<Button icon="logo-google" onPress={handleGoogleLogin}>
Sign In with Google
</Button>
</View>
);
}
import { Button } from "@/components/ui/button";
import { api } from "@/convex/_generated/api";
import { storage } from "@/lib/storage";
import { useAction } from "convex/react";
import * as AuthSession from "expo-auth-session";
import * as WebBrowser from "expo-web-browser";
import { View } from "react-native";

export function AuthForm() {
const getAuthorisationUrl = useAction(api.workos.action.getAuthorisationUrl);
const authenticateWithCode = useAction(
api.workos.action.authenticateWithCode
);

async function handleGoogleLogin() {
// The redirectUri must be added to the sign-in callback in the workos dashboard
const redirectUri = AuthSession.makeRedirectUri().toString();

const authorisationUrl = await getAuthorisationUrl({
provider: "GoogleOAuth",
redirectUri,
});

const result = await WebBrowser.openAuthSessionAsync(
authorisationUrl,
redirectUri
);

if (result.type !== "success" || !result.url)
throw new Error("Authentication cancelled or failed");

const params = new URL(result.url).searchParams;

const code = params.get("code");

if (!code) throw new Error("No code returned");

const { accessToken, refreshToken, user } = await authenticateWithCode({
code,
});

await storage.setItem("refreshToken", refreshToken);
await storage.setItem("accessToken", accessToken);
await storage.setItem("user", JSON.stringify(user));
}

return (
<View className="flex-1 items-center justify-center">
<Button icon="logo-google" onPress={handleGoogleLogin}>
Sign In with Google
</Button>
</View>
);
}
convex public actions to get the authorisation url and authenticate with code
export const getAuthorisationUrl = publicAction({
args: {
provider: v.string(),
redirectUri: v.string(),
},
async handler(_ctx, args) {
const workos = new WorkOS(env.WORKOS_API_KEY);
const url = workos.userManagement.getAuthorizationUrl({
clientId: env.WORKOS_CLIENT_ID,
provider: args.provider,
redirectUri: args.redirectUri,
});

return url;
},
});

export const authenticateWithCode = publicAction({
args: {
code: v.string(),
},
async handler(_ctx, { code }) {
const workos = new WorkOS(env.WORKOS_API_KEY);
const response = await workos.userManagement.authenticateWithCode({
clientId: env.WORKOS_CLIENT_ID,
code,
});

return response;
},
});
export const getAuthorisationUrl = publicAction({
args: {
provider: v.string(),
redirectUri: v.string(),
},
async handler(_ctx, args) {
const workos = new WorkOS(env.WORKOS_API_KEY);
const url = workos.userManagement.getAuthorizationUrl({
clientId: env.WORKOS_CLIENT_ID,
provider: args.provider,
redirectUri: args.redirectUri,
});

return url;
},
});

export const authenticateWithCode = publicAction({
args: {
code: v.string(),
},
async handler(_ctx, { code }) {
const workos = new WorkOS(env.WORKOS_API_KEY);
const response = await workos.userManagement.authenticateWithCode({
clientId: env.WORKOS_CLIENT_ID,
code,
});

return response;
},
});
basically using convex actions to use the workos node package in a secure manner replacing the server component and api routes in nextjs. I know it's available in expo with some setup but wanted to keep it simple btw the react-query part is optional but I like the api and use it everywhere. need to make a similar approach for magicAuth and SSO
Rishad B
Rishad B3mo ago
I use magic auth but this looks like a good start for me to try I've been using workos inside of actions to use it in both my web and mobile apps. I just couldn't get them protected like you just did. And my other protected queries and mutations were breaking. This will really help me, thanks! I'll post here once I've tested it.
ballingt
ballingt3mo ago
If possible to include a single command to set multiple env variables as well. It is marked as a todo on the convex-auth package.
@sbkl getting back to this, when would you want this to happen? Right before pushing the code, right?
sbkl
sbkl3mo ago
Correct. So when pushing and executing the code, the env variables are there and the zod parsing the process.env variables making sure the variables must be set, doesn’t produce an error because they are missing.
Rishad B
Rishad B3mo ago
This file helped me for my setup with expo, thanks! I handled the token refresh a bit differently though - separate provider that was refreshing in the background.
sbkl
sbkl3mo ago
I wrapped the whole thing with a provider managing the entire logic of sign in via expo-auth-session, sign out, refresh token via expo api routes with a storage props using expo-secure-store
Perfect
Perfect2mo ago
Update - I shared this thread with the founder of work os, he was super responsive on twitter/x. Said they are working on it 🙂 Nothing official yet but this should help some people https://github.com/workos/template-convex-nextjs-authkit
Noss
Noss2mo ago
Am I the only one that gets: Cannot find module '@workos-inc/authkit-nextjs/components' or its corresponding type declarations.ts(2307) When tying to: import { useAuth } from '@workos-inc/authkit-nextjs/components'; ? 😄 Well it seems like installing the package without setting version to 2.4.3, installs some 0.4.3 version or something like this
Perfect
Perfect2mo ago
Has anyone else noticed that everytime auth refreshes using the approaches in this thread, the loading state shows again briefly? I feel like it should only be considered on initial page load and then do its refreshes in the background.
Noss
Noss2mo ago
yeah, I also had the problem that the backend got called and I received unauthorized. Very weird
sbkl
sbkl2mo ago
Would be nice for the preloaded query under app/server/page.tsx to also have a "protected" query example (throw error if ctx.auth.getUserIdentity() is null) and show how to get the token server side on nextjs. If you can make the feedback. I actually believe the identitiy will always render null if you don't pass the accessToken. For instance, the preloaded query example:
const preloaded = await preloadQuery(api.myFunctions.listNumbers, {
count: 3,
});
const preloaded = await preloadQuery(api.myFunctions.listNumbers, {
count: 3,
});
could should how to get the accessToken server side with AuthKit and how to pass it to convex query/mutation.
const { accessToken } = await withAuth({ ensureSignedIn: true });

const preloaded = await preloadQuery(
api.myFunctions.listNumbers,
{
count: 3,
},
{ token: accessToken }
);
const { accessToken } = await withAuth({ ensureSignedIn: true });

const preloaded = await preloadQuery(
api.myFunctions.listNumbers,
{
count: 3,
},
{ token: accessToken }
);
And the convex function would change from this:
//// Read the database as many times as you need here.
//// See https://docs.convex.dev/database/reading-data.
const numbers = await ctx.db
.query('numbers')
// Ordered by _creationTime, return most recent
.order('desc')
.take(args.count);
return {
viewer: (await ctx.auth.getUserIdentity())?.name ?? null,
numbers: numbers.reverse().map((number) => number.value),
};
//// Read the database as many times as you need here.
//// See https://docs.convex.dev/database/reading-data.
const numbers = await ctx.db
.query('numbers')
// Ordered by _creationTime, return most recent
.order('desc')
.take(args.count);
return {
viewer: (await ctx.auth.getUserIdentity())?.name ?? null,
numbers: numbers.reverse().map((number) => number.value),
};
To this:
const identity = await ctx.auth.getUserIdentity();

if (!identity) {
throw new Error("Unauthorized");
}

//// Read the database as many times as you need here.
//// See https://docs.convex.dev/database/reading-data.
const numbers = await ctx.db
.query('numbers')
// Ordered by _creationTime, return most recent
.order('desc')
.take(args.count);
return {
viewer: identity.name ?? null,
numbers: numbers.reverse().map((number) => number.value),
};
const identity = await ctx.auth.getUserIdentity();

if (!identity) {
throw new Error("Unauthorized");
}

//// Read the database as many times as you need here.
//// See https://docs.convex.dev/database/reading-data.
const numbers = await ctx.db
.query('numbers')
// Ordered by _creationTime, return most recent
.order('desc')
.take(args.count);
return {
viewer: identity.name ?? null,
numbers: numbers.reverse().map((number) => number.value),
};
Niklas
Niklas2mo ago
Hi, I'm also currently investigating a Next.js App with Convex and WorkOS. The authentication on Convex Functions using ctx.auth.getUserIdentity() does work for queries in components that are children of the <Authenticated> component. My problem is that when WorkOS refreshes its access token (by default after 5 minutes), the app is briefly in a <Loading> / <Unauthenticated> state which will unmount all components for a moment and then rerender, once the new JWT is validated by the Convex backend. This causes the problem that all local state of app components is lost due to the rerender after such an access token refresh. Does anyone else have the same problem? I saw that Clerk also has a session token, that expires after 60 seconds. Is this reload thing a problem for users of Clerk. I looked into the implementation briefly.
ballingt
ballingt2mo ago
It's not a problem because their hook doesn't return "loading" while refreshing the token https://github.com/get-convex/convex-js/blob/main/src/react-clerk/ConvexProviderWithClerk.tsx the convex client reads the iat/exp or something (going from memory) to see when the token will expire, and tries to refresh the token 10 seconds? more? before the token expires to avoid a gap. Their auth hooks returns isLoaded: true throughout this process, so no interruption. If the workos hook doesn't, then we should include in the wrapper hook logic to store that "hey this is a refresh, let's say we're not loading for a couple seconds" or similar
Niklas
Niklas2mo ago
yes i also noticed that. i tried to cache that the auth was loaded before and than only set the loading state for the initial loading. i will look into it again. now that you mention it: the useAccessToken hook by WorkOS also automatically refreshes the token when its about to expire. Does Convex do the same? Whose responsibility is that really and shouldn’t I be able to deactivate this behavior from Convex, so the Auth provider will refresh the token only?
ballingt
ballingt2mo ago
Oh interesting, right now the convex client will call the fetchAuth function 10s before expiring; I don't think it calls it with forceRefresh, so ideally that doesn't make a network call and just uses the refreshed token
mwoof
mwoof2mo ago
@sbkl thank you for sharing your trials and tribulations with this. Were you (or anyone) able to get the workos OIDC example to actually work? ...or more specifically, retrieve the authed user's identity with ctx.auth.getUserIdentity() inside of a convex query? Was able to get a JWT version working but am hitting the same refresh issues as @Niklas and the OIDC just seems a lot cleaner. I just convinced my team to give convex a shot for a large refactor... and I am hoping that auth (literally the first thing we need to implement lol) isn't a blocker.
GitHub
GitHub - workos/template-convex-nextjs-authkit
Contribute to workos/template-convex-nextjs-authkit development by creating an account on GitHub.
mwoof
mwoof2mo ago
@Niklas if you use the custom JWT implementation, directly handle client side auth/ssr redirects with the official WorkOS packages and only ever use convex auth inside of queries and mutations then the following code seems to eliminate loading state issues:
export function ConvexClientProvider({ children }: { children: React.ReactNode }) {
return (
<AuthKitProvider>
<ConvexProviderWithAuth client={convex} useAuth={useWorkosConvexAuth}>
{children}
</ConvexProviderWithAuth>
</AuthKitProvider>
);
}

function useWorkosConvexAuth() {
const { accessToken, loading: accessTokenLoading, refresh} = useAccessToken();
const { user, loading } = useAuth();
const hasInitiallyLoaded = useRef(false);

useEffect(() => {
// Mark as initially loaded once we have a definitive auth state
if (!loading && !accessTokenLoading) hasInitiallyLoaded.current = true;
}, [loading, accessTokenLoading]);

const fetchAccessToken = useCallback(
async ({ forceRefreshToken }: { forceRefreshToken: boolean }) => {
// Don't return tokens while initial auth is still loading
if (!hasInitiallyLoaded.current && (loading || accessTokenLoading)) return null;

if (!user) return null;

// Only refresh if forced or no token exists
if (forceRefreshToken && !accessToken) {
try {
return (await refresh()) ?? null;
} catch (error) {
console.error("Failed to refresh access token:", error);
return null;
}
}
return accessToken ?? null;
},
[accessToken, accessTokenLoading, loading, refresh, user]
);

return useMemo(
() => ({
// Only show loading during initial load, not during token refreshes
isLoading: !hasInitiallyLoaded.current && (loading || accessTokenLoading),
isAuthenticated: Boolean(user),
fetchAccessToken,
}),
[loading, accessTokenLoading, user, fetchAccessToken]
);
}
export function ConvexClientProvider({ children }: { children: React.ReactNode }) {
return (
<AuthKitProvider>
<ConvexProviderWithAuth client={convex} useAuth={useWorkosConvexAuth}>
{children}
</ConvexProviderWithAuth>
</AuthKitProvider>
);
}

function useWorkosConvexAuth() {
const { accessToken, loading: accessTokenLoading, refresh} = useAccessToken();
const { user, loading } = useAuth();
const hasInitiallyLoaded = useRef(false);

useEffect(() => {
// Mark as initially loaded once we have a definitive auth state
if (!loading && !accessTokenLoading) hasInitiallyLoaded.current = true;
}, [loading, accessTokenLoading]);

const fetchAccessToken = useCallback(
async ({ forceRefreshToken }: { forceRefreshToken: boolean }) => {
// Don't return tokens while initial auth is still loading
if (!hasInitiallyLoaded.current && (loading || accessTokenLoading)) return null;

if (!user) return null;

// Only refresh if forced or no token exists
if (forceRefreshToken && !accessToken) {
try {
return (await refresh()) ?? null;
} catch (error) {
console.error("Failed to refresh access token:", error);
return null;
}
}
return accessToken ?? null;
},
[accessToken, accessTokenLoading, loading, refresh, user]
);

return useMemo(
() => ({
// Only show loading during initial load, not during token refreshes
isLoading: !hasInitiallyLoaded.current && (loading || accessTokenLoading),
isAuthenticated: Boolean(user),
fetchAccessToken,
}),
[loading, accessTokenLoading, user, fetchAccessToken]
);
}
ballingt
ballingt2mo ago
@mwoof this is all workable stuff! might take a couple days but there's no fundamental blocker here. Is there a repo I can make a PR to to fix this, do you have an example, is it the one you just posted? (reading more carefully now) @mwoof is this a workable solution for you, is there anything else you're missing? We'll get an example up in the next few days, I'm working with folks from WorkOS (well they're blocked on my merging their PRs, I'll do that) to make this smooth
mwoof
mwoof2mo ago
Absolutely! My first post probably came across a bit harsher than I meant. I’ve been a huge fan of Convex for years, and you, @Tom , have solved more of my problems than I can count. This solution works totally fine for now. Just a heads up though, WorkOS has recently released a few official videos and templates on integrating Convex, but they don’t explain how to secure data in the Convex database. Because of that, new users are being funneled into the Convex ecosystem without clear next steps on security (assuming thats the reason for a lot of the recent posts in this thread). A cleaner implantation might save you a headache or two (not that you need more on your plate lol). If I can help with documentation—or anything else—just let me know. Happy to pitch in!
ballingt
ballingt2mo ago
OH dont' worry, didn't sound harsh and harsh isn't a problem! Just want to make sure we get you set here, since this is already work we're doing it'd be a bummer if we just missed you
Noss
Noss2mo ago
Yes I was able to get it work as OIDC. I contacted WorkOS Support and they added a feature flag for me. After that aud was sent without me needing to add it to jwt template. The only thing that was missing: org id was not sent. I added this with the custom jwt template. I will take a look at your refresh fix. This is the only thing currently not working for me. Aud will then be the client id and the only thing that needs to be added in convex is:
export default {
providers: [
{
domain: "https://api.workos.com/convex/<CLIENT_ID>",
applicationID: "<CLIENT_ID>",
},
],
};
export default {
providers: [
{
domain: "https://api.workos.com/convex/<CLIENT_ID>",
applicationID: "<CLIENT_ID>",
},
],
};
If you were using custom domains or plan to use them in the future then you'll need to use the aud claim on your JWT template and the convex/auth.config.ts file should look like:
export default {
providers: [
{
domain: "https://custom-auth.example.com/sso/<CLIENT_ID>",
applicationID: "<value of aud claim in JWT template>",
},
],
};
export default {
providers: [
{
domain: "https://custom-auth.example.com/sso/<CLIENT_ID>",
applicationID: "<value of aud claim in JWT template>",
},
],
};
I am using "useAuth" and "withAuth" only in NextJS for checking if routes are accessible etc. In convex I only use const userIdentity = await ctx.auth.getUserIdentity(); I will test your implementation to see if it works for me. Thanks 🙂
Perfect
Perfect2mo ago
Did they mention if anything official with it was coming soon? Thanks for looking into it!
Noss
Noss2mo ago
@Perfect I have no official comment from WorkOS but Tom said they are working with them. I guess until then contact the support for the feature flag or use the CustomJWT... Either way you need the CustomJWT to get the orgid in the identity. 🙂 The orgid is then attached to the user you get from "withAuth" in NextJS like this for example:
const getOrgId = (user: any): string | undefined => {
return user['org_id'] || undefined;
};
const getOrgId = (user: any): string | undefined => {
return user['org_id'] || undefined;
};
And in convex:
const userIdentity = await ctx.auth.getUserIdentity();

const orgId = userIdentity['org_id'] as string
const userIdentity = await ctx.auth.getUserIdentity();

const orgId = userIdentity['org_id'] as string
Perfect
Perfect2mo ago
I think I’m just gonna wait until this settles down and something official comes out haha (at least for the feature flag function) I’ve been piecing things together from various places, not sure i feel highly confident in the solution
PushPull
PushPull2mo ago
I'm also kinda waiting for something official, I've been following this thread for a while and followed @sbkl 's instructions on getting the custom JWT working. It currently works, but I am still facing the constant refreshes that others have reported here 🥲
sonandmjy
sonandmjy2mo ago
I ended up using the client side react sdk from auth kit works much better if u don’t need SSR actually no refreshes anymore but have to use devMode=true in production cause i dont have a custom domain setup
Noss
Noss2mo ago
I am also still facing this problem. Its annoying... Until its fixed I think I have to live with it. It dev I have 5 Minutes jwt token validation and in prod 5h. So its not as annoying in prod
Para
Para2mo ago
Hi guys, what is the state of workos + convex combo? Based on what I have read, seems like the "native" integration is not as ideal yet, only the webhook method is working?
Perfect
Perfect2mo ago
No updates lately unless I missed something somewhere outside of discord I think @ballingt would know best for that 🙂 I just checked https://github.com/workos/template-convex-nextjs-authkit/commits/main/ Looks like progress is being made and there was an update to workos package to prevent the loading issue.
wodka
wodka2mo ago
For me there is still the issue with the token refresh (default at 5 min for workos) - during witch a wrong token seems to be sent and then the identity is not set on the server. Any working twoards solving this?
ballingt
ballingt2mo ago
Hey all, WorkOS is now mentioned in the Convex auth docs with a guide https://docs.convex.dev/auth/authkit
Perfect
Perfect2mo ago
Lets goo! Thanks @ballingt
ballingt
ballingt2mo ago
@wodka would love to hear if this is still an issue with this guide, or if that's been fixed in https://www.npmjs.com/package/@convex-dev/workos
npm
@convex-dev/workos
Helper for authenticating a Convex client via WorkOS AuthKit. Latest version: 0.0.1, last published: a day ago. Start using @convex-dev/workos in your project by running npm i @convex-dev/workos. There are no other projects in the npm registry using @convex-dev/workos.
Perfect
Perfect2mo ago
Is there a simple explanation for why we need both providers in auth config? Seems only difference is the issuer url?
ballingt
ballingt2mo ago
Pinging some folks this is relevant to @Para @Noss @sonandmjy @mwoof @Niklas @sbkl @Rishad B @adam @Robert Kirk @Bruce @gizmo_jake_04110 @ahmed @Abhishek @John I can add something, the explanation is that older WorkOS accounts have https://api.workos.com/ as the issuer, but newer ones have https://api.workos.com/user_management/${clientId}` oh shoot one of these is wrong, let me fix
Noss
Noss2mo ago
Thanks a lot for the update! I will rework my current implementation
ballingt
ballingt2mo ago
should be issuer: "https://api.workos.com/" for the first one, no client_id at the end
adam
adam2mo ago
this is awesome! cc: @amianthus
ballingt
ballingt2mo ago
fix coming in 5 min
sonandmjy
sonandmjy2mo ago
thanks, my current impl in react is working ok but i will test this one out as well soon! thank you for your work @ballingt
ballingt
ballingt2mo ago
OK fixed, this should be accurate now And thanks @nicknisi for doing all the work here!
Perfect
Perfect2mo ago
Is anyone else seeing this issue? (see link) I’m the only one who has seemed to report it, but I’m fairly confident all my code is correct. https://github.com/workos/template-convex-nextjs-authkit/commit/31ec6dcb25e0191a6b7e6522f2bfe3295d141be5#r162823020
mwoof
mwoof2mo ago
@ballingt you dog! This is awesome. Implementing now
Para
Para2mo ago
@ballingt awesome work! My backend is nuxt. Let me try to see if it works on nuxt
ballingt
ballingt2mo ago
there's a minor issue I'm looking into around refreshing, but I haven't been able to repro the issue you see yet @Perfect with Vite. I'll try Next.js soon, need to get to some other things first. To me knowledge this works, would love to hear if that's other people's experience too.
Perfect
Perfect2mo ago
Interesting, maybe a nextjs specific issue
ballingt
ballingt2mo ago
I'm sure we can fix it, we'll get there
Rishad B
Rishad B2mo ago
First off, thank you so much for documenting this. Is the react section of the docs applicable to react native? Also, it sounds like that brief error state where protected functions are not authenticated is fixed by this, is that correct?
Perfect
Perfect2mo ago
Are you using nextjs? The issue was a flash while refreshing a session. The new issue I brought up (yet to be reported by anyone else which makes me suspiscious of it) is unauth showing briefly even if authed. https://github.com/workos/template-convex-nextjs-authkit/commit/31ec6dcb25e0191a6b7e6522f2bfe3295d141be5#r162823020
Rishad B
Rishad B2mo ago
Unauth showing briefly even if authed is what I've been getting all along. I don't think it's new. I get it like this:
8/1/2025, 3:45:33 PM [CONVEX Q(lineups:list)] Uncaught ConvexError: Authentication required - no identity found
at getCurrentUserOrThrow (../../convex/lib/auth/utils.ts:51:21)
at async handler (../../convex/entities/lineups/queries.ts:366:16)
8/1/2025, 3:45:33 PM [CONVEX Q(lineups:list)] Uncaught ConvexError: Authentication required - no identity found
at getCurrentUserOrThrow (../../convex/lib/auth/utils.ts:51:21)
at async handler (../../convex/entities/lineups/queries.ts:366:16)
And yes, I've done the same logging you did. I'm pretty sure this has come up in the thread already.
Perfect
Perfect2mo ago
I dont get any thrown errors, just the convex auth hook returning the wrong values briefly Not sure, but Tom was looking into it. My ui looks pretty clunky on page load but gonna ignore it until things are fixed.
wodka
wodka2mo ago
sadly still the same problem - for a brief moment it is always not returning a identity on the backend (even though user is authenticated all the time)
ballingt
ballingt2mo ago
Are you also using Next.js? looking into Next.js today, I couldn't repro for a Vite SPA on Friday @wodka or @Perfect do you have a repro you could share, or could you tell me more about what you're seeing? I haven't been able to repro this yet.
ballingt
ballingt2mo ago
Here's what I see with my JWT expiration set to 1 minute (and a bit cut out in the middle): when a query reruns and hits an auth error, a new WebSocket is opened and new auth sent without any flash
ballingt
ballingt2mo ago
I'm using https://github.com/workos/template-convex-nextjs-authkit plus a new query that checks auth and throws when it's not set There are things to fix here like that flash on initial load; that's no good and I'd rather avoid the websocket reconnects, it would be better to proactively get updated auth tokens but as a fallback, a query or mutation encounterying expired auth is supposed to work like above
Perfect
Perfect2mo ago
That’s the only issue I’ve been seeing myself - I haven’t had any issue with the refresh flash, that was fixed after the official updates I didn’t even notice it was doing that, what’s causing that?
ballingt
ballingt2mo ago
that's just how the protocol deals with expired auth, the server cuts the connection and the client is responsible for getting some an updated auth token, and isn't supposed to reconnect until it gets one; I'm showing it here to convey I am hitting this what I would assume woudl cause this issue Cool, ok I'm going to work on that initial flash first
Perfect
Perfect2mo ago
Ah ok gotcha And last thing I noticed
ballingt
ballingt2mo ago
but we should be able to fix the slight delay caused by expired auth too, will need to check in with @nicknisi about the client getting pushed an updated token, but it's lower pri since the backup mechanism seems to be working
Perfect
Perfect2mo ago
What controls the Ui in your header auth?
Perfect
Perfect2mo ago
Ok yeah I just wanted to make sure this wasn’t convex Makes sense why it doesn’t update Care to elaborate what you mean on this? Appreciate it! Curious how you will fix 🙂
ballingt
ballingt2mo ago
Each time the server finds auth is expired, it closes the WebSocket connection as a signal to the client to get its act together and get a refreshed token. The WebSocket closes and the client requests fresh auth from WorkOS (which already has it, so this part if instant) and so the websocket is opened again and the auth is sent. This causes a delay vs just getting the updated query. It's lower priority because it's just ones every five minutes, the first interaction (mutation send or new query run or query update receive) will have an extra ~100-300ms delay. It's not good, but lower pri that the flash
ballingt
ballingt2mo ago
Here's what I see now 1. page is white (or if we were navigating form another page, would show the old page. This is normal Next.js stuff. "Auth is loading" because it's loading during SSR (we say it's always loading during SSR) and then on the client it's also considered loading as WorkOS doesn't know whether we're logged in or not. 2. WorkOS says we're logged out (?) 3. WorkOS says we're logged in, and the convex query data is now loading 4. convex query data loads The part I'm going to try to fix is eliminating Step 2 (seems like that's wrong, WorkOS should know that we're logged in, or else should keep saying "loading") which might not speed things up, but should make the flash less dramatic. The next thing I want to fix is having an auth token on initial render: if we're SSRing, we should be able to have a token ready The final thing I want to fix is making SSR'd data just snap in. With Next.js this required using preloadQuery and usePreloadedQuery, so that's not an automatic speedup for everyone but everything else above could be
Perfect
Perfect2mo ago
I don't think the template does any explicit SSR if that is what you were asking. But I agree with all of this. In my head I think of it working properly like this... (prob have some misunderstaning somewhere though) 1. The nextjs page is loading up. Nothing weird here. 2. workOS useAuth hook is loading, accessToken is being fetched. 3. Things that rely on workOS useAuth can display. 4. Things that rely on convex have to wait for both hooks to finish loading, then gets the token and is happy. 5. Workos refresh happens in background and convex has to get new token with disconnect like you explained once every 5 minutes. Also when you say "the server finds auth is expired" that would be via the next convex function that checks it right? It would fail because auth is expired? or realtime it knows you mean?
ballingt
ballingt2mo ago
GitHub
Consider no auth token a loading state by thomasballinger · Pull R...
An auth token not yet being available shoudl be considered a loading state for the Convex useAuth helper
Perfect
Perfect2mo ago
Shouldnt tokenLoading be true until the access token exists? At least I assume that naturally I did the same fix but assumed it was just like a duct tape kinda thing
ballingt
ballingt2mo ago
Yep, that would be the next convex function that checks it. A query has to run (either because a client requests it or the arguments it as called with changed or the database ranges it reads are updated) or a mutation or action has to run to find out that auth is expired. It's considered an edge case, ideally clients should keep sending in fresh auth.
Perfect
Perfect2mo ago
So if I am authed all good then its been 5 minutes A query runs, it will fail? -> auth will be resent like it was initially, all good again for another 5
ballingt
ballingt2mo ago
ah yeah you would think so! But apparently not
No description
Perfect
Perfect2mo ago
Ok yeah I found out same thing https://github.com/workos/template-convex-nextjs-authkit/commit/31ec6dcb25e0191a6b7e6522f2bfe3295d141be5#r162823020 Ok cool I feel like Im not going crazy anymore
ballingt
ballingt2mo ago
yep that's the current state, would like to fix that but it'll take some coordination from the AuthKit side and Convex. I raised this last week at https://github.com/get-convex/convex-js/pull/55#discussion_r2246638584 Since WorkOS wants to manage refreshing their own auth tokens (this is great, they're in a better position to do it than we are) we might need them to push those tokens to the convex client.
Perfect
Perfect2mo ago
Ahh i see The reasoning being in this comment
const fetchAccessToken = useCallback(async () => {
// WorkOS AuthKit automatically refreshes tokens before they expire,
// so we don't need to manually handle forceRefreshToken to avoid infinite loops.
// This is the recommended approach for WorkOS + Convex integration.
if (stableAccessToken.current && !tokenError) {
return stableAccessToken.current;
}
return null;
}, [tokenError]);
const fetchAccessToken = useCallback(async () => {
// WorkOS AuthKit automatically refreshes tokens before they expire,
// so we don't need to manually handle forceRefreshToken to avoid infinite loops.
// This is the recommended approach for WorkOS + Convex integration.
if (stableAccessToken.current && !tokenError) {
return stableAccessToken.current;
}
return null;
}, [tokenError]);
But I dont really get why it would be an infinite loop 🤔 either way, unless I missed it, a deeper explanation of how convex auth works in the docs would be awesome I think for someone trying to understand all the stuff going on to make convex happy. 🙂 thx for ur help
Rishad B
Rishad B2mo ago
@ballingt Is there any way we can push workos to get a react native/expo repo too?
ballingt
ballingt2mo ago
I don't work there, I don't know! But if you're a paying WorkOS customer I'd bet if you reach out to them and let them know that's something you want you'll hear something opened an issue about for passing an accessToken through to the client when it's available https://github.com/workos/authkit-nextjs/issues/286
wodka
wodka2mo ago
I can try to put a repo together - it is based on nextjs, also for me the socket connection is not disconnected - so it keeps updating but briefly sends a empty auth and then the updated auth - in the helper I could not find why this is empty for a moment but I guess the error is coming from that. (the 2 messages are only a few milliseconds appart)
Perfect
Perfect2mo ago
This might be the issue I was having? Try adding || !accessToken to your loading boolean in provider
makisuo
makisuo2mo ago
Have the same issue in a React SPA app, getting into permannt redirect loops after the session runs out, is there a current solution there?
wodka
wodka5w ago
that actually fixed it - now there are no more strange logout for a split second!! Working for me! thx @Perfect and @Tom
Perfect
Perfect5w ago
cool - loading is broken on that hook so you cant rely on it to mean anything. That just adds a check to make sure its actually there.
Hamburger-Fries
Has anyone had better luck with Better-Auth? https://github.com/get-convex/better-auth I tried to get WorkOS working without the annoying refresh and then it kind of got fixed but now having a looping issue.
So tomorrow I will swap out WorkOS for Better-Auth. I just have not found anyone talking about Bett-Auth 100% working.
GitHub
GitHub - get-convex/better-auth: Convex + Better Auth 🔥
Convex + Better Auth 🔥. Contribute to get-convex/better-auth development by creating an account on GitHub.
Perfect
Perfect5w ago
It is in alpha, probably will have more issues than with the workos integration. I dont think tom was seeing this looping issue you are talking about, might want to prepare a minimal example to reproduce it/report it
Hamburger-Fries
So you think for now just go with WorkOS?
adam
adam5w ago
I genuinely just switched to convex Auth for a bit and am planning to switch to either workos or Kinde soon
adam
adam5w ago
Gist
A guide on setting up Kinde Auth with Convex and TanStack Router
A guide on setting up Kinde Auth with Convex and TanStack Router - Kinde + Convex + TanStack Router.md
Rishad B
Rishad B5w ago
I'm using WorkOS for projects but I can never get auth inside functions working without that unauthorized error. Debating switching to Kinde for future projects. Is your use case B2B or B2C?
ballingt
ballingt5w ago
@Hamburger-Fries yeah I haven't been able to reproduce this issue, which template are you using? See upthread for where we fixed the refresh issue @Rishad B what template or guide are you using? this should be fixed, WorkOS is working well now The only flash left is the transition in Next.js from server-rendered content to live-refreshing (authed preloadQuery()) and I've got a fix for that coming, should be out this week. To anyone having WorkOS issues, please let me know or raise a GitHub issue on https://github.com/workos/template-convex-react-vite-authkit or https://github.com/workos/template-convex-nextjs-authkit; I think everything is working here, if you have the refresh issue note the change here https://github.com/workos/template-convex-nextjs-authkit/pull/2/files
Rishad B
Rishad B5w ago
I'll put this in a query:
const identity = await ctx.auth.getUserIdentity();
if (identity === null) {
throw new Error('Unauthenticated call to mutation');
}
const identity = await ctx.auth.getUserIdentity();
if (identity === null) {
throw new Error('Unauthenticated call to mutation');
}
And even though I'm logged in, I'll get:
8/11/2025, 2:28:31 PM [CONVEX Q(entities/dashboard/queries:getOverview)] Uncaught Error: Unauthenticated call to mutation
at handler (../../convex/entities/dashboard/queries.ts:43:12)

8/11/2025, 2:28:32 PM [CONVEX Q(entities/dashboard/queries:getOverview)] Uncaught Error: Unauthenticated call to mutation
at handler (../../convex/entities/dashboard/queries.ts:43:12)
8/11/2025, 2:28:31 PM [CONVEX Q(entities/dashboard/queries:getOverview)] Uncaught Error: Unauthenticated call to mutation
at handler (../../convex/entities/dashboard/queries.ts:43:12)

8/11/2025, 2:28:32 PM [CONVEX Q(entities/dashboard/queries:getOverview)] Uncaught Error: Unauthenticated call to mutation
at handler (../../convex/entities/dashboard/queries.ts:43:12)
Perfect
Perfect5w ago
What package will be updated?
ballingt
ballingt5w ago
convex Can you share your setup or and show the WebSocket messages, is an Authenticate message being sent? Or run in verbose mode, like new ConvexReactClient(url, { verbose: true }) plan is to have a "requireAuth: true" mode that just holds off requesting any queries until an auth token has been sent, and to make sure preloaded data is used until then
Rishad B
Rishad B5w ago
Where can I see websocket messages? verbose: true isn't doing anything different for me
ballingt
ballingt5w ago
there should be a lot more console.log messages
Perfect
Perfect5w ago
Ah that makes sense, haven’t tried with SSR currently. I assume this is fixing the preloaded data just not being used properly for authed queries?
ballingt
ballingt5w ago
you might be SSRing already if you're using preloadQuery?
Perfect
Perfect5w ago
Will that require auth mode be for all of convex or per query Nah nothing with preloads yet but I did plan to use it as my default approach for most things Is preloaded data that used auth just going into the void or something right now?
ballingt
ballingt5w ago
It should be rendering, but then it disappears for a moment (that flash)
Perfect
Perfect5w ago
The flash being the loading state with auth getting all synced up?
Perfect
Perfect5w ago
Ahhh ok Awesome stuff
ballingt
ballingt5w ago
I see "server confirmed auth token is valid [v1]", that makes me this think should be working @Rishad B is this a race condition, does the mutation sometimes work and sometimes not? It looks to me like await ctx.auth.getUserIdentity() would return something
Rishad B
Rishad B5w ago
Sorry used it in a query, not mutation
ballingt
ballingt5w ago
Does the query work later? Just not at first?
Rishad B
Rishad B5w ago
I'm using it via preloadQuery Doesn't work no matter how much I refresh
8/11/2025, 2:49:58 PM [CONVEX Q(entities/dashboard/queries:getOverview)] [LOG] 'identity' null
8/11/2025, 2:49:58 PM [CONVEX Q(entities/dashboard/queries:getOverview)] [LOG] 'identity' null
ballingt
ballingt5w ago
Oh preloadQuery runs on the server, you need to pass in a token explicitly, preloadQuery(api.foo.bar, args, { token: yourJWT }) hmm maybe we should add a lint rule for this, this is another time when I wish we know at the type level that a query/mutation/action expects auth
Rishad B
Rishad B5w ago
api.foo.bar is still a query though which is where I'm testing it I'm just calling it from the page using preloadQuery
Perfect
Perfect5w ago
Reminds me - I think clerk and auth0 have examples of how to get the token on server (in tabs in convex docs). Could add for workos, unless it’s already there now. Haven’t checked
ballingt
ballingt5w ago
I don't understand yet, so you're running preloadQuery from the browser instead of the server? that's unusua but it shoudl work, but you'll need to pass in the JWT explicitly.
Rishad B
Rishad B5w ago
Hmm maybe I'm using preloadQuery wrong
ballingt
ballingt5w ago
There's an example here https://github.com/workos/template-convex-nextjs-authkit/blob/main/app/server/page.tsx but not in the quickstart nevermind, that's not of grabbing a token
Perfect
Perfect5w ago
Preload is for getting the data on the server so it’s ready when it reaches the client @Rishad B
Rishad B
Rishad B5w ago
Oh I switched to query and it works When I searched docs, I didn't see a token being specified I'm using it wrong right now obviously, but I just looked
ballingt
ballingt5w ago
Yeah good point, I have it in my local verison but haven't made a PR
Perfect
Perfect5w ago
Next.js Server Rendering | Convex Developer Hub
Implement server-side rendering with Convex in Next.js App Router using preloadQuery, fetchQuery, and server actions for improved performance.
Perfect
Perfect5w ago
We can add a WorkOS tab here
ballingt
ballingt5w ago
some maybe-interesting changes https://github.com/workos/template-convex-nextjs-authkit/compare/main...thomasballinger:template-convex-nextjs-authkit:tmp from testing, one I have the auth mode thing in I'll update the official one
Rishad B
Rishad B5w ago
Also need to figure out how to make sure this works with Expo. I'm doing manual token storage right now. I've also seen this but it's an SSO example, not magic auth which is what I'm using. Although both providers are in auth.config, so maybe it will work.
React Native Expo – Integrations – WorkOS Docs
Learn how to integrate WorkOS SSO into a React Native Expo app.
Rishad B
Rishad B5w ago
Yes please!
Perfect
Perfect5w ago
I should say Tom can do that haha I wish I worked at convex, if ur in SF they are hiring 😂 Nevermind the docs are on GitHub I can prob open a PR later
ballingt
ballingt5w ago
Nice I'll add it. This repo is open source if you want to do a PR, but seeing there are twice the number of venture-backed for-profit companies involved here than usual I think I can manage 😆 But yeah feel free, would love the contribution! Or save your contributing muscles for fixing bugs 😃
Hamburger-Fries
Thanks Tom! I will add the new implementation layer and scheme changes today. I spent the day removing Permify and replacing it with Zenstack for ReBAC/RBAC/ABAC
Rishad B
Rishad B4w ago
Are you using those for RBAC alongside Convex?
Hamburger-Fries
Yes - so far so great. Mostly just RBAC and ReBAC for now.
Rishad B
Rishad B4w ago
Cool. I use WorkOS roles and then hardcode permissions though I could use theirs too.
ballingt
ballingt4w ago
@Perfect check out the experimental expectAuth: true option in convex@1.26.0-alpha.9, re the flash conversation we were having
Perfect
Perfect4w ago
Oh sweet, definitely will maybe later tonight Thanks for letting me know
wodka
wodka4w ago
looking forward to it - for me the errors are now gone in the development, but are now only in production :/ maybe this fixes it as well
Para
Para4w ago
Can I use the convex workos docs for react for Tanstack start setup?
Perfect
Perfect3w ago
did this end up shipping? have been away from laptop for a few days
ballingt
ballingt3w ago
This is still only in the alpha, new client release tomorrow at this point
wodka
wodka3w ago
saw the release commit for 1.26.0 but cannot get the package anywhere^^
ballingt
ballingt3w ago
not out yet, testing it; you can do with convex@1.26.0-alpha.12 if you like
Perfect
Perfect2w ago
Hey @ballingt @nicknisi Just wanted to see if you had any updates in this area? Saw your PR Tom, I think everything makes sense in it. The loading state change should not be needed once this PR from Nick is completed right? https://github.com/workos/authkit-nextjs/pull/297
sbkl
sbkl4d ago
@ballingt I still get unauthenticated errors on my protected query which is a custom query I use to inject the user to the context. If the user identity doesn't exist, I throw an error. On first load and subsequent load, no problem. But after some time I get the unauthenticated error I throw which causes a client runtime error both in dev and production (using nextjs and vercel). Could this be related to token refresh automatically handled by the workos library? I also use the tanstack query wrapper (@convex-dev/react-query) so I can get pending state. Not sure if related...
ballingt
ballingt4d ago
Hm I have not thought about auth with the TanStack Query wrapper; how does it handle auth I wonder.
sbkl
sbkl4d ago
I can confirm the error happens even with the standard useQuery from convex. Just reproduced it.
ballingt
ballingt4d ago
Ah thanks, that's helpful Sounds like it's something around re-auth? What is your JWT expiration set to, if you ramp it down to 1 minutes do you see these every 60s?
sbkl
sbkl4d ago
let me try and feedback
ballingt
ballingt4d ago
If you've got a repro that makes it easy, the WorkOS team is real responsive so we just need to share the issue but also it could have to do with the Convex integration part, so I should jump in
sbkl
sbkl4d ago
So I've put the token to 1 min and using
const { exp } = useTokenClaims();
const { exp } = useTokenClaims();
. When I stay in front of my computer and unfocus the tab, I can see the token refreshing and no trouble. I leave my computer for a while and then reopen it, the error is there... Let me try to make a repro and share it with them. So the below seemed to solve this. Instead of relying on the accessToken from useAccessToken which is flagged as "might be stale" and the stableAccessToken ref from the workos example in the docs, I replaced it with relying on the getAccessToken which the docs describe as "Get a guaranteed fresh access token. Automatically refreshes if needed. Use this for API calls where token freshness is critical."
function useAuthFromAuthKit() {
const { user, loading: isUserLoading } = useAuth();
const {
getAccessToken,
loading: isTokenLoading,
error: tokenError,
} = useAccessToken();

const isLoading = (isUserLoading ?? false) || (isTokenLoading ?? false);
const isAuthenticated = !!user;

const fetchAccessToken = React.useCallback(async () => {
if (tokenError) return null;
const token = await getAccessToken();
return token ?? null;
}, [getAccessToken, tokenError]);

return {
isLoading,
isAuthenticated,
fetchAccessToken,
};
}
function useAuthFromAuthKit() {
const { user, loading: isUserLoading } = useAuth();
const {
getAccessToken,
loading: isTokenLoading,
error: tokenError,
} = useAccessToken();

const isLoading = (isUserLoading ?? false) || (isTokenLoading ?? false);
const isAuthenticated = !!user;

const fetchAccessToken = React.useCallback(async () => {
if (tokenError) return null;
const token = await getAccessToken();
return token ?? null;
}, [getAccessToken, tokenError]);

return {
isLoading,
isAuthenticated,
fetchAccessToken,
};
}
Noss
Noss3d ago
I see this: { "type": "Authenticate", "tokenType": "None", "baseVersion": 1 } in the sync always when this happens
Para
Para2d ago
@Tom At my home page, I have used the AuthLoading Authenticated and Unauthenticated helpers and now every time I refresh the page, it takes about 2s to load. How can I do better?

Did you find this page helpful?