gwilliamnn
gwilliamnn•2y ago

How can I use Clerk with Convex on NextJS13 app routes?

Any one can help? I saw the tutorial to use Clerk + Convex on Next13, but is using pages router, I tried, but not works on app router, says router is not ready.
33 Replies
ballingt
ballingt•2y ago
@gwilliamnn Where does it say that router is not ready?
gwilliamnn
gwilliamnnOP•2y ago
On the browser, let me post here all the code I used, one second!
ballingt
ballingt•2y ago
Here's our guide to using the App router, is this what you're using? https://docs.convex.dev/quickstart/nextjs Clerk's integration with App router is a little different, I could understand these being confusing to combine.
gwilliamnn
gwilliamnnOP•2y ago
here:
'use client';
import { ReactNode } from 'react';
import { ConvexProviderWithClerk } from 'convex/react-clerk';
import { ConvexReactClient } from 'convex/react';
import { ClerkProvider, useAuth } from '@clerk/nextjs';

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

export default function ConvexClientProvider({ children }: { children: ReactNode }) {
return (
<ClerkProvider>
<ConvexProviderWithClerk client={convex} useAuth={useAuth}>
{children}
</ConvexProviderWithClerk>
</ClerkProvider>
);
}
'use client';
import { ReactNode } from 'react';
import { ConvexProviderWithClerk } from 'convex/react-clerk';
import { ConvexReactClient } from 'convex/react';
import { ClerkProvider, useAuth } from '@clerk/nextjs';

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

export default function ConvexClientProvider({ children }: { children: ReactNode }) {
return (
<ClerkProvider>
<ConvexProviderWithClerk client={convex} useAuth={useAuth}>
{children}
</ConvexProviderWithClerk>
</ClerkProvider>
);
}
Context
import './globals.css';
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import ConvexClientProvider from '../context/ConvexClientProvider';

const inter = Inter({ subsets: ['latin'] });

export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app'
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang='en'>
<body className={inter.className}>
<ConvexClientProvider>{children}</ConvexClientProvider>
</body>
</html>
);
}
import './globals.css';
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import ConvexClientProvider from '../context/ConvexClientProvider';

const inter = Inter({ subsets: ['latin'] });

export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app'
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang='en'>
<body className={inter.className}>
<ConvexClientProvider>{children}</ConvexClientProvider>
</body>
</html>
);
}
Layout Error
gwilliamnn
gwilliamnnOP•2y ago
No description
gwilliamnn
gwilliamnnOP•2y ago
- error node_modules/.pnpm/next@13.4.12_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/client/router.js (146:14) @ useRouter
- error Error: NextRouter was not mounted. https://nextjs.org/docs/messages/next-router-not-mounted
null
- error node_modules/.pnpm/next@13.4.12_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/client/router.js (146:14) @ useRouter
- error Error: NextRouter was not mounted. https://nextjs.org/docs/messages/next-router-not-mounted
null
But! If instead use clerk nextjs, use clerk react, works...
'use client';
import { ReactNode } from 'react';
import { ConvexProviderWithClerk } from 'convex/react-clerk';
import { ConvexReactClient } from 'convex/react';
import { ClerkProvider, useAuth } from '@clerk/clerk-react';

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

export default function ConvexClientProvider({ children }: { children: ReactNode }) {
return (
<ClerkProvider publishableKey={process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY!}>
<ConvexProviderWithClerk client={convex} useAuth={useAuth}>
{children}
</ConvexProviderWithClerk>
</ClerkProvider>
);
}
'use client';
import { ReactNode } from 'react';
import { ConvexProviderWithClerk } from 'convex/react-clerk';
import { ConvexReactClient } from 'convex/react';
import { ClerkProvider, useAuth } from '@clerk/clerk-react';

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

export default function ConvexClientProvider({ children }: { children: ReactNode }) {
return (
<ClerkProvider publishableKey={process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY!}>
<ConvexProviderWithClerk client={convex} useAuth={useAuth}>
{children}
</ConvexProviderWithClerk>
</ClerkProvider>
);
}
This works fine...
ballingt
ballingt•2y ago
💯 thanks for the repro! Using clerk-react is the approach we've tested
gwilliamnn
gwilliamnnOP•2y ago
BTW, I LOVED CONVEX, for now on, its my favorite serveless backend 🙂 Nice, so, will be good to but this on documentation 🙂
ballingt
ballingt•2y ago
but wanting to use clerk-nextjs makes sense, I'll look into this
gwilliamnn
gwilliamnnOP•2y ago
Yeah, because I don't know what feature we will gonna miss but, another question, now, with this implementation, what change on the flow?
ballingt
ballingt•2y ago
We installing that library in step 6 of https://docs.convex.dev/auth/clerk but we can call it out specifically there
gwilliamnn
gwilliamnnOP•2y ago
owww, I saw now So, just tell me if I made the correct implementation here (its another thing) Like, I put a funciton to every new user create on clerk, create a profile on convex So, the profile quere/mutations:
import { query, mutation } from './_generated/server';
import { v } from 'convex/values';

