Skip to content

Commit 2052847

Browse files
author
thorwebdev
committed
feat: migrate kv to postgres.
1 parent 842803c commit 2052847

File tree

13 files changed

+392
-178
lines changed

13 files changed

+392
-178
lines changed

.env.example

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,6 @@
22
## Then get your OpenAI API Key here: https://platform.openai.com/account/api-keys
33
OPENAI_API_KEY=XXXXXXXX
44

5-
# Instructions to create kv database here: https://vercel.com/docs/storage/vercel-kv/quickstart and
6-
KV_URL=XXXXXXXX
7-
KV_REST_API_URL=XXXXXXXX
8-
KV_REST_API_TOKEN=XXXXXXXX
9-
KV_REST_API_READ_ONLY_TOKEN=XXXXXXXX
10-
115
# Update these with your Supabase details from your project settings > API
126
# https://app.supabase.com/project/_/settings/api
137
# In local dev you can get these by running `supabase status`.

README.md

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
</a>
55

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

1010
<p align="center">
@@ -27,7 +27,7 @@
2727
- Styling with [Tailwind CSS](https://tailwindcss.com)
2828
- [Radix UI](https://radix-ui.com) for headless component primitives
2929
- Icons from [Phosphor Icons](https://phosphoricons.com)
30-
- Chat History, rate limiting, and session storage with [Vercel KV](https://vercel.com/storage/kv)
30+
- Chat History with [Supabase Postgres DB](https://supabase.com)
3131
- [Supabase Auth](https://supabase.com/auth) for authentication
3232

3333
## Model Providers
@@ -41,12 +41,6 @@ You can deploy your own version of the Next.js AI Chatbot to Vercel with one cli
4141
TODO: update button with supabase integration
4242
[![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"}]) -->
4343

44-
## Creating a KV Database Instance
45-
46-
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.
47-
48-
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.
49-
5044
## Running locally
5145

5246
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.

app/actions.ts

Lines changed: 48 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,120 +1,97 @@
11
'use server'
22

3+
import { createServerActionClient } from '@supabase/auth-helpers-nextjs'
4+
import { cookies } from 'next/headers'
5+
import { Database } from '@/lib/db_types'
36
import { revalidatePath } from 'next/cache'
47
import { redirect } from 'next/navigation'
5-
import { kv } from '@vercel/kv'
68

7-
import { auth } from '@/auth'
89
import { type Chat } from '@/lib/types'
10+
import { auth } from '@/auth'
11+
12+
const supabase = createServerActionClient<Database>({ cookies })
913

1014
export async function getChats(userId?: string | null) {
1115
if (!userId) {
1216
return []
1317
}
1418

1519
try {
16-
const pipeline = kv.pipeline()
17-
const chats: string[] = await kv.zrange(`user:chat:${userId}`, 0, -1, {
18-
rev: true
19-
})
20-
21-
for (const chat of chats) {
22-
pipeline.hgetall(chat)
23-
}
20+
const { data } = await supabase
21+
.from('chats')
22+
.select('payload')
23+
.order('payload->createdAt', { ascending: false })
24+
.throwOnError()
2425

25-
const results = await pipeline.exec()
26-
27-
return results as Chat[]
26+
return (data?.map(entry => entry.payload) as Chat[]) ?? []
2827
} catch (error) {
2928
return []
3029
}
3130
}
3231

33-
export async function getChat(id: string, userId: string) {
34-
const chat = await kv.hgetall<Chat>(`chat:${id}`)
35-
36-
if (!chat || (userId && chat.userId !== userId)) {
37-
return null
38-
}
32+
export async function getChat(id: string) {
33+
const { data } = await supabase
34+
.from('chats')
35+
.select('payload')
36+
.eq('id', id)
37+
.maybeSingle()
3938

40-
return chat
39+
return (data?.payload as Chat) ?? null
4140
}
4241

4342
export async function removeChat({ id, path }: { id: string; path: string }) {
44-
const session = await auth()
45-
46-
if (!session) {
47-
return {
48-
error: 'Unauthorized'
49-
}
50-
}
51-
52-
const uid = await kv.hget<string>(`chat:${id}`, 'userId')
43+
try {
44+
await supabase.from('chats').delete().eq('id', id).throwOnError()
5345

54-
if (uid !== session?.user?.id) {
46+
revalidatePath('/')
47+
return revalidatePath(path)
48+
} catch (error) {
5549
return {
5650
error: 'Unauthorized'
5751
}
5852
}
59-
60-
await kv.del(`chat:${id}`)
61-
await kv.zrem(`user:chat:${session.user.id}`, `chat:${id}`)
62-
63-
revalidatePath('/')
64-
return revalidatePath(path)
6553
}
6654

6755
export async function clearChats() {
68-
const session = await auth()
69-
70-
if (!session?.user?.id) {
56+
try {
57+
const session = await auth()
58+
await supabase
59+
.from('chats')
60+
.delete()
61+
.eq('user_id', session?.user.id)
62+
.throwOnError()
63+
revalidatePath('/')
64+
return redirect('/')
65+
} catch (error) {
66+
console.log('clear chats error', error)
7167
return {
7268
error: 'Unauthorized'
7369
}
7470
}
75-
76-
const chats: string[] = await kv.zrange(`user:chat:${session.user.id}`, 0, -1)
77-
if (!chats.length) {
78-
return redirect('/')
79-
}
80-
const pipeline = kv.pipeline()
81-
82-
for (const chat of chats) {
83-
pipeline.del(chat)
84-
pipeline.zrem(`user:chat:${session.user.id}`, chat)
85-
}
86-
87-
await pipeline.exec()
88-
89-
revalidatePath('/')
90-
return redirect('/')
9171
}
9272

9373
export async function getSharedChat(id: string) {
94-
const chat = await kv.hgetall<Chat>(`chat:${id}`)
95-
96-
if (!chat || !chat.sharePath) {
97-
return null
98-
}
99-
100-
return chat
74+
const { data } = await supabase
75+
.from('chats')
76+
.select('payload')
77+
.eq('id', id)
78+
.not('payload->sharePath', 'is', null)
79+
.maybeSingle()
80+
81+
return (data?.payload as Chat) ?? null
10182
}
10283

10384
export async function shareChat(chat: Chat) {
104-
const session = await auth()
105-
106-
if (!session?.user?.id || session.user.id !== chat.userId) {
107-
return {
108-
error: 'Unauthorized'
109-
}
110-
}
111-
11285
const payload = {
11386
...chat,
11487
sharePath: `/share/${chat.id}`
11588
}
11689

117-
await kv.hmset(`chat:${chat.id}`, payload)
90+
await supabase
91+
.from('chats')
92+
.update({ payload: payload as any })
93+
.eq('id', chat.id)
94+
.throwOnError()
11895

11996
return payload
12097
}

app/api/chat/route.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import { kv } from '@vercel/kv'
21
import { OpenAIStream, StreamingTextResponse } from 'ai'
32
import { Configuration, OpenAIApi } from 'openai-edge'
3+
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
4+
import { cookies } from 'next/headers'
5+
import { Database } from '@/lib/db_types'
46

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

1618
export async function POST(req: Request) {
19+
const supabase = createRouteHandlerClient<Database>({ cookies })
1720
const json = await req.json()
1821
const { messages, previewToken } = json
1922
const userId = (await auth())?.user.id
@@ -55,11 +58,8 @@ export async function POST(req: Request) {
5558
}
5659
]
5760
}
58-
await kv.hmset(`chat:${id}`, payload)
59-
await kv.zadd(`user:chat:${userId}`, {
60-
score: createdAt,
61-
member: `chat:${id}`
62-
})
61+
// Insert chat into database.
62+
await supabase.from('chats').upsert({ id, payload }).throwOnError()
6363
}
6464
})
6565

app/chat/[id]/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export async function generateMetadata({
2323
return {}
2424
}
2525

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

39-
const chat = await getChat(params.id, session.user.id)
39+
const chat = await getChat(params.id)
4040

4141
if (!chat) {
4242
notFound()

app/share/[id]/opengraph-image.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export default async function Image({ params }: ImageProps) {
9090
<div tw="flex text-[1.8rem] ml-4 text-[#9b9ba4]">
9191
Built with{' '}
9292
<div tw="flex text-[#eaeaf0] ml-2 mr-2">Vercel AI SDK</div> &
93-
<div tw="flex text-[#eaeaf0] ml-2">KV</div>
93+
<div tw="flex text-[#eaeaf0] ml-2">Supabase Auth & DB</div>
9494
</div>
9595
</div>
9696
<div tw="text-[1.8rem] ml-auto text-[#9b9ba4]">chat.vercel.ai</div>

components/empty-screen.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,7 @@ export function EmptyScreen({ setInput }: Pick<UseChatHelpers, 'setInput'>) {
2929
<p className="mb-2 leading-normal text-muted-foreground">
3030
This is an open source AI chatbot app template built with{' '}
3131
<ExternalLink href="https://nextjs.org">Next.js</ExternalLink> and{' '}
32-
<ExternalLink href="https://vercel.com/storage/kv">
33-
Vercel KV
34-
</ExternalLink>
35-
.
32+
<ExternalLink href="https://supabase.com">Supabase</ExternalLink>.
3633
</p>
3734
<p className="leading-normal text-muted-foreground">
3835
You can start a conversation here or try the following examples:

components/footer.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,7 @@ export function FooterText({ className, ...props }: React.ComponentProps<'p'>) {
1414
>
1515
Open source AI chatbot built with{' '}
1616
<ExternalLink href="https://nextjs.org">Next.js</ExternalLink> and{' '}
17-
<ExternalLink href="https://vercel.com/storage/kv">
18-
Vercel KV
19-
</ExternalLink>
20-
.
17+
<ExternalLink href="https://supabase.com">Supabase</ExternalLink>.
2118
</p>
2219
)
2320
}

0 commit comments

Comments
 (0)