Tanstack Start with Clerk and Convex
My current setup you can see on screen.
All works fine except when I try to open webpage from bookmark like
/dashboard/transactions/incomes
on first render (I was logged) I'm getting error shown on image.
Most important code parts:
// router.tsx
{* ... file content ... *}
const router = routerWithQueryClient(
createTanStackRouter({
routeTree,
defaultPreload: "intent",
defaultErrorComponent: DefaultCatchBoundary,
defaultNotFoundComponent: () => <NotFound />,
context: { queryClient },
Wrap: ({ children }) => (
<ClerkProvider
publishableKey={(import.meta as any).env.VITE_CLERK_PUBLISHABLE_KEY!}
>
<ConvexProviderWithClerk
client={convexQueryClient.convexClient}
useAuth={useAuth}
>
{children}
</ConvexProviderWithClerk>
</ClerkProvider>
),
scrollRestoration: true,
}),
queryClient,
);
{* ... file content ... *}
__root.tsx
const fetchClerkAuth = createServerFn({ method: "GET" }).handler(async () => {
const { userId } = await getAuth(getWebRequest()!);
return {
userId,
};
});
export const Route = createRootRouteWithContext<{
queryClient: QueryClient;
}>()({
beforeLoad: async (ctx) => {
const { userId } = await fetchClerkAuth();
return {
userId,
};
},
component: RootComponent,
});
_authed.tsx
export const Route = createFileRoute("/_authed")({
beforeLoad: ({ context }) => {
if (!context.userId) {
throw new Error("Not authenticated");
}
},
errorComponent: SOME_ERROR_COMPONENT,
});
_authed/dashboard/transactions/incomes
export const Route = createFileRoute('/_authed/dashboard/transactions/incomes')(
{
loader: ({ context: { queryClient } }) =>
queryClient.ensureQueryData({
...convexQuery(
api.transactions.getIncomeTransactionsWithCategories,
{},
),
}),
component: RouteComponent,
},
)
// router.tsx
{* ... file content ... *}
const router = routerWithQueryClient(
createTanStackRouter({
routeTree,
defaultPreload: "intent",
defaultErrorComponent: DefaultCatchBoundary,
defaultNotFoundComponent: () => <NotFound />,
context: { queryClient },
Wrap: ({ children }) => (
<ClerkProvider
publishableKey={(import.meta as any).env.VITE_CLERK_PUBLISHABLE_KEY!}
>
<ConvexProviderWithClerk
client={convexQueryClient.convexClient}
useAuth={useAuth}
>
{children}
</ConvexProviderWithClerk>
</ClerkProvider>
),
scrollRestoration: true,
}),
queryClient,
);
{* ... file content ... *}
__root.tsx
const fetchClerkAuth = createServerFn({ method: "GET" }).handler(async () => {
const { userId } = await getAuth(getWebRequest()!);
return {
userId,
};
});
export const Route = createRootRouteWithContext<{
queryClient: QueryClient;
}>()({
beforeLoad: async (ctx) => {
const { userId } = await fetchClerkAuth();
return {
userId,
};
},
component: RootComponent,
});
_authed.tsx
export const Route = createFileRoute("/_authed")({
beforeLoad: ({ context }) => {
if (!context.userId) {
throw new Error("Not authenticated");
}
},
errorComponent: SOME_ERROR_COMPONENT,
});
_authed/dashboard/transactions/incomes
export const Route = createFileRoute('/_authed/dashboard/transactions/incomes')(
{
loader: ({ context: { queryClient } }) =>
queryClient.ensureQueryData({
...convexQuery(
api.transactions.getIncomeTransactionsWithCategories,
{},
),
}),
component: RouteComponent,
},
)


2 Replies
flat-fuchsiaOP•8mo ago
Convex part of code
export async function getCurrentUserOrThrow(ctx: QueryCtx) {
const userRecord = await getCurrentUser(ctx);
if (!userRecord) throw new Error("Can't get current user");
return userRecord;
}
export async function getCurrentUser(ctx: QueryCtx) {
const identity = await ctx.auth.getUserIdentity();
if (identity === null) {
return null;
}
return await userByExternalId(ctx, identity.subject);
}
async function userByExternalId(ctx: QueryCtx, clerkId: string) {
return await ctx.db
.query("users")
.withIndex("by_clerkId", (q) => q.eq("clerkId", clerkId))
.unique();
}
export async function getCurrentUserOrThrow(ctx: QueryCtx) {
const userRecord = await getCurrentUser(ctx);
if (!userRecord) throw new Error("Can't get current user");
return userRecord;
}
export async function getCurrentUser(ctx: QueryCtx) {
const identity = await ctx.auth.getUserIdentity();
if (identity === null) {
return null;
}
return await userByExternalId(ctx, identity.subject);
}
async function userByExternalId(ctx: QueryCtx, clerkId: string) {
return await ctx.db
.query("users")
.withIndex("by_clerkId", (q) => q.eq("clerkId", clerkId))
.unique();
}
export const getIncomeTransactionsWithCategories = query({
args: {},
async handler(ctx, args) {
const user = await getCurrentUserOrThrow(ctx);
// Fetch all transactions for the user in one query
const transactions = await ctx.db
.query("transactions")
.withIndex("by_userId", (q) => q.eq("userId", user._id))
.filter((q) => q.eq(q.field("type"), "income"))
.collect();
if (transactions.length === 0) {
return [];
}
// Get unique category IDs from transactions
const categoryIds = [...new Set(transactions.map((t) => t.categoryId))];
// Fetch all relevant categories in one query
const categories = await ctx.db
.query("categories")
.filter((q) => q.or(...categoryIds.map((id) => q.eq(q.field("_id"), id))))
.collect();
// Create a map of categories for faster lookups
const categoryMap = new Map(
categories.map((category) => [category._id, category]),
);
// Join transactions with categories in memory
return transactions.map((transaction) => ({
...transaction,
category: categoryMap.get(transaction.categoryId),
}));
},
});
export const getIncomeTransactionsWithCategories = query({
args: {},
async handler(ctx, args) {
const user = await getCurrentUserOrThrow(ctx);
// Fetch all transactions for the user in one query
const transactions = await ctx.db
.query("transactions")
.withIndex("by_userId", (q) => q.eq("userId", user._id))
.filter((q) => q.eq(q.field("type"), "income"))
.collect();
if (transactions.length === 0) {
return [];
}
// Get unique category IDs from transactions
const categoryIds = [...new Set(transactions.map((t) => t.categoryId))];
// Fetch all relevant categories in one query
const categories = await ctx.db
.query("categories")
.filter((q) => q.or(...categoryIds.map((id) => q.eq(q.field("_id"), id))))
.collect();
// Create a map of categories for faster lookups
const categoryMap = new Map(
categories.map((category) => [category._id, category]),
);
// Join transactions with categories in memory
return transactions.map((transaction) => ({
...transaction,
category: categoryMap.get(transaction.categoryId),
}));
},
});
flat-fuchsiaOP•8mo ago
I think is related to this one:
https://docs.convex.dev/auth/debug#ctxauthgetuseridentity-returns-null-in-a-query
But no idea how to fix it.
Debugging Authentication | Convex Developer Hub
You have followed one of our authentication guides but something is not working.