export const getAll = query({
args: {},
handler: async (ctx) => {
return await ctx.db.query('profile').collect();
}
});

export const getByUserId = query({
args: {
userId: v.string()
},
handler: async (ctx, { userId }) => {
return await ctx.db
.query('profile')
.withIndex('byUserId', (q) => q.eq('userId', userId))
.first();
}
});

export const create = mutation({
args: {
userId: v.string(),
name: v.string(),
email: v.string(),
avatarUrl: v.optional(v.string())
},
handler: async (ctx, { userId, name, email, avatarUrl }) => {
return await ctx.db.insert('profile', {
userId,
name,
email,
avatarUrl
});
}
});

export const update = mutation({
args: {
userId: v.string(),
name: v.string(),
email: v.string(),
avatarUrl: v.optional(v.string())
},
handler: async (ctx, { userId, name, email, avatarUrl }) => {
const profile = await ctx.db
.query('profile')
.withIndex('byUserId', (q) => q.eq('userId', userId))
.first();

if (!profile) {
return await ctx.db.insert('profile', {
userId,
name,
email,
avatarUrl
});
}

return await ctx.db.patch(profile._id, {
userId,
name,
email,
avatarUrl
});
}
});

export const remove = mutation({
args: {
userId: v.string(),
},
handler: async (ctx, { userId }) => {
const profile = await ctx.db
.query('profile')
.withIndex('byUserId', (q) => q.eq('userId', userId))
.first();

if (!profile) {
return
}

return await ctx.db.delete(profile._id);
}
});
import { query, mutation } from './_generated/server';
import { v } from 'convex/values';

export const getAll = query({
args: {},
handler: async (ctx) => {
return await ctx.db.query('profile').collect();
}
});

export const getByUserId = query({
args: {
userId: v.string()
},
handler: async (ctx, { userId }) => {
return await ctx.db
.query('profile')
.withIndex('byUserId', (q) => q.eq('userId', userId))
.first();
}
});

export const create = mutation({
args: {
userId: v.string(),
name: v.string(),
email: v.string(),
avatarUrl: v.optional(v.string())
},
handler: async (ctx, { userId, name, email, avatarUrl }) => {
return await ctx.db.insert('profile', {
userId,
name,
email,
avatarUrl
});
}
});

export const update = mutation({
args: {
userId: v.string(),
name: v.string(),
email: v.string(),
avatarUrl: v.optional(v.string())
},
handler: async (ctx, { userId, name, email, avatarUrl }) => {
const profile = await ctx.db
.query('profile')
.withIndex('byUserId', (q) => q.eq('userId', userId))
.first();

if (!profile) {
return await ctx.db.insert('profile', {
userId,
name,
email,
avatarUrl
});
}

return await ctx.db.patch(profile._id, {
userId,
name,
email,
avatarUrl
});
}
});

export const remove = mutation({
args: {
userId: v.string(),
},
handler: async (ctx, { userId }) => {
const profile = await ctx.db
.query('profile')
.withIndex('byUserId', (q) => q.eq('userId', userId))
.first();

if (!profile) {
return
}

return await ctx.db.delete(profile._id);
}
});
ballingt
ballingt•2y ago
But I agree this deserves specifically mentioning, Next.js is common enough that we can have a guide specifically for Next.js + Clerk someday
gwilliamnn
gwilliamnnOP•2y ago
and this is the api/route who will receive the webhooks events:
import { NextResponse } from 'next/server';
import type { WebhookEvent } from '@clerk/clerk-sdk-node';
import { ConvexHttpClient } from 'convex/browser';
import { api } from '../../../../../convex/_generated/api';

const client = new ConvexHttpClient(process.env.CONVEX_URL!);

export async function POST(req: Request) {
const body = (await req.json()) as WebhookEvent;
console.log(body);
try {
if (body.type === 'user.created') {
const name =
(body.data.first_name ? body.data.first_name : 'No') +
' ' +
(body.data.last_name ? body.data.last_name : 'Name');

const user = await client.query(api.profile.getByUserId, { userId: body.data.id });
if (!user) {
await client.mutation(api.profile.create, {
userId: body.data.id,
name,
email: body.data.email_addresses[0].email_address,
avatarUrl: body.data.profile_image_url
});

console.log('Profile created');
}
}

if (body.type === 'user.updated') {
const name =
(body.data.first_name ? body.data.first_name : 'No') +
' ' +
(body.data.last_name ? body.data.last_name : 'Name');

console.log(name);

await client.mutation(api.profile.update, {
userId: body.data.id,
name,
email: body.data.email_addresses[0].email_address,
avatarUrl: body.data.profile_image_url
});

console.log('Profile updated');
}

if (body.type === 'user.deleted') {
const user = await client.query(api.profile.getByUserId, { userId: body.data.id as string });
if (user) {
await client.mutation(api.profile.remove, { userId: body.data.id as string });
} else {
console.log('Profile deleted not found profile');
}
}

return NextResponse.json({ status: 'ok' });
} catch (e) {
console.log(e);
return NextResponse.json({ status: 'error' });
}
}
import { NextResponse } from 'next/server';
import type { WebhookEvent } from '@clerk/clerk-sdk-node';
import { ConvexHttpClient } from 'convex/browser';
import { api } from '../../../../../convex/_generated/api';

