Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Supabaseify #1

Merged
merged 5 commits into from
Jul 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 7 additions & 12 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,12 @@
## Then get your OpenAI API Key here: https://platform.openai.com/account/api-keys
OPENAI_API_KEY=XXXXXXXX

## Generate a random secret: https://generate-secret.vercel.app/32 or `openssl rand -base64 32`
AUTH_SECRET=XXXXXXXX
## Create a GitHub OAuth app here: https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app
# Update these with your Supabase details from your project settings > API
# https://app.supabase.com/project/_/settings/api
# In local dev you can get these by running `supabase status`.
NEXT_PUBLIC_SUPABASE_URL=your-project-url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key

## Follow GitHub Oauth setup steps from Supabase:
AUTH_GITHUB_ID=XXXXXXXX
AUTH_GITHUB_SECRET=XXXXXXXX
## Support OAuth login on preview deployments, see: https://authjs.dev/guides/basics/deployment#securing-a-preview-deployment
AUTH_REDIRECT_PROXY_URL=https://auth.example.com/api/auth

# Instructions to create kv database here: https://vercel.com/docs/storage/vercel-kv/quickstart and
KV_URL=XXXXXXXX
KV_REST_API_URL=XXXXXXXX
KV_REST_API_TOKEN=XXXXXXXX
KV_REST_API_READ_ONLY_TOKEN=XXXXXXXX

34 changes: 20 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
</a>

<p align="center">
An open-source AI chatbot app template built with Next.js, the Vercel AI SDK, OpenAI, and Vercel KV.
An open-source AI chatbot app template built with Next.js, the Vercel AI SDK, OpenAI, and Supabase Auth and Postgres DB.
</p>

