NazCodeland
NazCodeland3mo ago

Queries Invalidated on Auth Token Refresh (Clerk)

Hey, I’m running into a problem with Convex query invalidation. Whenever my auth token is refreshed (I’m using Clerk, which refreshes every ~50 seconds as described: https://clerk.com/docs/how-clerk-works/overview#token-refresh-mechanism, Convex seems to invalidate and re-run queries that depend on authentication and also a query that doesn't depend on authentication—even when no data has changed and no mutations have occurred. - My queries use ctx.auth - I’m calling client.setAuth(fetchToken, handleAuthChange) in my Svelte app. All query invalidations are showing (cached) in the dashboard logs. However, from what I understand, even cached queries have a cost, although less than none-cached queries. However, I would like to avoid this altogether if possible since all these additional function calls will incur a cost and lead to additional bandwidth costs. I'm 100% sure this is being caused by client.setAuth(fetchToken, handleAuthChange); because when I comment this line out (after it has been set once) and save. The queries are no longer getting invalidated. I’ve gone through this GitHub issue: https://github.com/get-convex/convex-backend/issues/95 and this Discord thread https://discord.com/channels/1019350475847499849/1374166494019059822/1374166494019059822, where auth token causing query invalidation was mentioned but in the rest of the conversation it didn’t really come up again.
9 Replies
Convex Bot
Convex Bot3mo ago
Thanks for posting in <#1088161997662724167>. Reminder: If you have a Convex Pro account, use the Convex Dashboard to file support tickets. - Provide context: What are you trying to achieve, what is the end-user interaction, what are you seeing? (full error message, command output, etc.) - Use search.convex.dev to search Docs, Stack, and Discord all at once. - Additionally, you can post your questions in the Convex Community's <#1228095053885476985> channel to receive a response from AI. - Avoid tagging staff unless specifically instructed. Thank you!
NazCodeland
NazCodelandOP3mo ago
In that thread, Jamie asked the following questions to help narrow the problem down (I’m not sure if they apply here, but sharing in case it’s helpful): - Q: Can you share the code in the query?
A: I can if applicable, but it’s many queries that are getting invalidated. - Q: And these are all the same logged in user in the logs?
A: Yes. - Q: And none of these libraries rely on randomness?
A: n/a - Q: Final question -- no code changes in between as well, right? Any code change invalidates all the caches.
A: No code changes. - Q: How big are these results, btw? Like # of KB. And is this the cloud product, or self-hosted image?
A: a) Small for now but it’s in the development stage, in production if it gets users it will add up.
b) Cloud product. - Q: Also, free account or convex pro? Just trying to rule out cache eviction
A: Free account. Thanks in advance. Hmm, as soon as I posted this, I got a new idea about what might be causing the issue. Please ignore this for now—I’ll follow up if it turns out my suspicion isn’t correct. Just got back and my conclusion from earlier is correct, everytime setAuth is called, the queries invalidate. Since I am in a Svelte/SvelteKit application, I am calling setAuth like this:
<script lang="ts">
import { useConvexClient } from 'convex-svelte';
import { useClerkContext } from 'svelte-clerk';
import { api } from '../convex/_generated/api.js';
import { userState } from '$lib/state/userState.svelte.js';
import { getLocalStorage, updateLocalStorage } from '$lib/utils/localStorage.js';
import type { AppState } from '$lib/types.js';
import { untrack } from 'svelte';

const client = useConvexClient();
const ctx = useClerkContext();

$effect(() => {
// unauthenticated (not a clerk user) user refreshing page
if (untrack(() => !ctx.auth.userId)) {
client
.query(api.queries.getUserBySessionId, { sessionId: userState.sessionId })
.then((user) => {
if (user) userState.convexId = user._id;
});
}
});

async function fetchToken() {
try {
return await ctx.session?.getToken({
template: 'convex'
});
} catch (error) {
console.error('Error fetching token:', error);
return null;
}
}

export async function fetchNullToken(): Promise<null> {
return Promise.resolve(null);
}
<script lang="ts">
import { useConvexClient } from 'convex-svelte';
import { useClerkContext } from 'svelte-clerk';
import { api } from '../convex/_generated/api.js';
import { userState } from '$lib/state/userState.svelte.js';
import { getLocalStorage, updateLocalStorage } from '$lib/utils/localStorage.js';
import type { AppState } from '$lib/types.js';
import { untrack } from 'svelte';

