feat: notion provider (#3068)

* feat: add notion provider

* chore: lint

* chore: add docs for notion provider

---------

Co-authored-by: Bereket Engida <86073083+Bekacru@users.noreply.github.com>
This commit is contained in:
Ephraim Duncan
2025-07-08 01:53:52 +00:00
committed by GitHub
parent 3439fe954f
commit 0bff4af96b
5 changed files with 234 additions and 19 deletions

View File

@@ -1,15 +1,15 @@
"use client";
import { AnimatePresence, motion, MotionConfig } from "framer-motion";
import { AsideLink } from "@/components/ui/aside-link";
import { Suspense, useEffect, useState } from "react";
import { useSearchContext } from "fumadocs-ui/provider";
import { usePathname, useRouter } from "next/navigation";
import { contents, examples } from "./sidebar-content";
import { ChevronDownIcon, Search } from "lucide-react";
import { Select, SelectContent, SelectItem, SelectTrigger } from "./ui/select";
import { cn } from "@/lib/utils";
import { AnimatePresence, motion, MotionConfig } from "framer-motion";
import { useSearchContext } from "fumadocs-ui/provider";
import { ChevronDownIcon, Search } from "lucide-react";
import { usePathname, useRouter } from "next/navigation";
import { Suspense, useEffect, useState } from "react";
import { contents, examples } from "./sidebar-content";
import { Badge } from "./ui/badge";
import { Select, SelectContent, SelectItem, SelectTrigger } from "./ui/select";
export default function ArticleLayout() {
const [currentOpen, setCurrentOpen] = useState<number>(0);

View File

@@ -1,7 +1,10 @@
import {
Book,
CircleHelp,
Database,
Gauge,
Key,
KeyRound,
LucideAArrowDown,
LucideIcon,
Mail,
@@ -12,9 +15,6 @@ import {
UserCircle,
Users2,
UserSquare2,
Database,
KeyRound,
Book,
} from "lucide-react";
import { ReactNode, SVGProps } from "react";
import { Icons } from "./icons";
@@ -567,6 +567,24 @@ export const contents: Content[] = [
</svg>
),
},
{
title: "Notion",
href: "/docs/authentication/notion",
isNew: true,
icon: () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1.2em"
height="1.2em"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M4.459 4.208c.746.606 1.026.56 2.428.466l13.215-.793c.28 0 .047-.28-.046-.326L17.86 1.968c-.42-.326-.981-.7-2.055-.607L3.01 2.295c-.466.046-.56.28-.374.466zm.793 3.08v13.904c0 .747.373 1.027 1.214.98l14.523-.84c.841-.046.935-.56.935-1.167V6.354c0-.606-.233-.933-.748-.887l-15.177.887c-.56.047-.747.327-.747.933zm14.337.28c.093.42 0 .84-.42.888l-.7.14v10.264c-.608.327-1.168.514-1.635.514-.748 0-.935-.234-1.495-.933l-4.577-7.186v6.952L12.21 19s0 .84-1.168.84l-3.222.186c-.093-.186 0-.653.327-.746l.84-.233V9.854L7.822 9.76c-.094-.42.14-1.026.793-1.073l3.456-.233 4.764 7.279v-6.44l-1.215-.139c-.093-.514.28-.887.747-.933zM1.936 1.035l13.31-.98c1.634-.14 2.055-.047 3.082.7l4.249 2.986c.7.513.934.653.934 1.213v16.378c0 1.026-.373 1.634-1.68 1.726l-15.458.934c-.98.047-1.448-.093-1.962-.747l-3.129-4.06c-.56-.747-.793-1.306-.793-1.96V2.667c0-.839.374-1.54 1.447-1.632z"
/>
</svg>
),
},
{
title: "Tiktok",
href: "/docs/authentication/tiktok",

View File

@@ -0,0 +1,82 @@
---
title: Notion
description: Notion provider setup and usage.
---
<Steps>
<Step>
### Get your Notion credentials
To use Notion as a social provider, you need to get your Notion OAuth credentials. You can get them by creating a new integration in the [Notion Developers Portal](https://www.notion.so/my-integrations).
In the Notion integration settings > OAuth Domain & URIs, make sure to set the redirect URL to `http://localhost:3000/api/auth/callback/notion` for local development. For production, make sure to set the redirect URL as your application domain, e.g. `https://example.com/api/auth/callback/notion`. If you change the base path of the auth routes, you should update the redirect URL accordingly.
<Callout>
Make sure your Notion integration has the appropriate capabilities enabled. For user authentication, you'll need the "Read user information including email addresses" capability.
</Callout>
</Step>
<Step>
### Configure the provider
To configure the provider, you need to pass the `clientId` and `clientSecret` to `socialProviders.notion` in your auth configuration.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
socialProviders: {
notion: { // [!code highlight]
clientId: process.env.NOTION_CLIENT_ID as string, // [!code highlight]
clientSecret: process.env.NOTION_CLIENT_SECRET as string, // [!code highlight]
}, // [!code highlight]
},
})
```
</Step>
</Steps>
## Usage
### Sign In with Notion
To sign in with Notion, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties:
- `provider`: The provider to use. It should be set to `notion`.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
const authClient = createAuthClient()
const signIn = async () => {
const data = await authClient.signIn.social({
provider: "notion"
})
}
```
### Notion Integration Types
Notion supports different integration types. When creating your integration, you can choose between:
- **Public integrations**: Can be installed by any Notion workspace
- **Internal integrations**: Limited to your own workspace
For most authentication use cases, you'll want to create a public integration to allow users from different workspaces to sign in.
### Requesting Additional Notion Scopes
If your application needs additional Notion capabilities after the user has already signed up, you can request them using the `linkSocial` method with the same Notion provider and additional scopes.
```ts title="auth-client.ts"
const requestNotionAccess = async () => {
await authClient.linkSocial({
provider: "notion",
// Notion automatically provides access based on integration capabilities
});
};
// Example usage in a React component
return <button onClick={requestNotionAccess}>Connect Notion Workspace</button>;
```
<Callout>
After authentication, you can use the access token to interact with the Notion API to read and write pages, databases, and other content that the user has granted access to.
</Callout>

View File

@@ -1,12 +1,21 @@
import { z } from "zod";
import type { Prettify } from "../types/helper";
import { apple } from "./apple";
import { discord } from "./discord";
import { dropbox } from "./dropbox";
import { facebook } from "./facebook";
import { github } from "./github";
import { gitlab } from "./gitlab";
import { google } from "./google";
import { kick } from "./kick";
import { linkedin } from "./linkedin";
import { huggingface } from "./huggingface";
import { microsoft } from "./microsoft-entra-id";
import { notion } from "./notion";
import { reddit } from "./reddit";
import { roblox } from "./roblox";
import { spotify } from "./spotify";
import { tiktok } from "./tiktok";
import { twitch } from "./twitch";
import { twitter } from "./twitter";
import { dropbox } from "./dropbox";
@@ -18,7 +27,6 @@ import { reddit } from "./reddit";
import { roblox } from "./roblox";
import { z } from "zod";
import { vk } from "./vk";
import { kick } from "./kick";
import { zoom } from "./zoom";
export const socialProviders = {
apple,
@@ -41,6 +49,7 @@ export const socialProviders = {
roblox,
vk,
zoom,
notion,
};
export const socialProviderList = Object.keys(socialProviders) as [
@@ -62,22 +71,25 @@ export type SocialProviders = {
>;
};
export * from "./github";
export * from "./google";
export * from "./apple";
export * from "./microsoft-entra-id";
export * from "./discord";
export * from "./spotify";
export * from "./twitch";
export * from "./facebook";
export * from "./twitter";
export * from "./dropbox";
export * from "./facebook";
export * from "./github";
export * from "./linear";
export * from "./linkedin";
export * from "./gitlab";
export * from "./tiktok";
export * from "./google";
export * from "./kick";
export * from "./linkedin";
export * from "./microsoft-entra-id";
export * from "./notion";
export * from "./reddit";
export * from "./roblox";
export * from "./spotify";
export * from "./tiktok";
export * from "./twitch";
export * from "./twitter";
export * from "./vk";
export * from "./zoom";
export * from "./kick";

View File

@@ -0,0 +1,103 @@
import { betterFetch } from "@better-fetch/fetch";
import type { OAuthProvider, ProviderOptions } from "../oauth2";
import {
createAuthorizationURL,
refreshAccessToken,
validateAuthorizationCode,
} from "../oauth2";
export interface NotionProfile {
object: "user";
id: string;
type: "person" | "bot";
name?: string;
avatar_url?: string;
person?: {
email?: string;
};
bot?: {
owner: {
type: "workspace" | "user";
};
workspace_name?: string;
};
}
export interface NotionOptions extends ProviderOptions<NotionProfile> {}
export const notion = (options: NotionOptions) => {
const tokenEndpoint = "https://api.notion.com/v1/oauth/token";
return {
id: "notion",
name: "Notion",
createAuthorizationURL({ state, scopes, loginHint, redirectURI }) {
const _scopes: string[] = options.disableDefaultScope ? [] : [];
options.scope && _scopes.push(...options.scope);
scopes && _scopes.push(...scopes);
return createAuthorizationURL({
id: "notion",
options,
authorizationEndpoint: "https://api.notion.com/v1/oauth/authorize",
scopes: _scopes,
state,
redirectURI,
loginHint,
additionalParams: {
owner: "user",
},
});
},
validateAuthorizationCode: async ({ code, redirectURI }) => {
return validateAuthorizationCode({
code,
redirectURI,
options,
tokenEndpoint,
});
},
refreshAccessToken: options.refreshAccessToken
? options.refreshAccessToken
: async (refreshToken) => {
return refreshAccessToken({
refreshToken,
options: {
clientId: options.clientId,
clientKey: options.clientKey,
clientSecret: options.clientSecret,
},
tokenEndpoint,
});
},
async getUserInfo(token) {
if (options.getUserInfo) {
return options.getUserInfo(token);
}
const { data: profile, error } = await betterFetch<NotionProfile>(
"https://api.notion.com/v1/users/me",
{
headers: {
Authorization: `Bearer ${token.accessToken}`,
"Notion-Version": "2022-06-28",
},
},
);
if (error) {
return null;
}
const userMap = await options.mapProfileToUser?.(profile);
return {
user: {
id: profile.id,
name: profile.name || "Notion User",
email: profile.person?.email || null,
image: profile.avatar_url,
emailVerified: !!profile.person?.email,
...userMap,
},
data: profile,
};
},
options,
} satisfies OAuthProvider<NotionProfile>;
};