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
263 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 Srb2y 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 Luo16mo 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 Luo16mo ago
Actually, let me do some more research before going so far as to request a feature for Convex's official WorkOS support.
imad
imad6mo 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
erquhart6mo 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
Bruce6mo 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
erquhart6mo ago
Support for this content type is on the way
ahmed
ahmed6mo ago
@imad @erquhart so there is no way to use workos with convex right now? I was going to setup that
erquhart
erquhart6mo 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 Luo6mo ago
If you have a working WorkOS implementation with Convex, please share what you learned!
ahmed
ahmed6mo 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
ahmed6mo ago
Custom JWT Provider | Convex Developer Hub
Note: This is an advanced feature! We recommend sticking with the
ahmed
ahmed6mo 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
erquhart6mo 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
ahmed6mo 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
erquhart6mo 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
ahmed6mo 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
ahmed6mo 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
Bruce6mo ago
Wow! amazing! thank you!
erquhart
erquhart6mo 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
erquhart6mo 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 Kirk5mo 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 Kirk5mo ago
The token that I manually verified (being sent down by the websocket)
No description
erquhart
erquhart5mo ago
was just going to ask for that hmm aud needs to match your applicationID
Robert Kirk
Robert Kirk5mo ago
That worked! I swore I tried that but I guess in all the combinations I didn't. Thanks for your help
sbkl
sbkl5mo 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 B5mo 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
sbkl5mo 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
sbkl5mo 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 B5mo 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
sbkl5mo ago
this example is made especially for custom designed login pages. Not using workos urls.
Rishad B
Rishad B5mo ago
Ah my bad I missed the AuthForm!
sbkl
sbkl5mo ago
Just updated the repo to add google oauth example. Make sure to add "authentication.oauth_succeeded" to your webhook.
ballingt
ballingt5mo 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
Perfect5mo 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
ballingt5mo 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
Perfect5mo 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
ballingt5mo 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
sbkl5mo 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
sbkl5mo ago
Sharing some pics with the form. Repo now includes magic auth with email OTP, google auth and SSO.
No description
No description
ballingt
ballingt5mo ago
Got it, I haven't cloned the repo yet but repro'd it in our internal tests of customJwt behavior.
Rishad B
Rishad B5mo 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
ballingt5mo 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 B5mo 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
ballingt5mo 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 B5mo 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
sbkl5mo 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
sbkl5mo 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
ballingt5mo 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
sbkl5mo 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
ballingt5mo 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
sbkl5mo ago
Works like a charm!
ballingt
ballingt5mo 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
sbkl5mo 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.
ether
ether5mo 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 B5mo 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.
ether
ether5mo ago
Ah yes thank you! This would be perfect actually I'll start changing my repo for this
Rishad B
Rishad B5mo 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.
ether
ether5mo 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 B5mo 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
sbkl5mo 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
sonandmjy5mo 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 B5mo 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 B5mo 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 B5mo ago
Now that I think of it, I might be able to do the same for react native
sonandmjy
sonandmjy5mo 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
sbkl5mo 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 B5mo 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
sbkl5mo 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
sonandmjy5mo 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 B5mo 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
sonandmjy5mo 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
sbkl5mo ago
wonder if it is not because of the hot reload
sonandmjy
sonandmjy5mo ago
I think I found the error message in the sync websocket but idk how to fix it hmm
No description
Perfect
Perfect5mo 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
sbkl5mo 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 B5mo 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
ballingt5mo 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
sbkl5mo 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 B5mo 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
sbkl5mo 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
Perfect4mo 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
Noss4mo 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
Perfect4mo 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
Noss4mo ago
yeah, I also had the problem that the backend got called and I received unauthorized. Very weird
sbkl
sbkl4mo 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
Niklas4mo 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
ballingt4mo 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
Niklas4mo 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
ballingt4mo 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
mwoof4mo 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
mwoof4mo 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
ballingt4mo 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
mwoof4mo 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
ballingt4mo 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
Noss4mo 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
Perfect4mo ago
Did they mention if anything official with it was coming soon? Thanks for looking into it!
Noss
Noss4mo 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
Perfect4mo 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
PushPull4mo 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
sonandmjy4mo 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
Noss4mo 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
Para4mo 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
Perfect4mo 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
wodka4mo 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
ballingt4mo ago
Hey all, WorkOS is now mentioned in the Convex auth docs with a guide https://docs.convex.dev/auth/authkit
Perfect
Perfect4mo ago
Lets goo! Thanks @ballingt
ballingt
ballingt4mo 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
Perfect4mo ago
Is there a simple explanation for why we need both providers in auth config? Seems only difference is the issuer url?
ballingt
ballingt4mo 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
Noss4mo ago
Thanks a lot for the update! I will rework my current implementation
ballingt
ballingt4mo ago
should be issuer: "https://api.workos.com/" for the first one, no client_id at the end
ether
ether4mo ago
this is awesome! cc: @amianthus
ballingt
ballingt4mo ago
fix coming in 5 min
sonandmjy
sonandmjy4mo 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
ballingt4mo ago
OK fixed, this should be accurate now And thanks @nicknisi for doing all the work here!
Perfect
Perfect4mo 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
mwoof4mo ago
@ballingt you dog! This is awesome. Implementing now
Para
Para4mo ago
@ballingt awesome work! My backend is nuxt. Let me try to see if it works on nuxt
ballingt
ballingt4mo 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
Perfect4mo ago
Interesting, maybe a nextjs specific issue
ballingt
ballingt4mo ago
I'm sure we can fix it, we'll get there
Rishad B
Rishad B4mo 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
Perfect4mo 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 B4mo 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
Perfect4mo 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
wodka4mo 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
ballingt4mo 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
ballingt4mo 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
ballingt4mo 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
Perfect4mo 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
ballingt4mo 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
Perfect4mo ago
Ah ok gotcha And last thing I noticed
ballingt
ballingt4mo 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
Perfect4mo ago
What controls the Ui in your header auth?
Perfect
Perfect4mo 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
ballingt4mo 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
ballingt4mo 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
Perfect4mo 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
ballingt4mo 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
Perfect4mo 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
ballingt4mo 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
Perfect4mo 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
ballingt4mo ago
ah yeah you would think so! But apparently not
No description
Perfect
Perfect4mo 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
ballingt4mo 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
Perfect4mo 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 B4mo ago
@ballingt Is there any way we can push workos to get a react native/expo repo too?
ballingt
ballingt4mo 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
wodka4mo 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
Perfect4mo ago
This might be the issue I was having? Try adding || !accessToken to your loading boolean in provider
makisuo
makisuo4mo 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
wodka4mo ago
that actually fixed it - now there are no more strange logout for a split second!! Working for me! thx @Perfect and @Tom
Perfect
Perfect4mo 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
Hamburger-Fries4mo ago
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
Perfect4mo 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
Hamburger-Fries4mo ago
So you think for now just go with WorkOS?
ether
ether4mo ago
I genuinely just switched to convex Auth for a bit and am planning to switch to either workos or Kinde soon
ether
ether4mo 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 B4mo 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
ballingt4mo 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 B4mo 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
Perfect4mo ago
What package will be updated?
ballingt
ballingt4mo 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 B4mo ago
Where can I see websocket messages? verbose: true isn't doing anything different for me
ballingt
ballingt4mo ago
there should be a lot more console.log messages
Perfect
Perfect4mo 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
ballingt4mo ago
you might be SSRing already if you're using preloadQuery?
Perfect
Perfect4mo 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
ballingt4mo ago
It should be rendering, but then it disappears for a moment (that flash)
Perfect
Perfect4mo ago
The flash being the loading state with auth getting all synced up?
Perfect
Perfect4mo ago
Ahhh ok Awesome stuff
ballingt
ballingt4mo 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 B4mo ago
Sorry used it in a query, not mutation
ballingt
ballingt4mo ago
Does the query work later? Just not at first?
Rishad B
Rishad B4mo 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
ballingt4mo 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 B4mo 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
Perfect4mo 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
ballingt4mo 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 B4mo ago
Hmm maybe I'm using preloadQuery wrong
ballingt
ballingt4mo 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
Perfect4mo ago
Preload is for getting the data on the server so it’s ready when it reaches the client @Rishad B
Rishad B
Rishad B4mo 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
ballingt4mo ago
Yeah good point, I have it in my local verison but haven't made a PR
Perfect
Perfect4mo 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
Perfect4mo ago
We can add a WorkOS tab here
ballingt
ballingt4mo 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 B4mo 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 B4mo ago
Yes please!
Perfect
Perfect4mo 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
ballingt4mo 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
Hamburger-Fries4mo ago
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 B3mo ago
Are you using those for RBAC alongside Convex?
Hamburger-Fries
Hamburger-Fries3mo ago
Yes - so far so great. Mostly just RBAC and ReBAC for now.
Rishad B
Rishad B3mo ago
Cool. I use WorkOS roles and then hardcode permissions though I could use theirs too.
ballingt
ballingt3mo ago
@Perfect check out the experimental expectAuth: true option in convex@1.26.0-alpha.9, re the flash conversation we were having
Perfect
Perfect3mo ago
Oh sweet, definitely will maybe later tonight Thanks for letting me know
wodka
wodka3mo 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
Para3mo ago
Can I use the convex workos docs for react for Tanstack start setup?
Perfect
Perfect3mo ago
did this end up shipping? have been away from laptop for a few days
ballingt
ballingt3mo ago
This is still only in the alpha, new client release tomorrow at this point
wodka
wodka3mo ago
saw the release commit for 1.26.0 but cannot get the package anywhere^^
ballingt
ballingt3mo ago
not out yet, testing it; you can do with convex@1.26.0-alpha.12 if you like
Perfect
Perfect3mo 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
sbkl3mo 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
ballingt3mo ago
Hm I have not thought about auth with the TanStack Query wrapper; how does it handle auth I wonder.
sbkl
sbkl3mo ago
I can confirm the error happens even with the standard useQuery from convex. Just reproduced it.
ballingt
ballingt3mo 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
sbkl3mo ago
let me try and feedback
ballingt
ballingt3mo 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
sbkl3mo 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
Noss3mo ago
I see this: { "type": "Authenticate", "tokenType": "None", "baseVersion": 1 } in the sync always when this happens
Para
Para3mo 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?
mwoof
mwoof3mo ago
Even with template-convex-nextjs-authkit, There seem to be errors with authed queries. Client side using the WorkOS "useAuth" hook works every time.
'use client';
import { useAuth } from '@workos-inc/authkit-nextjs/components';
export default function Page() {
const { user, signOut } = useAuth();
return <pre>{JSON.stringify(user, null, 2)}</pre>
}
'use client';
import { useAuth } from '@workos-inc/authkit-nextjs/components';
export default function Page() {
const { user, signOut } = useAuth();
return <pre>{JSON.stringify(user, null, 2)}</pre>
}
But adding auth into a convex queries breaks things. Here is a convex query
export const getUser: ReturnType<typeof query> = query({
handler: async (ctx) => {
const user = await ctx.auth.getUserIdentity();
const workos_user_id = user?.subject;
if (!workos_user_id) throw new Error('User not authenticated');
return await ctx.runQuery(internal.users.getByWorkOSId, { workos_id: workos_user_id });
},
});
export const getUser: ReturnType<typeof query> = query({
handler: async (ctx) => {
const user = await ctx.auth.getUserIdentity();
const workos_user_id = user?.subject;
if (!workos_user_id) throw new Error('User not authenticated');
return await ctx.runQuery(internal.users.getByWorkOSId, { workos_id: workos_user_id });
},
});
Using this query sever side , everything works as expected. User is displayed correctly every time.
import { preloadQuery, preloadedQueryResult } from 'convex/nextjs';
import { api } from '@/convex/_generated/api';
import { withAuth } from '@workos-inc/authkit-nextjs';
export default async function Page() {
const { accessToken } = await withAuth();
const preloaded = await preloadQuery(api.users.getUser, {}, { token: accessToken });
const data = preloadedQueryResult(preloaded);
return <pre>{JSON.stringify(data, null, 2)}</pre>
}
import { preloadQuery, preloadedQueryResult } from 'convex/nextjs';
import { api } from '@/convex/_generated/api';
import { withAuth } from '@workos-inc/authkit-nextjs';
export default async function Page() {
const { accessToken } = await withAuth();
const preloaded = await preloadQuery(api.users.getUser, {}, { token: accessToken });
const data = preloadedQueryResult(preloaded);
return <pre>{JSON.stringify(data, null, 2)}</pre>
}
Using it on the client, getUserIdentity returns null This happens every time in prod and on the first load in dev (refreshing gets around it in dev)
'use client';
import { useQuery } from 'convex/react';
import { api } from '@/convex/_generated/api';
export default function Page() {
const user = useQuery(api.users.getUser, {});
return <pre>{JSON.stringify(user, null, 2)}</pre>
}
'use client';
import { useQuery } from 'convex/react';
import { api } from '@/convex/_generated/api';
export default function Page() {
const user = useQuery(api.users.getUser, {});
return <pre>{JSON.stringify(user, null, 2)}</pre>
}
A hybrid approach (preloadQuery then usePreloadedQuery) correctly loads the page loads but breaks when usePreloadedQuery runs (when api.users.getUser is run from the client). There has been a TON of progress made on the WorkOS integration, so thank you @ballingt & @nicknisi for that!
ballingt
ballingt3mo ago
OK if I understand this right this is a known issue, you're using this approach of no <Authenticated> and also no expectAuth right? I'm creating an example separate from the template so we can try this stuff out
mwoof
mwoof3mo ago
Correct. I am not using <Authenticated> or <expectAuth>. I will try expectAuth I am currently doing the exact same thing (starting with the template and stripping out peices until I can find the step that is broken). Notes on docs - I dont think this is true: "WorkOS API Key for production follows the format sklive...", only the staging key has a prefix (sktest...) Notes on template-convex-nextjs-authkit readme: - You might remove the blurb about "npx convex auth add workos" (convex auth commands were removed, see https://docs.convex.dev/auth for up to date instructions)
ballingt
ballingt3mo ago
thank you! things are in flux right now, I'm not sure that expectAuth will be reccommened yet I'm seeing that I don't need expectAuth, hold up and I'll get this in productino to confirm @mwoof ok I can repro, thanks for pointing this out! https://workos-convex-nextjs-testbed.previews.convex.dev/server
mwoof
mwoof3mo ago
lol I was having trouble reproducing... glad I am not going crazy. is it just in prod for you?
Perfect
Perfect3mo ago
Hey Tom is there a list of known issues or something of that sort out there right now? I’m getting confused on what works and what’s broken now
ballingt
ballingt3mo ago
I'm going to use https://github.com/get-convex/workos-convex-nextjs-testbed as a place to show these techniques while keeping the template simpler let me document the existing techniques a bit, I believe that (protected routes + expectAuth) and client-side are the only methods that work right now and there are some reports of getting logged off when coming back to a suspended laptop so I ant to try to repro those too I've updated the README here https://github.com/get-convex/workos-convex-nextjs-testbed to describe the current status @Perfect, most changes are coming soon but this repo now shows the two approaches I'm testing
mwoof
mwoof2mo ago
Ok, I am able to reproduce now (no expectAuth or <Authenticated> component) and this is starting to smell like a racing condition... or at least thats my best guess. I get intermittent (in dev) and full blown (in prod) "Not Authenticated" convex function errors when ConvexClientProvider is not the the root component (even if that something-outside-the-ConvexClientProvider has nothing to do with convex). For example, this works without errors:
export default async function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body className="antialiased flex flex-col min-h-screen">
<ConvexClientProvider>
<ThemeProvider attribute="class">
{children}
</ThemeProvider>
</ConvexClientProvider>
</body>
</html>
);
}
export default async function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body className="antialiased flex flex-col min-h-screen">
<ConvexClientProvider>
<ThemeProvider attribute="class">
{children}
</ThemeProvider>
</ConvexClientProvider>
</body>
</html>
);
}
but this become buggy
export default async function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body className="antialiased flex flex-col min-h-screen">
<ThemeProvider attribute="class">
<ConvexClientProvider>
{children}
</ConvexClientProvider>
</ThemeProvider>
</body>
</html>
);
}
export default async function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body className="antialiased flex flex-col min-h-screen">
<ThemeProvider attribute="class">
<ConvexClientProvider>
{children}
</ConvexClientProvider>
</ThemeProvider>
</body>
</html>
);
}
Thanks for putting that repo together @Tom! My vote for potential features would be for Not specifying { expectAuth: true }. Obviously easier said than done, but only worrying about auth state in the query/mutation with ctx.auth.getUserIdentity() seems to be the most intuitive solution and (correct me if I'm wrong) how the rest of the auth integrations work.
ballingt
ballingt2mo ago
@mwoof I agree, I added a repro here https://workos-convex-nextjs-testbed.previews.convex.dev/server-does-not-work (code) This is not an approach that works
mwoof
mwoof2mo ago
Got it. So just to make sure I understand, the current state of authentication is: - Convex Auth and Auth0: ctx.auth.getUserIdentity() works for all client mutations, client queries and preloaded queries (as long as { token } is passed on the server). - WorkOS and Clerk: Preloaded queries work the same way, but client mutations/quires require at least one of the following due to convex requesting authentication state before the provider can provide it: - expectAuth to be set to true when initiating ConvexReactClient . The downside is that (A) auth will need to load before ANY client side query and (B) it does not handle switching tokens very well. A. There is negligible performance loss if you are using preloaded queries and/or your app does not have any unauthenticated pages. B. This could cause issues if the user switched their WorkOS organization (or performed some other token invalidating task) - Use <Authenticated>to wrap client components that have authenticated queries /mutations. This only renders the the component when the client has an authenticated state (negligible performance loss if you are using preloaded queries) You are currently working on: - Passing a token from the server: - does this opt these routes in to dynamic rendering? - Yes... but could you make passing the token optional? If the token is passed through the preloaded prop to the usePreloadedQuery hook, then the hook could fallback to the preloaded token until the client side auth state is established. This removes the need for expectAuth or <Authenticated> and adds no extra syntax to the user (unless they opt in/out of passing). - Not specifying { expectAuth: true }: Which would require solving the racing issue and make the WorkOS Authkit implementation mirror Convex Auth. - should a special argument to useQuery() hooks annotate whether they require auth? Opt-int could go the other way. (see get-convex/convex-js#69) - This could work... but seems a little tedious. apps range from fully authed to no auth and you will always be adding more work for someone.... That being said it could pair nicely with optional preloaded token passing.
ballingt
ballingt2mo ago
There should be difference between WorkOS and Clerk here, for both you need to use <Authenticated>. Hm I don't think there's any difference between WorkOS and Clerk here, but I will look into it. Regardless of auth provider, Clerk or WorkOS or Convex Auth or Better Auth, ctx.auth.getUserIdentity() works in all mutations, queries and actions. For all of these you need to use <Authenticated>. There are some experiments happening with expectAuth. I think everything you've said is correct, but I don't think about it in terms of differences between providers here; the differences are about how the Next.js libraries work.
Para
Para2mo ago
Can we have an updated docs once all these are ironed out? It's too complicated for beginner like me 🙁
ballingt
ballingt2mo ago
Absolutely, hang in there for something simpler.
Noss
Noss2mo ago
I dont know if this issue is a priority at the moment but the more I am developing with WorkOS and Convex the more annoying it gets. The constant refresh of the whole application because Auth is gone and then instanly valid again. If there is a way to help or get this going, let me know 😄
ballingt
ballingt2mo ago
What are you seeing, are you using one of the methods shown in https://github.com/get-convex/workos-convex-nextjs-testbed? Can you get this behavior with https://workos-convex-nextjs-testbed.previews.convex.dev/, and if not could you open a PR with what you're doing so I can repro the issue?
Noss
Noss2mo ago
I basically have:
<AuthKitProvider>
<DynamicBackground>
<div className="flex flex-col min-h-screen">
<Header />
<main className="flex-1">
<ConvexClientProvider>
{children}
</ConvexClientProvider>
</main>
<Footer />
</div>
</DynamicBackground>
</AuthKitProvider>
<AuthKitProvider>
<DynamicBackground>
<div className="flex flex-col min-h-screen">
<Header />
<main className="flex-1">
<ConvexClientProvider>
{children}
</ConvexClientProvider>
</main>
<Footer />
</div>
</DynamicBackground>
</AuthKitProvider>
With:
'use client';

import { ReactNode, useCallback, useRef } from 'react';
import { ConvexReactClient } from 'convex/react';
import { ConvexProviderWithAuth } from 'convex/react';
import { useAuth, useAccessToken } from '@workos-inc/authkit-nextjs/components';

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

export default function ConvexClientProvider({ children }: { children: ReactNode }) {
return (
<ConvexProviderWithAuth client={convex} useAuth={useAuthFromAuthKit}>
{children}
</ConvexProviderWithAuth>
);
}

function useAuthFromAuthKit() {
const { user, loading: isLoading } = useAuth();
const { accessToken, loading: tokenLoading, error: tokenError } = useAccessToken();

const stableAccessToken = useRef<string | null>(null);
if (accessToken && !tokenError) {
stableAccessToken.current = accessToken;
}

const authenticated = !!user && (!!stableAccessToken.current || (!!accessToken && !tokenError));
const loading = (isLoading ?? false) || (tokenLoading ?? false);

const fetchAccessToken = useCallback(async () => {
if (stableAccessToken.current && !tokenError) {
return stableAccessToken.current;
}
return null;
}, [tokenError]);

return {
isLoading: loading,
isAuthenticated: authenticated,
fetchAccessToken,
};
}
'use client';

import { ReactNode, useCallback, useRef } from 'react';
import { ConvexReactClient } from 'convex/react';
import { ConvexProviderWithAuth } from 'convex/react';
import { useAuth, useAccessToken } from '@workos-inc/authkit-nextjs/components';

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

export default function ConvexClientProvider({ children }: { children: ReactNode }) {
return (
<ConvexProviderWithAuth client={convex} useAuth={useAuthFromAuthKit}>
{children}
</ConvexProviderWithAuth>
);
}

function useAuthFromAuthKit() {
const { user, loading: isLoading } = useAuth();
const { accessToken, loading: tokenLoading, error: tokenError } = useAccessToken();

const stableAccessToken = useRef<string | null>(null);
if (accessToken && !tokenError) {
stableAccessToken.current = accessToken;
}

const authenticated = !!user && (!!stableAccessToken.current || (!!accessToken && !tokenError));
const loading = (isLoading ?? false) || (tokenLoading ?? false);

const fetchAccessToken = useCallback(async () => {
if (stableAccessToken.current && !tokenError) {
return stableAccessToken.current;
}
return null;
}, [tokenError]);

return {
isLoading: loading,
isAuthenticated: authenticated,
fetchAccessToken,
};
}
And in the pages I use:`
const { user } = await withAuth();
const { user } = await withAuth();
I updated the workos nextjs package as well as my convex client to what you provided and will test if that made a change
Divyam Chandel
Divyam Chandel2mo ago
https://github.com/chandeldivyam/docufy/blob/main/apps/dashboard/app/dashboard/layout.tsx I keep seeing Authenticating... On the home page, we have the user information from workos and hence we see Dashboard instead of sign in This seems to work in development as expected. A user comes on the home page, they are able to sign in and a user is created in convex via the webhook. They can click on dashboard and gets redirected inside. On prod, it doesn't go to <Authenticated> route at all. Deployed on vercel with all these. Steps to reproduce: go to https://www.trydocufy.com and use a temp email to create a new account. (avoid using sign in with google as its in test mode) There aren't any logs in convex logs either for any requests. Why does it work correctly in dev?
No description
No description
No description
Niklas
Niklas2mo ago
@ballingt To easier repro the issue, you might also set the Access token duration to a few minutes in the WorkOS dashboard. The unauthenticated state will be visible shortly, in between the access token refreshes.
No description
ballingt
ballingt2mo ago
Can you repro this issue on the testbed link above? That has always had its access token duration set to 1 minute
ballingt
ballingt2mo ago
@Noss thank you! Once we have seen it on the latest it's easier to ask WorkOS to fix it, opening an issue on https://github.com/workos/template-convex-nextjs-authkit would be great once we have a way to repro with https://workos-convex-nextjs-testbed.previews.convex.dev/
GitHub
GitHub - workos/template-convex-nextjs-authkit
Contribute to workos/template-convex-nextjs-authkit development by creating an account on GitHub.
Create Next App
Generated by create next app
ballingt
ballingt2mo ago
Cna you try the patterns in https://workos-convex-nextjs-testbed.previews.convex.dev/ / https://github.com/get-convex/workos-convex-nextjs-testbed to see if they work? There's a known issue in the template right now
Niklas
Niklas2mo ago
I will try 🙂
Noss
Noss2mo ago
I changed to the new convex client provider and it actually seems to work with no rerenders. Will keep you updated.
Clever Tagline
Clever Tagline2mo ago
I think you've accidentally hijacked another conversation. I don't see anything in your question related to the topic of this thread (WorkOS auth issues). Please copy your post text and start a new thread, then delete this post.
ballingt
ballingt2mo ago
I deleted and copied this to https://gist.github.com/thomasballinger/dcea7f0f64d8aaf3186ea4d9f5fc18cd so you have it if you need it @Calebe Aires
Perfect
Perfect2mo ago
Is the template all up to date at this point in time? @Noss @mwoof Are you guys still having any issues? I tried going back and reading all the discussions that happened here but Im a bit confused where the convex integration stands right now. What is broken, what works, what pattern is recommended, etc... Also, did the docs get updated or anything from what you can tell @Para ?
Noss
Noss2mo ago
No with the changes I described on the 16.09 it seems to work and there are no issues
ballingt
ballingt2mo ago
The templates should be up to date, and the only documented issue is - after a failed-to-fetch error you get logged out https://github.com/workos/template-convex-nextjs-authkit/issues/14 https://github.com/get-convex/workos-convex-nextjs-testbed/issues/1 (I think these are the same) There's also a race when doing server-side rendering that is rarer now but I think still exists, would love info about this. I've been doing integration work (try the new WorkOS CLI flow where it automaticlaly provsions an account / environment for you!) for a while instead of this client stuff, will be coming back to client issues later, but would be great to have repros
Noss
Noss2mo ago
Quick question... it is working for me but when I do it like this I got: Module '"convex/react"' has no exported member 'ConvexProviderWithAuth'.ts(2305) But it is working!? So the type is just missing? I am on the same package version
Antihero
Antihero2mo ago
@ballingt What do you think about implementing logic to catch the failed-to-fetch client error when a user returns to the website and automatically reloads it for them? Is that even possible?
ballingt
ballingt2mo ago
The first thing I want to do is catch that error and retry a few times; if that fetch errors, there's typically plenty of time to retry. But then if that keeps happening, yes, totally we can have an option to reload / redirect the page if convex finds it's logged out. I don't want to be the default because of course we want this as robust as possible, but allowing configuring what happens when the client gets logged out makes sense. will look at this by Monday, or this weekend if I get to it
Perfect
Perfect2mo ago
@ballingt - What provider should we use? The one in the workos template or in the convex docs? https://www.diffchecker.com/05ANesXG/ (link to diff) Also, are there docs yet for what eagerAuth does? And lastly - I saw this announcement - I think I have done everything the CLI would be doing for me based off the list provided and the template + convex docs. Is there anything existing projects could be missing that is recommended? I see nextjs does not get CORS configured? https://workos.com/blog/convex-authkit Thanks!
Para
Para2mo ago
Tanstack start + authkit + convex coming soon?
Perfect
Perfect2mo ago
Bumping this for my prior two messages
Antihero
Antihero2mo ago
@ballingt I noticed that due to constant refresh, in some cases, it triggers a 429 error from Workos using this logic: "https://github.com/workos/template-convex-nextjs-authkit/blob/main/components/ConvexClientProvider.tsx". Any advice on how to fix this issue? Is that a problem with eagerAuth: true (https://github.com/workos/template-convex-nextjs-authkit/blob/main/middleware.ts)?
No description
JCo
JCo2mo ago
Am I missing something because the authkit stuff says that it needs authkit-react not authkit-nextjs and the nextjs example doesn't work for me, if I follow the docs exactly, it ends up letting you sign in and everything, but the authenticated and unauthenticated components from convex/react dont change to the authenticated state.
Perfect
Perfect2mo ago
Are you following the convex docs? I believe it needs to be updated, the provider is not correct there and there are some new undocumented things like eagerAuth
JCo
JCo2mo ago
Yeah the convex docs. ChatGPT wants to use them biblically, context7 still uses them... And then workos is having a field day trying to do webhook syncing, this project is a pain. I think i need to figure out an apropriate way to sync user extId and email to a user record in my db (migrating from clerk + convex) without using webhooks, but idk how to do that with their api as they show that using node not next.
Niklas
Niklas2mo ago
@ballingt After using the ConvexClientProvider from the template-convex-nextjs-authkit, the token refresh is working properly without a page reload. Thank you very much 🎉
erquhart
erquhart4w ago
As this thread has become sort of a general how-to-use-workos thread, I want to recommend further messages go in the #workos channel for visibility.

Did you find this page helpful?