const client = new ConvexHttpClient(process.env.CONVEX_URL!);

export async function POST(req: Request) {
const body = (await req.json()) as WebhookEvent;
console.log(body);
try {
if (body.type === 'user.created') {
const name =
(body.data.first_name ? body.data.first_name : 'No') +
' ' +
(body.data.last_name ? body.data.last_name : 'Name');

const user = await client.query(api.profile.getByUserId, { userId: body.data.id });
if (!user) {
await client.mutation(api.profile.create, {
userId: body.data.id,
name,
email: body.data.email_addresses[0].email_address,
avatarUrl: body.data.profile_image_url
});

console.log('Profile created');
}
}

if (body.type === 'user.updated') {
const name =
(body.data.first_name ? body.data.first_name : 'No') +
' ' +
(body.data.last_name ? body.data.last_name : 'Name');

console.log(name);

await client.mutation(api.profile.update, {
userId: body.data.id,
name,
email: body.data.email_addresses[0].email_address,
avatarUrl: body.data.profile_image_url
});

console.log('Profile updated');
}

if (body.type === 'user.deleted') {
const user = await client.query(api.profile.getByUserId, { userId: body.data.id as string });
if (user) {
await client.mutation(api.profile.remove, { userId: body.data.id as string });
} else {
console.log('Profile deleted not found profile');
}
}

return NextResponse.json({ status: 'ok' });
} catch (e) {
console.log(e);
return NextResponse.json({ status: 'error' });
}
}
Every time the user is created, updated or delete, I will create the profile on the convex,
ballingt
ballingt•2y ago
@gwilliamnn have you seen the example for this?
gwilliamnn
gwilliamnnOP•2y ago
or on delete, I will remove
gwilliamnn
gwilliamnnOP•2y ago
No, I made using the webhook clerk documentation, I don't know
ballingt
ballingt•2y ago
Nice, well we can compare https://github.com/thomasballinger/convex-clerk-users-table/blob/main/convex/http.ts#L17 so one difference is that you're doing this in a Next.js API route, while the example does it with a Convex HTTP endpoint
gwilliamnn
gwilliamnnOP•2y ago
nice, its the same logic, but instead a profile, is the entire user yeah i used ngrok for my localhost receive the apis
ballingt
ballingt•2y ago
This looks reasonable though! You're not validating the webhook request yet
gwilliamnn
gwilliamnnOP•2y ago
Yeah, its one of the things I need to implement
ballingt
ballingt•2y ago
and you should add some authentication to the user profile update mutation, so that it's protected from public use
gwilliamnn
gwilliamnnOP•2y ago
good insight, thanks!
ballingt
ballingt•2y ago
I think your approach looks good? although using a Convex HTTP endpoint might be more convenient
gwilliamnn
gwilliamnnOP•2y ago
I will be open a lot more support, prob, I din't had this repo before, now I will look again My project will get a lot query relationship So, I will need help with this soon for now, I'm playing with convex, I will try migrate my sanity.io project to here...
ballingt
ballingt•2y ago
Sounds good, let us know how it goes!
gwilliamnn
gwilliamnnOP•2y ago
🙂
Michal Srb
Michal Srb•2y ago
@gwilliamnn there are helpful posts and code examples for dealing with document relationships on Stack: https://stack.convex.dev/relationship-structures-let-s-talk-about-schemas
Relationship Structures: Let's Talk About Schemas
In this post we’ll look at some patterns for structuring relationships in the Convex database.
gwilliamnn
gwilliamnnOP•2y ago
Just to update this topic, the implementation with react clerk instead clerk nextjs will be problematic with app route, because app route relies on clerk nextjs helpers functions I already got some errors
Michal Srb
Michal Srb•2y ago
As long as you stick to client-side and client components, you should be ok. So basically don't use Next-specific Clerk integration. Would that work?
gwilliamnn
gwilliamnnOP•2y ago
I think will, I just need first understand what the integration with clerk makes, and then take the decision So, after reading the documentation, the use case is to has a helper context to know if the user are or not logged in and block some query/mut?
Michal Srb
Michal Srb•2y ago
Yup! You can protect pages that should be only accessible after log in via a client-side redirect.

Did you find this page helpful?