Integrate InstantDB with Clerk
You will learn the following:
- Configure your Clerk session token to include the
email
claim. - Configure InstantDB to use your Clerk credentials.
- Integrate InstantDB into your Clerk application.
Integrating InstantDB with Clerk gives you the benefits of using an InstantDB database while leveraging Clerk's authentication features.
This tutorial will walk you through the steps to integrate InstantDB with Clerk in your Next.js app. If you're using a different framework, the steps are the same and the code can be adapted for any React-based framework.
Configure your Clerk session token
InstantDB uses Clerk's session token to authenticate users. To use InstantDB with Clerk, you need to include the email
claim in your session token.
- In the Clerk Dashboard, navigate to the Sessions page.
- In the Customize session token section, select Edit.
- Add the
email
claim to your session token:{ "email": "{{user.primary_email_address}}" }
You can have additional claims as long as the email
claim is set to {{user.primary_email_address}}
.
Get your Clerk Publishable Key
- In the Clerk Dashboard, navigate to the API keys page.
- In the Quick Copy section, copy your Clerk Publishable Key.
Configure InstantDB
- In the InstantDB dashboard, navigate to the Auth tab.
- At the top of the page, save the Public App ID somewhere as you'll need this later.
- Select Setup Clerk.
- Add the Clerk Publishable Key you copied in the previous step.
- Confirm the The session token has the "email" claim. message.
- Select Add Clerk app. Save the Client Name somewhere as you'll need this later.
Install the InstantDB library
Run the following command to add the InstantDB library to your project.
npm i @instantdb/react
yarn add @instantdb/react
pnpm add @instantdb/react
bun add @instantdb/react
Set your InstantDB credentials
In your .env
file, set the following environment variables to your InstantDB App ID and Clerk Client Name that you saved earlier:
NEXT_PUBLIC_INSTANTDB_APP_ID=
NEXT_PUBLIC_CLERK_CLIENT_NAME=
Initialize InstantDB in your app
To initialize InstantDB in your app:
- Create a
db
directory. - In the
db
directory, create ainstant.ts
file with the following code. It initializes InstantDB with your App ID and schema. The schema used below is necessary for this tutorial, but you can customize it as needed. Read more about InstantDB schemas in the InstantDB docs.
import { i, init } from '@instantdb/react'
const APP_ID = process.env.NEXT_PUBLIC_INSTANTDB_APP_ID
if (!APP_ID) {
throw new Error('Missing NEXT_PUBLIC_INSTANTDB_APP_ID in your .env file')
}
// Optional: Declare your schema
export const schema = i.schema({
entities: {
todos: i.entity({
text: i.string(),
done: i.boolean(),
createdAt: i.number(),
}),
},
})
export const db = init({ appId: APP_ID, schema })
Manage the Clerk and InstantDB auth sessions
Integrating InstantDB with Clerk means that your users will sign in to your app using Clerk, and then Clerk's session token will be used to sign the user in to InstantDB. This means that your user will have two sessions: one with Clerk and one with InstantDB. In order to handle both sessions, the following component uses Clerk to check if the user is signed in, and if they are, it uses Clerk's session token to sign the user in to InstantDB. If the user is not signed in to Clerk, it signs them out of InstantDB, ensuring that if the Clerk session ends, the InstantDB session will end as well.
'use client'
import { db } from '@/db/instant'
import { useAuth, useUser } from '@clerk/nextjs'
import { useEffect } from 'react'
// If a user is signed in with Clerk, sign them in with InstantDB
export default function InstantDBAuthSync() {
const { isSignedIn } = useUser()
const { getToken } = useAuth()
useEffect(() => {
if (isSignedIn) {
getToken()
.then((token) => {
// Create a long-lived session with Instant for your Clerk user
// It will look up the user by email or create a new user with
// the email address in the session token.
db.auth.signInWithIdToken({
clientName: process.env.NEXT_PUBLIC_CLERK_CLIENT_NAME as string,
idToken: token as string,
})
})
.catch((error) => {
console.error('Error signing in with Instant', error)
})
} else {
db.auth.signOut()
}
}, [isSignedIn])
return null
}
It's important to use this component in your root layout.tsx
file because the useEffect()
hook that syncs the Clerk and InstantDB sessions needs to run on every page load in order to manage the sessions properly.
import type { Metadata } from 'next'
import {
ClerkProvider,
SignInButton,
SignUpButton,
SignedIn,
SignedOut,
UserButton,
} from '@clerk/nextjs'
import { Geist, Geist_Mono } from 'next/font/google'
import './globals.css'
import InstantDBAuthSync from '@/component/InstantDBAuthSync'
const geistSans = Geist({
variable: '--font-geist-sans',
subsets: ['latin'],
})
const geistMono = Geist_Mono({
variable: '--font-geist-mono',
subsets: ['latin'],
})
export const metadata: Metadata = {
title: 'Clerk Next.js Quickstart',
description: 'Generated by create next app',
}
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
<ClerkProvider>
<InstantDBAuthSync />
<html lang="en">
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
<header className="flex justify-end items-center p-4 gap-4 h-16">
<SignedOut>
<SignInButton />
<SignUpButton />
</SignedOut>
<SignedIn>
<UserButton />
</SignedIn>
</header>
{children}
</body>
</html>
</ClerkProvider>
)
}
Update your homepage
Update your page.tsx
file with the following code to see InstantDB in action. This code was copied from the InstantDB quickstart. The only difference is that the initialization code was moved to /db/instant.ts
so that the InstantDB connection could be reused across the app.
'use client'
import { id, InstaQLEntity } from '@instantdb/react'
import { db, schema } from '@/db/instant'
type Todo = InstaQLEntity<typeof schema, 'todos'>
function App() {
// Use Instant's `useQuery()` hook to get the todos
const { isLoading, error, data } = db.useQuery({ todos: {} })
if (isLoading) {
return (
<div className="-mt-16 font-mono min-h-screen flex justify-center items-center flex-col space-y-4">
Loading...
</div>
)
}
if (error) {
return (
<div className="text-red-500 p-4 -mt-16 font-mono min-h-screen flex justify-center items-center flex-col space-y-4">
Error: {error.message}
</div>
)
}
const { todos } = data
return (
<div className="-mt-16 font-mono min-h-screen flex justify-center items-center flex-col space-y-4">
<h2 className="tracking-wide text-5xl text-gray-300">todos</h2>
<div className="border border-gray-300 max-w-xs w-full">
<TodoForm todos={todos} />
<TodoList todos={todos} />
<ActionBar todos={todos} />
</div>
<div className="text-xs text-center">Open another tab to see todos update in realtime!</div>
</div>
)
}
// Write Data
// ---------
function addTodo(text: string) {
db.transact(
db.tx.todos[id()].update({
text,
done: false,
createdAt: Date.now(),
}),
)
}
function deleteTodo(todo: Todo) {
db.transact(db.tx.todos[todo.id].delete())
}
function toggleDone(todo: Todo) {
db.transact(db.tx.todos[todo.id].update({ done: !todo.done }))
}
function deleteCompleted(todos: Todo[]) {
const completed = todos.filter((todo) => todo.done)
const txs = completed.map((todo) => db.tx.todos[todo.id].delete())
db.transact(txs)
}
function toggleAll(todos: Todo[]) {
const newVal = !todos.every((todo) => todo.done)
db.transact(todos.map((todo) => db.tx.todos[todo.id].update({ done: newVal })))
}
// Components
// ----------
function ChevronDownIcon() {
return (
<svg viewBox="0 0 20 20">
<path d="M5 8 L10 13 L15 8" stroke="currentColor" fill="none" strokeWidth="2" />
</svg>
)
}
function TodoForm({ todos }: { todos: Todo[] }) {
return (
<div className="flex items-center h-10 border-b border-gray-300">
<button
className="h-full px-2 border-r border-gray-300 flex items-center justify-center"
onClick={() => toggleAll(todos)}
>
<div className="w-5 h-5">
<ChevronDownIcon />
</div>
</button>
<form
className="flex-1 h-full"
onSubmit={(e) => {
e.preventDefault()
const input = e.currentTarget.input as HTMLInputElement
addTodo(input.value)
input.value = ''
}}
>
<input
className="w-full h-full px-2 outline-none bg-transparent"
autoFocus
placeholder="What needs to be done?"
type="text"
name="input"
/>
</form>
</div>
)
}
function TodoList({ todos }: { todos: Todo[] }) {
return (
<div className="divide-y divide-gray-300">
{todos.map((todo) => (
<div key={todo.id} className="flex items-center h-10">
<div className="h-full px-2 flex items-center justify-center">
<div className="w-5 h-5 flex items-center justify-center">
<input
type="checkbox"
className="cursor-pointer"
checked={todo.done}
onChange={() => toggleDone(todo)}
/>
</div>
</div>
<div className="flex-1 px-2 overflow-hidden flex items-center">
{todo.done ? (
<span className="line-through">{todo.text}</span>
) : (
<span>{todo.text}</span>
)}
</div>
<button
className="h-full px-2 flex items-center justify-center text-gray-300 hover:text-gray-500"
onClick={() => deleteTodo(todo)}
>
X
</button>
</div>
))}
</div>
)
}
function ActionBar({ todos }: { todos: Todo[] }) {
return (
<div className="flex justify-between items-center h-10 px-2 text-xs border-t border-gray-300">
<div>Remaining todos: {todos.filter((todo) => !todo.done).length}</div>
<button className=" text-gray-300 hover:text-gray-500" onClick={() => deleteCompleted(todos)}>
Delete Completed
</button>
</div>
)
}
export default App
Feedback
Last updated on