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

use Supabase for auth and database 🐘 #424

Draft
wants to merge 29 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
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
12 changes: 4 additions & 8 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,7 @@
# 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

# 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
# Update these with your Supabase details from your project settings > API
# https://app.supabase.com/project/_/settings/api
NEXT_PUBLIC_SUPABASE_URL=XXXXXXXX
NEXT_PUBLIC_SUPABASE_ANON_KEY=XXXXXXXX
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,6 @@ yarn-error.log*
.vercel
.vscode
.env*.local

# typescript
tsconfig.tsbuildinfo
40 changes: 32 additions & 8 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.
</p>

<p align="center">
Expand All @@ -21,6 +21,7 @@
- [Next.js](https://nextjs.org) App Router
- React Server Components (RSCs), Suspense, and Server Actions
- [Vercel AI SDK](https://sdk.vercel.ai/docs) for streaming chat UI
- [Supabase](https://supabase.com) for Chat History storage and auth
- Support for OpenAI (default), Anthropic, Cohere, Hugging Face, or custom AI chat models and/or LangChain
- [shadcn/ui](https://ui.shadcn.com)
- Styling with [Tailwind CSS](https://tailwindcss.com)
Expand All @@ -35,17 +36,38 @@ This template ships with OpenAI `gpt-3.5-turbo` as the default. However, thanks

## Deploy Your Own

You can deploy your own version of the Next.js AI Chatbot to Vercel with one click:
### Get an OpenAI API key

[![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_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"}])
Go to the [OpenAI Platform](https://platform.openai.com/api-keys) and login or signup, and create a new secret key. You'll need this key for the next step.

## Creating a KV Database Instance
### Deploy with Vercel

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.
Deploy your own version of the Next.js AI Chatbot to Vercel with one click:
Paste the OpenAI API key when prompted in Vercel.

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.
[![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=false&env=OPENAI_API_KEY%2CAUTH_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=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22supabase%22%2C%22productSlug%22%3A%22supabase%22%7D%5D)

## Running locally
### Store your Chat History in Supabase

Setup your Supabase project to store your Chat History:

1. In your new Vercel project, click the Storage tab at the top, then click the Supabase project you just created. From there, click “Open in Supabase”.
2. Copy the contents of [../supabase/migrations/20240916140519_init.sql](https://github.com/vercel/ai-chatbot/supabase/migrations/20240916140519_init.sql). While in
the Supabase dashbaord; Paste the SQL you copied into the SQL editor and click "run".

### Setup Redirect URLs for Authentication sign up:

1. While still in the Supabase dashbaord; Go to "Authentication", then "URL Configuration", and add in redirect URLs.
i. Add the following `https:/[vercel-deployment-url]-*-[VERCEL-TEAM-URL-SLUG].vercel.app/*/*`, `https://[vercel-deployment-url.vercel].app/**`
ii. For example, with the deployment url and team slug, these would look like, `https://nextjs-with-supabase-site.vercel.app/**`, `https://nextjs-with-supabase-site-*-supabase.vercel.app/**`

### Change the Auth confirmation path in the email confirmation template:

1. While still in the Supabase dashbaord; Go to "Authentication", then "Email templates"
2. In the `Confirm signup` template, change `{{ .ConfirmationURL }}` to `{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=signup`.


### Running locally

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/projects/environment-variables) for this, but a `.env` file is all that is necessary.

Expand All @@ -55,6 +77,8 @@ You will need to use the environment variables [defined in `.env.example`](.env.
2. Link local instance with Vercel and GitHub accounts (creates `.vercel` directory): `vercel link`
3. Download your environment variables: `vercel env pull`

Last, start your local server:

```bash
pnpm install
pnpm dev
Expand All @@ -64,7 +88,7 @@ Your app template should now be running on [localhost:3000](http://localhost:300

## Authors

This library is created by [Vercel](https://vercel.com) and [Next.js](https://nextjs.org) team members, with contributions from:
This library is created by [Vercel](https://vercel.com), [Next.js](https://nextjs.org) and [Supabase](https://supabase.com) team members, with contributions from:

- Jared Palmer ([@jaredpalmer](https://twitter.com/jaredpalmer)) - [Vercel](https://vercel.com)
- Shu Ding ([@shuding\_](https://twitter.com/shuding_)) - [Vercel](https://vercel.com)
Expand Down
130 changes: 78 additions & 52 deletions app/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

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 { createClient } from '@/utils/supabase/server'

export async function getChats(userId?: string | null) {
const session = await auth()
Expand All @@ -21,18 +21,16 @@ export async function getChats(userId?: string | null) {
}

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

for (const chat of chats) {
pipeline.hgetall(chat)
}

const results = await pipeline.exec()
const { data } = await supabase
.from('chats')
.select('payload')
.order('payload->createdAt', { ascending: false })
.eq('user_id', userId)
.throwOnError()

return results as Chat[]
return data?.map(chat => chat.payload as Chat) ?? []
} catch (error) {
return []
}
Expand All @@ -47,13 +45,15 @@ export async function getChat(id: string, userId: string) {
}
}

const chat = await kv.hgetall<Chat>(`chat:${id}`)
const supabase = createClient()

if (!chat || (userId && chat.userId !== userId)) {
return null
}
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 }) {
Expand All @@ -65,18 +65,15 @@ export async function removeChat({ id, path }: { id: string; path: string }) {
}
}

// Convert uid to string for consistent comparison with session.user.id
const uid = String(await kv.hget(`chat:${id}`, 'userId'))

if (uid !== session?.user?.id) {
try {
const supabase = createClient()
await supabase.from('chats').delete().eq('id', id).throwOnError()
} catch (_) {
return {
error: 'Unauthorized'
}
}

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

revalidatePath('/')
return revalidatePath(path)
}
Expand All @@ -90,31 +87,34 @@ export async function clearChats() {
}
}

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)
try {
const supabase = createClient()
await supabase
.from('chats')
.delete()
.eq('user_id', session.user.id)
.throwOnError()
} catch (_) {
return {
error: 'Unauthorized'
}
}

await pipeline.exec()

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

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

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

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

export async function shareChat(id: string) {
Expand All @@ -126,7 +126,15 @@ export async function shareChat(id: string) {
}
}

const chat = await kv.hgetall<Chat>(`chat:${id}`)
const supabase = createClient()

const { data } = await supabase
.from('chats')
.select('payload')
.eq('id', id)
.maybeSingle()

const chat = data?.payload as Chat | null

if (!chat || chat.userId !== session.user.id) {
return {
Expand All @@ -139,7 +147,11 @@ export async function shareChat(id: string) {
sharePath: `/share/${chat.id}`
}

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

return payload
}
Expand All @@ -148,21 +160,35 @@ export async function saveChat(chat: Chat) {
const session = await auth()

if (session && session.user) {
const pipeline = kv.pipeline()
pipeline.hmset(`chat:${chat.id}`, chat)
pipeline.zadd(`user:chat:${chat.userId}`, {
score: Date.now(),
member: `chat:${chat.id}`
})
await pipeline.exec()
try {
const supabase = createClient()
const { error } = await supabase.from('chats').upsert(
{
id: chat.id,
user_id: chat.userId,
payload: chat
},
{ onConflict: 'id' }
)
if (error) {
console.error('Error saving chat:', error)
return { error: error.message }
}
return { success: true }
} catch (err) {
console.error('Unexpected error saving chat:', err)
return { error: 'An unexpected error occurred' }
}
} else {
return
console.warn('Attempted to save chat without an active session')
return { error: 'No active session' }
}
}

export async function refreshHistory(path: string) {
redirect(path)
}
// not in use?
// export async function refreshHistory(path: string) {
// redirect(path)
// }

export async function getMissingKeys() {
const keysRequired = ['OPENAI_API_KEY']
Expand Down
23 changes: 23 additions & 0 deletions app/auth/confirm/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { EmailOtpType } from '@supabase/supabase-js'
import { createClient } from '@/utils/supabase/server'
import { redirect } from 'next/navigation'

export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const token_hash = searchParams.get('token_hash')
const type = searchParams.get('type') as EmailOtpType | null
const next = searchParams.get('next') ?? '/'

if (token_hash && type) {
const supabase = createClient()

const { error } = await supabase.auth.verifyOtp({
type,
token_hash
})
if (!error) {
// redirect user to specified redirect URL or root of app
redirect(next)
}
}
}
Loading