<p align="center">
Expand All @@ -27,35 +27,40 @@
- Styling with [Tailwind CSS](https://tailwindcss.com)
- [Radix UI](https://radix-ui.com) for headless component primitives
- Icons from [Phosphor Icons](https://phosphoricons.com)
- Chat History, rate limiting, and session storage with [Vercel KV](https://vercel.com/storage/kv)
- [Next Auth](https://github.com/nextauthjs/next-auth) for authentication
- Chat History with [Supabase Postgres DB](https://supabase.com)
- [Supabase Auth](https://supabase.com/auth) for authentication

## Model Providers

This template ships with OpenAI `gpt-3.5-turbo` as the default. However, thanks to the [Vercel AI SDK](https://sdk.vercel.ai/docs), you can switch LLM providers to [Anthropic](https://anthropic.com), [Hugging Face](https://huggingface.co), or using [LangChain](https://js.langchain.com) with just a few lines of code.

## Deploy Your Own
<!-- ## Deploy Your Own

You can deploy your own version of the Next.js AI Chatbot to Vercel with one click:

[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?demo-title=Next.js+Chat&demo-description=A+full-featured%2C+hackable+Next.js+AI+chatbot+built+by+Vercel+Labs&demo-url=https%3A%2F%2Fchat.vercel.ai%2F&demo-image=%2F%2Fimages.ctfassets.net%2Fe5382hct74si%2F4aVPvWuTmBvzM5cEdRdqeW%2F4234f9baf160f68ffb385a43c3527645%2FCleanShot_2023-06-16_at_17.09.21.png&project-name=Next.js+Chat&repository-name=nextjs-chat&repository-url=https%3A%2F%2Fgithub.com%2Fvercel-labs%2Fai-chatbot&from=templates&skippable-integrations=1&env=OPENAI_API_KEY%2CAUTH_GITHUB_ID%2CAUTH_GITHUB_SECRET&envDescription=How+to+get+these+env+vars&envLink=https%3A%2F%2Fgithub.com%2Fvercel-labs%2Fai-chatbot%2Fblob%2Fmain%2F.env.example&teamCreateStatus=hidden&stores=[{"type":"kv"}])
TODO: update button with supabase integration
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?demo-title=Next.js+Chat&demo-description=A+full-featured%2C+hackable+Next.js+AI+chatbot+built+by+Vercel+Labs&demo-url=https%3A%2F%2Fchat.vercel.ai%2F&demo-image=%2F%2Fimages.ctfassets.net%2Fe5382hct74si%2F4aVPvWuTmBvzM5cEdRdqeW%2F4234f9baf160f68ffb385a43c3527645%2FCleanShot_2023-06-16_at_17.09.21.png&project-name=Next.js+Chat&repository-name=nextjs-chat&repository-url=https%3A%2F%2Fgithub.com%2Fvercel-labs%2Fai-chatbot&from=templates&skippable-integrations=1&env=OPENAI_API_KEY%2CAUTH_GITHUB_ID%2CAUTH_GITHUB_SECRET&envDescription=How+to+get+these+env+vars&envLink=https%3A%2F%2Fgithub.com%2Fvercel-labs%2Fai-chatbot%2Fblob%2Fmain%2F.env.example&teamCreateStatus=hidden&stores=[{"type":"kv"}]) -->

## Creating a KV Database Instance
## Running locally

Follow the steps outlined in the [quick start guide](https://vercel.com/docs/storage/vercel-kv/quickstart#create-a-kv-database) provided by Vercel. This guide will assist you in creating and configuring your KV database instance on Vercel, enabling your application to interact with it.
You will need to use the environment variables [defined in `.env.example`](.env.example) to run Next.js AI Chatbot. It's recommended you use [Vercel Environment Variables](https://vercel.com/docs/concepts/projects/environment-variables) for this, but a `.env` file is all that is necessary.

Remember to update your environment variables (`KV_URL`, `KV_REST_API_URL`, `KV_REST_API_TOKEN`, `KV_REST_API_READ_ONLY_TOKEN`) in the `.env` file with the appropriate credentials provided during the KV database setup.
> Note: You should not commit your `.env` file or it will expose secrets that will allow others to control access to your various OpenAI and authentication provider accounts.

Copy the `.env.example` file and populate the required env vars:

## Running locally
```bash
cp .env.example .env
```

You will need to use the environment variables [defined in `.env.example`](.env.example) to run Next.js AI Chatbot. It's recommended you use [Vercel Environment Variables](https://vercel.com/docs/concepts/projects/environment-variables) for this, but a `.env` file is all that is necessary.
[Install the Supabase CLI](https://supabase.com/docs/guides/cli) and start the local Supabase stack:

> Note: You should not commit your `.env` file or it will expose secrets that will allow others to control access to your various OpenAI and authentication provider accounts.
```bash
npm install supabase --save-dev
npx supabase start
```

1. Install Vercel CLI: `npm i -g vercel`
2. Link local instance with Vercel and GitHub accounts (creates `.vercel` directory): `vercel link`
3. Download your environment variables: `vercel env pull`
Install the local dependencies and start dev mode:

```bash
pnpm install
Expand All @@ -71,3 +76,4 @@ This library is created by [Vercel](https://vercel.com) and [Next.js](https://ne
- Jared Palmer ([@jaredpalmer](https://twitter.com/jaredpalmer)) - [Vercel](https://vercel.com)
- Shu Ding ([@shuding\_](https://twitter.com/shuding_)) - [Vercel](https://vercel.com)
- shadcn ([@shadcn](https://twitter.com/shadcn)) - [Contractor](https://shadcn.com)
- Thor Schaeff ([@thorwebdev](https://twitter.com/thorwebdev)) - [Supabaseifier](https://thor.bio)
119 changes: 48 additions & 71 deletions app/actions.ts
Original file line number Diff line number Diff line change
@@ -1,120 +1,97 @@
'use server'

import { createServerActionClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
import { Database } from '@/lib/db_types'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
import { kv } from '@vercel/kv'

import { auth } from '@/auth'
import { type Chat } from '@/lib/types'
import { auth } from '@/auth'

const supabase = createServerActionClient<Database>({ cookies })

export async function getChats(userId?: string | null) {
if (!userId) {
return []
}

try {
const pipeline = kv.pipeline()
const chats: string[] = await kv.zrange(`user:chat:${userId}`, 0, -1, {
rev: true
})

for (const chat of chats) {
pipeline.hgetall(chat)
}
const { data } = await supabase
.from('chats')
.select('payload')
.order('payload->createdAt', { ascending: false })
.throwOnError()

const results = await pipeline.exec()

return results as Chat[]
return (data?.map(entry => entry.payload) as Chat[]) ?? []
} catch (error) {
return []
}
}

export async function getChat(id: string, userId: string) {
const chat = await kv.hgetall<Chat>(`chat:${id}`)

if (!chat || (userId && chat.userId !== userId)) {
return null
}
export async function getChat(id: string) {
const { data } = await supabase
.from('chats')
.select('payload')
.eq('id', id)
.maybeSingle()

return chat
return (data?.payload as Chat) ?? null
}

export async function removeChat({ id, path }: { id: string; path: string }) {
const session = await auth()

if (!session) {
return {
error: 'Unauthorized'
}
}

const uid = await kv.hget<string>(`chat:${id}`, 'userId')
try {
await supabase.from('chats').delete().eq('id', id).throwOnError()

if (uid !== session?.user?.id) {
revalidatePath('/')
return revalidatePath(path)
} catch (error) {
return {
error: 'Unauthorized'
}
}

await kv.del(`chat:${id}`)
await kv.zrem(`user:chat:${session.user.id}`, `chat:${id}`)

revalidatePath('/')
return revalidatePath(path)
}

export async function clearChats() {
const session = await auth()

if (!session?.user?.id) {
try {
const session = await auth()
await supabase
.from('chats')
.delete()
.eq('user_id', session?.user.id)
.throwOnError()
revalidatePath('/')
return redirect('/')
} catch (error) {
console.log('clear chats error', error)
return {
error: 'Unauthorized'
}
}

const chats: string[] = await kv.zrange(`user:chat:${session.user.id}`, 0, -1)
if (!chats.length) {
return redirect('/')
}
const pipeline = kv.pipeline()

for (const chat of chats) {
pipeline.del(chat)
pipeline.zrem(`user:chat:${session.user.id}`, chat)
}

await pipeline.exec()

revalidatePath('/')
return redirect('/')
}

export async function getSharedChat(id: string) {
const chat = await kv.hgetall<Chat>(`chat:${id}`)

if (!chat || !chat.sharePath) {
return null
}

return chat
const { data } = await supabase
.from('chats')
.select('payload')
.eq('id', id)
.not('payload->sharePath', 'is', null)
.maybeSingle()

return (data?.payload as Chat) ?? null
}

export async function shareChat(chat: Chat) {
const session = await auth()

if (!session?.user?.id || session.user.id !== chat.userId) {
return {
error: 'Unauthorized'
}
}

const payload = {
...chat,
sharePath: `/share/${chat.id}`
}

await kv.hmset(`chat:${chat.id}`, payload)
await supabase
.from('chats')
.update({ payload: payload as any })
.eq('id', chat.id)
.throwOnError()

return payload
}
2 changes: 0 additions & 2 deletions app/api/auth/[...nextauth]/route.ts

This file was deleted.

19 changes: 19 additions & 0 deletions app/api/auth/callback/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
import { NextResponse } from 'next/server'

export async function GET(request: Request) {
// The `/auth/callback` route is required for the server-side auth flow implemented
// by the Auth Helpers package. It exchanges an auth code for the user's session.
// https://supabase.com/docs/guides/auth/auth-helpers/nextjs#managing-sign-in-with-code-exchange
const requestUrl = new URL(request.url)
const code = requestUrl.searchParams.get('code')

if (code) {
const supabase = createRouteHandlerClient({ cookies })
await supabase.auth.exchangeCodeForSession(code)
}

// URL to redirect to after sign in process completes
return NextResponse.redirect(requestUrl.origin)
}
12 changes: 6 additions & 6 deletions app/api/chat/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { kv } from '@vercel/kv'
import { OpenAIStream, StreamingTextResponse } from 'ai'
import { Configuration, OpenAIApi } from 'openai-edge'
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
import { Database } from '@/lib/db_types'

import { auth } from '@/auth'
import { nanoid } from '@/lib/utils'
Expand All @@ -14,6 +16,7 @@ const configuration = new Configuration({
const openai = new OpenAIApi(configuration)

export async function POST(req: Request) {
const supabase = createRouteHandlerClient<Database>({ cookies })
const json = await req.json()
const { messages, previewToken } = json
const userId = (await auth())?.user.id
Expand Down Expand Up @@ -55,11 +58,8 @@ export async function POST(req: Request) {
}
]
}
await kv.hmset(`chat:${id}`, payload)
await kv.zadd(`user:chat:${userId}`, {
score: createdAt,
member: `chat:${id}`
})
// Insert chat into database.
await supabase.from('chats').upsert({ id, payload }).throwOnError()
}
})

Expand Down
4 changes: 2 additions & 2 deletions app/chat/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export async function generateMetadata({
return {}
}

const chat = await getChat(params.id, session.user.id)
const chat = await getChat(params.id)
return {
title: chat?.title.toString().slice(0, 50) ?? 'Chat'
}
Expand All @@ -36,7 +36,7 @@ export default async function ChatPage({ params }: ChatPageProps) {
redirect(`/sign-in?next=/chat/${params.id}`)
}

const chat = await getChat(params.id, session.user.id)
const chat = await getChat(params.id)

if (!chat) {
notFound()
Expand Down
2 changes: 1 addition & 1 deletion app/share/[id]/opengraph-image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export default async function Image({ params }: ImageProps) {
<div tw="flex text-[1.8rem] ml-4 text-[#9b9ba4]">
Built with{' '}
<div tw="flex text-[#eaeaf0] ml-2 mr-2">Vercel AI SDK</div> &
<div tw="flex text-[#eaeaf0] ml-2">KV</div>
<div tw="flex text-[#eaeaf0] ml-2">Supabase Auth & DB</div>
</div>
</div>
<div tw="text-[1.8rem] ml-auto text-[#9b9ba4]">chat.vercel.ai</div>
Expand Down
41 changes: 8 additions & 33 deletions auth.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,10 @@
import NextAuth, { type DefaultSession } from 'next-auth'
import GitHub from 'next-auth/providers/github'
import { NextResponse } from 'next/server'
import { createServerActionClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'

declare module 'next-auth' {
interface Session {
user: {
/** The user's id. */
id: string
} & DefaultSession['user']
}
export const auth = async () => {
// Create a Supabase client configured to use cookies
const supabase = createServerActionClient({ cookies })
const { data, error } = await supabase.auth.getSession()
if (error) throw error
return data.session
}

export const {
handlers: { GET, POST },
auth,
CSRF_experimental
} = NextAuth({
providers: [GitHub],
callbacks: {
jwt({ token, profile }) {
if (profile) {
token.id = profile.id
token.image = profile.picture
}
return token
},
authorized({ auth }) {
return !!auth?.user
}
},
pages: {
signIn: '/sign-in'
}
Comment on lines -14 to -34
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just to call out a potential issue - these are actual config/UX choices https://github.com/vercel-labs/ai-chatbot/pull/90/files i think you have mostly addressed them (except for the forced signin?) but just proactively calling it out

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah interesting, their deployed demo actually allows anonymous conversations without requiring you to log in. But yah, happy to add a middleware based redirect if user isn't logged in, is that your preference?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, added here with an comment that it's optional: 842803c

})
Loading