Skip to content

Commit

Permalink
Merge pull request #1 from supabase-community/supabaseify
Browse files Browse the repository at this point in the history
Supabaseify
  • Loading branch information
thorwebdev committed Jul 7, 2023
2 parents 012ea40 + 045b0b9 commit 2a68048
Show file tree
Hide file tree
Showing 25 changed files with 837 additions and 311 deletions.
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'
}
})

0 comments on commit 2a68048

Please sign in to comment.