How to add user auth with Clerk to Nextjs (App Directory) and store it in Sanity CMS
First we are going to install a new Nextjs project but this should work with any Nextjs App Directory project, then we will install Clerk and get it working, once it’s working we will install and embed a Sanity studio and get it ready to receive the information provided by Clerk. We will finish by creating an API route, where we will send the user data to Sanity, and verify that the user was created.
- Install Nextjs in an empty folder.
npx create-next-app@latest .
- Proceed with you favourite settings I will choose Tailwind, Typescript, and the App Directory.
For the purpose of this tutorial I want to start with an empty home page in app/page.tsx
.
// app/page.tsx
const Home = () => {
return (
<main className="grid place-content-center min-h-screen">
Hello World
</main>
)
}
export default Home;
- Run the project and verify everything is working
npm run dev
- Install Clerk and dependencies.
npm install @clerk/nextjs
- Go to www.clerk.com and sign-up for a free account.
- Once inside click on Add Application and give it a name
- Select your favorite sign-in providers. I will choose email and Google.
- Copy the API Keys and paste them into your .env.local file.
- Mount ClerkProvider Update your root layout to include the wrapper. The component wraps your Next.js application to provide active session and user context to Clerk's hooks and other components. It is recommended that the wraps the to enable the context to be accessible anywhere within the app.
- Your app/layout.tsx should look something like this:
// app/layout.tsx
import './globals.css'
import { Inter } from 'next/font/google'
import { ClerkProvider } from '@clerk/nextjs'
const inter = Inter({ subsets: ['latin'] })
export const metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<ClerkProvider>
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
</ClerkProvider>
)
-
Protect your Application
Now that Clerk is installed and mounted in your application, it’s time to decide which pages are public and which need to hide behind authentication. We do this by creating a
middleware.ts
file at the root folder (or insidesrc/
if that is how you set up your app).In my case I will create the middleware.ts file inside the root folder of my project. I will also add a Public Routes array, so any route you would like to be public you will need to add to this array.
We will add the studio route, and let sanity handle the auth for the CMS studio.
// middlewate.ts
import { authMiddleware } from "@clerk/nextjs";
// This example protects all routes including api/trpc routes
// Please edit this to allow other routes to be public as needed.
// See https://clerk.com/docs/nextjs/middleware for more information about configuring your middleware
const publicRoutes = ["/", "/studio"];
export default authMiddleware({
publicRoutes,
});
export const config = {
matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
};
-
Create the sign-up page.
In the app directory create a sign-up folder and then another catch-all [[…sign-up]] folder with a page inside of it. Your route should look like this:
app/sign-up/[[…sign-up]]/page.tsx
Inside the page.tsx file we will add the provided SignUp component from Clerk.
// app/sign-up/[[…sign-up]]/page.tsx import { SignUp } from "@clerk/nextjs"; export default function Page() { return ( <main className="grid place-content-center min-h-screen"> <SignUp/> </main> ); }
Do the same for the Sign in page. But we will also add the redirectUrl parameter provided by nextjs so that the user can be redirected back to the page they were visiting before or home if there isn’t any.
// app/sign-up/[[…sign-in]]/page.tsx
import { SignIn } from "@clerk/nextjs";
export default function Page({searchParams}: {searchParams: {redirectUrl: string | undefined}}) {
const {redirectUrl} = searchParams;
return (
<main className="grid place-content-center min-h-screen">
<SignIn redirectUrl={redirectUrl || "/"}/>
</main>
);
}
- Next we will add some more environment variables to control the behavior of clerk and also to tell it what route the sign-up and sign-in pages are in, we do this in the .env.local file in the root right after our API keys.
// .env.local
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/
- Create a link to the sign up page inside the homepage. In
app/page.tsx
lets add the link -
Run the app, navigate to http://localhost:3000 using the browser and click on sign-up, sign-up for your app.
// app/page.tsx import Link from "next/link"; const Home = () => { return ( <main className="grid place-content-center min-h-screen"> <Link href="/sign-up">Sign Up</Link> </main> ); }; export default Home;
- Go to dashboard.clerk.com select your project and verify your new user.
- In the homepage let’s add some user data to the home page and some conditional rendering to see the data if the user is logged in.
// app/page.tsx
import { UserButton, currentUser } from "@clerk/nextjs";
import Link from "next/link";
export default async function Home() {
const user = await currentUser();
if (user) {
return (
<main className="grid place-content-center gap-2 min-h-screen">
<div className="flex items-center gap-2">
<UserButton afterSignOutUrl="/" />
<h2>Welcome back {user.firstName}!</h2>
</div>
</main>
);
}
return (
<main className="grid place-content-center min-h-screen">
<Link href="/sign-up">Sign Up</Link>
</main>
);
}
- Let’s visit our homepage:
- If we click on the user image we have access to some options, for now I will try to sign out to see the other view.
- You should see your sign-up button now:
- We can also add a signout button provided by clerk to make it easier to sign out and clearer that its a sign up / sign in page as well:
//// app/page.tsx
import { currentUser, UserButton, SignOutButton } from "@clerk/nextjs";
import Link from "next/link";
export default async function Home() {
const user = await currentUser();
if (user) {
return (
<main className="grid place-content-center gap-2 min-h-screen">
<div className="flex items-center gap-2">
<UserButton afterSignOutUrl="/" />
<h2>Welcome back {user.firstName}!</h2>
<SignOutButton />
</div>
</main>
);
}
return (
<main className="grid place-content-center min-h-screen">
<Link href="/sign-up">Sign Up / Sign In</Link>
</main>
);
}
- Now that we have sign up and sign out flows setup let’s create and embed a new sanity CMS in our Nextjs project, this process will still work even if the CMS isn’t embedded however I will embed a new project for the purpose of this tutorial.
- Install the necessary packages for Sanity to work as well as the client packages to make the requests to the CMS.
npm install sanity next-sanity next-sanity-image @portabletext/react @sanity/client @sanity/image-url
- Go to sanity.io sign up for an account. Sanity gives instructions on how to create the studio however since we are embedding the studio on a Nextjs project we can just ignore them and navigate to
https://www.[sanity.io/manage](http://sanity.io/manage)
. If Sanity created a project for you click on it and copy the project ID, if they didn’t you can click on Create a new project on the top and then copy the project ID provided. - Go to your .env.local and add the project id under the Clerk variables like this:
/// .env.local
SANITY_PROJECT_ID=(your sanity project id)
- Create a sanity folder on the root of your project, and a config.ts file with the following content:
// sanity/config.ts
export const sanityConfig = {
projectId: process.env.SANITY_PROJECT_ID || "",
dataset: 'production',
useCdn: process.env.NODE_ENV === 'production',
apiVersion: '2021-03-25',
}
- Create your studioConfig.ts file inside the sanity folder in it for now we will define the config and the basePath for the sanity studio.
// sanity/studioConfig.ts
import { sanityConfig } from './config';
import { defineConfig } from "sanity";
export const sanityAdminConfig = {
...sanityConfig,
title: "Clerk Users and Sanity Test",
basePath: "/admin",
};
export const studioConfig = defineConfig(sanityAdminConfig);
- Inside App directory create a page studio route and an index catch all filder, where we will mount the studio.
app/studio/[[…index]]/page.tsx
We will import the studio config we just created and give it as a prop to the NextStudio component provided by Sanity, this is Client component so we will have to add the use client directive at the top of the file.
// app/studio/[[…index]]/page.tsx
'use client'
import { studioConfig } from '@/sanity/studioConfig'
import {NextStudio} from 'next-sanity/studio'
export default function StudioPage() {
// Supports the same props as `import {Studio} from 'sanity'`, `config` is required
return <NextStudio config={studioConfig} />
}
- Lets restart our project by running
npm run dev
and navigate tohttp://localhost:3000/studio
- We should get this error:
This is because we need to allow the route in the Sanity studio for it to be able to read and write to sanity’s database. We can add CORS origins in the Sanity Management Dashboard for the project.
- Go to https://www.sanity.io/manage open the project we created previously and navigate to the API tab and look for the + Add CORS origin button
- Add:
http://localhost:3000
, and click Allow Credentials since we are embedding the studio in our NextJS project we need to allow it to have credentials. When you deploy your site you will need to add the root address of your site ie:[http://www.yoursite.com](http://www.yoursite.com)
to the list of allowed sites.
- Click Save.
- We can also delete localhost:3333 since this is the location of the default sanity studio which we are not using.
- Go back to `http://localhost:3000/studio` and sign in with your user.
Now we have an empty studio embedded in the Nextjs site! Let’s add a users document list to our sanity.
- Create the schema for the users, for our tutorial we will just need first name, last name, and email.
- Create a schemas folder inside the sanity folder and create user.ts
sanity/schemas/user.ts
add the document for users, we will use an icon from sanity so that the users’ list gets a nice icon.
// sanity/schemas/user.ts
import { defineType, defineField } from "sanity";
import { UserIcon } from "@sanity/icons";
export const userSchema = defineType({
name: "user",
title: "Users",
type: "document",
icon: UserIcon,
fields: [
defineField({
name: "firstName",
title: "First Name",
type: "string",
}),
defineField({
name: "lastName",
title: "Last Name",
type: "string",
}),
defineField({
name: "email",
title: "Email",
type: "string",
})
],
});
Now lets add the schemas to our studioConfig. I like to keep my schemas organized in a separate file so that it’s easy to add or remove schemas from my project.
- Create in the sanity folder a schemas.ts file.
sanity/schema.ts
and add the following:
// sanity/schema.ts
import {userSchema} from "./schemas/user";
export const schemaTypes = [
userSchema
]
- Import the schemas into the
sanity/studioConfig.ts
file and add it to the config.
// sanity/studioConfig.ts
import { sanityConfig } from './config';
import { defineConfig } from "sanity";
import { schemaTypes } from "./schemas";
export const sanityAdminConfig = {
...sanityConfig,
title: "Clerk Users and Sanity Test",
basePath: "/studio",
schema: {
types: schemaTypes,
},
};
export const studioConfig = defineConfig(sanityAdminConfig);
We are still getting the same screen, that’s because I forgot to add the desktool to the config, let’s do that.
// sanity/studioConfig.ts
import { sanityConfig } from './config';
import { defineConfig } from "sanity";
import { schemaTypes } from "./schemas";
import {deskTool} from "sanity/desk"
export const sanityAdminConfig = {
...sanityConfig,
title: "Clerk Users and Sanity Test",
basePath: "/studio",
plugins: [
deskTool(),
],
schema: {
types: schemaTypes,
},
};
export const studioConfig = defineConfig(sanityAdminConfig);
- Refresh, and we should be able to see the users list and we can add users to our list,
However, we want them to be added when the user signs in with our site. To do that we will use Clerks environment variables to redirect the user after sign-up to an API route, where we can get the user’s information add save it in our sanity and redirect the user again either to where they where coming from or to the homepage.
Let’s start by creating the API route first.
- Create a folder create-sanity-user (you can name this something relevant to your app) inside the app folder and create a file route.tsx. This is a special Nextjs file that let’s us create api end points.
app/create-sanity-user/route.ts
, in it we will use clerk’scurrentUser
server function to access the user information. We also want to prevent anyone from accessing the route, so if the user doesn’t exist then we can redirect them to the sign-in page, however since we are using the middleware.ts file we won’t need to add that to our api route. Let’s try to return the user’s data first.
// app/create-sanity-user/route.ts
import { currentUser } from "@clerk/nextjs";
import { NextResponse } from "next/server";
export const GET = async () => {
const user = await currentUser();
return NextResponse.json({ user });
};
- Navigate to
http://localhost:3000/create-sanity-user
we should now see the data that clerk gives you access too, we can save any of it in our sanity. - Navigate to
[http://localhost:3000/](http://localhost:3000/)
and log out and navigate back tohttp://localhost:3000/create-sanity-user
` and we should now get redirected to the sign-in page! Great! - Let’s log back in and we should get redirected back to create-sanity-user end point.
- Let’s now add the user data to sanity, we need to create a sanityClient file which will allow us to have access to sanity’s functionality, and be able to read and write data from our nextjs app.
- We will also need an API token from sanity to be able to write data onto it. Go back to
https://www.[sanity.io/manage](http://sanity.io/manage)
and in the API tab click on + Add API token
- Give it a name and select the Editor option to have access to read and write token options. Copy your token, and add it to your
.env.local
file.
jsx
SANITY_API_TOKEN=(your api token)
- Create in the sanity folder a
sanityClient.ts
file and import the createClient function from next-sanity, and your own config, and just add the token from the environment variables, or empty if it doesn’t exist.
`jsx
// sanity/sanityClient.ts
import { createClient } from "next-sanity";
import { sanityConfig } from "./config";
export default createClient({
...sanityConfig,
token: process.env.SANITY_API_TOKEN || "",
});
`
- Go to the
app/create-sanity-user/route.ts
file and import the sanityClient we just created, we will use the createIfNotExists (which prevents errors when a user already exists) function that the next-sanity package provides to create a document in our sanity. - We are also going to get the base path URL to redirect to from the request, this will work for both development and deployed, since the NextResponse.redirect function expects a full url. And redirect the user to this base URL.
- Even though the middleware is taking care of authentication, we still want to prevent typescript errors in case the user doesn’t exist.
`jsx
// app/create-sanity-user/route.ts
import { NextApiRequest } from "next";
import sanityClient from "@/sanity/sanityClient";
import { currentUser } from "@clerk/nextjs";
import { NextResponse } from "next/server";
export const GET = async (req: NextApiRequest) => {
const user = await currentUser();
if (!user) {
return NextResponse.redirect("/sign-in");
}
const {id, firstName, lastName, emailAddresses} = user;
await sanityClient.createIfNotExists({
_type: "user",
_id: id,
firstName,
lastName,
email: emailAddresses[0].emailAddress
})
const url = req.url?.split("/create-sanity-user")[0] || "/"
return NextResponse.redirect(url);
};
`
- Restart your server and navigate to
http://localhost:3000/create-sanity-user
then navigate to your studio at `http://localhost:3000/studio` and see the new user has been added to your studio - The last step will be to update the
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL
environment variable and point it to /create-sanity-user route to our environment variables so Clerk can have the right behavior after signing-up.
jsx
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/create-sanity-user
- Restart your server since we updated the environment variables and try to signup with a different user, then check your studio and verify the new user was added.
That’s it, now you can add your own specific details to your users, or relate it to other documents specific for your use case.
Top comments (7)
Thanks for a great tutorial. Just wanted to mention that you can sign in with a google account without having to sign up and this user doesn't get put into sanity.
You're right I currently handle what happens after with the clerk env variables, and also I check wether the user exists in sanity if not it takes them through the create user API route, but the user doesn't really notice this happening...
For some reason dev.to stopped parsing correctly code blocks towards the end of the article I hope this doesn't stop you from getting the whole concept. You can read the original article here with correct markdown formatting: julian-bustos.notion.site/How-to-a...
Hey, Julian, welcome to the Dev.to community! About formatting, maybe this is related to an extra backtick somewhere in your text. I would guess it's the backtick on
doesn't
, atOr it could also be a missing backtick at the
code block.
Nice article, by the way!
Hey Henrique thanks for appreciating it, unless ’ count as
it shouldn't affect it, I did check several times around that
SANITY_API_TOKEN
` and it has the 3 ticks before and 3 after, but it seems to register the 2 first ticks as an inblock code section and then the rest of it as a separate in block section rather than recognizing it as a code block section with triple ticks... It works all the way to that SANITY_API_TOKEN just fine.... do you think it's too long and I broke dev.to? :DHmm, that's odd... 🤔
I believe not, there are some pretty long posts around. Maybe there's something related w/ the backticks and we're overlooking it 😅
For some reason i am only storing the client in sanity by manually entering the api path into the search bar and pressing Enter.
I have changed my *NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL * variable to the path i set.
Note: I am using the api folder structure instead of the one you used in the example, i wonder if maybe that is causing the error.
do you have any pointers regarding this?
Great article by the way.