const client = useConvexClient();
const ctx = useClerkContext();

$effect(() => {
// unauthenticated (not a clerk user) user refreshing page
if (untrack(() => !ctx.auth.userId)) {
client
.query(api.queries.getUserBySessionId, { sessionId: userState.sessionId })
.then((user) => {
if (user) userState.convexId = user._id;
});
}
});

async function fetchToken() {
try {
return await ctx.session?.getToken({
template: 'convex'
});
} catch (error) {
console.error('Error fetching token:', error);
return null;
}
}

export async function fetchNullToken(): Promise<null> {
return Promise.resolve(null);
}
async function ensureUserStoredOnce() {
const appState = getLocalStorage<AppState>('appState');
const tokenSet = appState?.tokenSet;

if (!tokenSet && ctx.auth.userId) {
updateLocalStorage('appState', { tokenSet: true });

// 1. Existing user by tokenIdentifier
// 2. Patch anonymous user with tokenIdentifier
// 3. Create new user entry
let user = await client.mutation(api.mutations.upgradeSessionUser, {
sessionId: userState.sessionId,
payload: userState.buildConvexUser()
});
userState.clerkId = ctx.auth.userId;
userState.convexId = user ? user._id : null;
}

// authenticated user (clerk user) and refreshing page
if (ctx.auth.userId && !userState.convexId) {
client.query(api.queries.getUserByToken, {}).then((user) => {
if (user) {
userState.clerkId = user.clerkId;
userState.convexId = user._id;
}
});
}
}

$effect(() => {
const isSignedIn = untrack(()=> !!ctx.auth.userId);
if (userState.firstSignIn && isSignedIn) {
userState.firstSignIn = false;
}

if (isSignedIn) {
client.setAuth(fetchToken, handleAuthChange);
} else {
client.setAuth(fetchNullToken);
}
});
</script>
async function ensureUserStoredOnce() {
const appState = getLocalStorage<AppState>('appState');
const tokenSet = appState?.tokenSet;

if (!tokenSet && ctx.auth.userId) {
updateLocalStorage('appState', { tokenSet: true });

// 1. Existing user by tokenIdentifier
// 2. Patch anonymous user with tokenIdentifier
// 3. Create new user entry
let user = await client.mutation(api.mutations.upgradeSessionUser, {
sessionId: userState.sessionId,
payload: userState.buildConvexUser()
});
userState.clerkId = ctx.auth.userId;
userState.convexId = user ? user._id : null;
}

// authenticated user (clerk user) and refreshing page
if (ctx.auth.userId && !userState.convexId) {
client.query(api.queries.getUserByToken, {}).then((user) => {
if (user) {
userState.clerkId = user.clerkId;
userState.convexId = user._id;
}
});
}
}

$effect(() => {
const isSignedIn = untrack(()=> !!ctx.auth.userId);
if (userState.firstSignIn && isSignedIn) {
userState.firstSignIn = false;
}

if (isSignedIn) {
client.setAuth(fetchToken, handleAuthChange);
} else {
client.setAuth(fetchNullToken);
}
});
</script>
erquhart
erquhart2mo ago
I didn't read the whole article you linked but 60 seconds is a very short jwt expiration. 15 minutes or an hour are more typical. In any case, the auth info is input to your queries, so there's no way around rerunning them if the token changes as you risk unauthorized access to the data those queries return. I'd raise the token expiration if possible, personally.
NazCodeland
NazCodelandOP2mo ago
That makes sense. I wasn't able to figure out how to extend the 60 seconds token invalidation interval. The getToken() method accepts a getTokenOptions https://clerk.com/docs/references/ios/get-token#get-token-options
NazCodeland
NazCodelandOP2mo ago
No description
NazCodeland
NazCodelandOP2mo ago
I was hoping I could expirationBuffer a higher value but it the max is 60 seconds
erquhart
erquhart2mo ago
You can set expiration in the Clerk dashboard under jwt template
NazCodeland
NazCodelandOP2mo ago
I didn't know that, it''s set to 3600 which is an hour so hmm...I'll have to revisit this, currently working in another part of the code base, will update this thread when I revisit this. Thank you for the help
erquhart
erquhart2mo ago
Yeah that's odd, might want to ask about this in the Clerk discord

Did you find this page helpful?