Skip to content

Commit 24a181d

Browse files
authored
Merge pull request supabase-community#81 from supabase-community/feat/rate-limits
feat: rate limits
2 parents 2480217 + af1645f commit 24a181d

File tree

10 files changed

+149
-4
lines changed

10 files changed

+149
-4
lines changed

apps/postgres-new/.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,7 @@ NEXT_PUBLIC_SUPABASE_URL="<supabase-api-url>"
33
NEXT_PUBLIC_IS_PREVIEW=true
44

55
OPENAI_API_KEY="<openai-api-key>"
6+
7+
# Vercel KV (local Docker available)
8+
KV_REST_API_URL="http://localhost:8080"
9+
KV_REST_API_TOKEN="local_token"

apps/postgres-new/README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,16 @@ From this directory (`./apps/postgres-new`):
4646
```shell
4747
echo 'OPENAI_API_KEY="<openai-api-key>"' >> .env.local
4848
```
49-
5. Start Next.js development server:
49+
5. Start local Redis containers (used for rate limiting). Serves an API on port 8080:
50+
```shell
51+
docker compose up -d
52+
```
53+
6. Store local KV (Redis) vars. Use these exact values:
54+
```shell
55+
echo 'KV_REST_API_URL="http://localhost:8080"' >> .env.local
56+
echo 'KV_REST_API_TOKEN="local_token"' >> .env.local
57+
```
58+
7. Start Next.js development server:
5059
```shell
5160
npm run dev
5261
```

apps/postgres-new/app/api/chat/route.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,51 @@
11
import { openai } from '@ai-sdk/openai'
2+
import { Ratelimit } from '@upstash/ratelimit'
3+
import { kv } from '@vercel/kv'
24
import { ToolInvocation, convertToCoreMessages, streamText } from 'ai'
35
import { codeBlock } from 'common-tags'
46
import { convertToCoreTools, maxMessageContext, maxRowLimit, tools } from '~/lib/tools'
7+
import { createClient } from '~/utils/supabase/server'
58

69
// Allow streaming responses up to 30 seconds
710
export const maxDuration = 30
811

12+
const inputTokenRateLimit = new Ratelimit({
13+
redis: kv,
14+
limiter: Ratelimit.fixedWindow(1000000, '30m'),
15+
prefix: 'ratelimit:tokens:input',
16+
})
17+
18+
const outputTokenRateLimit = new Ratelimit({
19+
redis: kv,
20+
limiter: Ratelimit.fixedWindow(10000, '30m'),
21+
prefix: 'ratelimit:tokens:output',
22+
})
23+
924
type Message = {
1025
role: 'user' | 'assistant'
1126
content: string
1227
toolInvocations?: (ToolInvocation & { result: any })[]
1328
}
1429

1530
export async function POST(req: Request) {
31+
const supabase = createClient()
32+
33+
const { data, error } = await supabase.auth.getUser()
34+
35+
// We have middleware, so this should never happen (used for type narrowing)
36+
if (error) {
37+
return new Response('Unauthorized', { status: 401 })
38+
}
39+
40+
const { user } = data
41+
42+
const { remaining: inputRemaining } = await inputTokenRateLimit.getRemaining(user.id)
43+
const { remaining: outputRemaining } = await outputTokenRateLimit.getRemaining(user.id)
44+
45+
if (inputRemaining <= 0 || outputRemaining <= 0) {
46+
return new Response('Rate limited', { status: 429 })
47+
}
48+
1649
const { messages }: { messages: Message[] } = await req.json()
1750

1851
// Trim the message context sent to the LLM to mitigate token abuse
@@ -64,6 +97,14 @@ export async function POST(req: Request) {
6497
model: openai('gpt-4o-2024-08-06'),
6598
messages: convertToCoreMessages(trimmedMessageContext),
6699
tools: convertToCoreTools(tools),
100+
async onFinish({ usage }) {
101+
await inputTokenRateLimit.limit(user.id, {
102+
rate: usage.promptTokens,
103+
})
104+
await outputTokenRateLimit.limit(user.id, {
105+
rate: usage.completionTokens,
106+
})
107+
},
67108
})
68109

69110
return result.toAIStreamResponse()

apps/postgres-new/components/app-provider.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export default function AppProvider({ children }: AppProps) {
2828
const [isLoadingUser, setIsLoadingUser] = useState(true)
2929
const [user, setUser] = useState<User>()
3030
const [isSignInDialogOpen, setIsSignInDialogOpen] = useState(false)
31+
const [isRateLimited, setIsRateLimited] = useState(false)
3132

3233
const focusRef = useRef<FocusHandle>(null)
3334

@@ -113,6 +114,8 @@ export default function AppProvider({ children }: AppProps) {
113114
signOut,
114115
isSignInDialogOpen,
115116
setIsSignInDialogOpen,
117+
isRateLimited,
118+
setIsRateLimited,
116119
focusRef,
117120
isPreview,
118121
dbManager,
@@ -136,6 +139,8 @@ export type AppContextValues = {
136139
signOut: () => Promise<void>
137140
isSignInDialogOpen: boolean
138141
setIsSignInDialogOpen: (open: boolean) => void
142+
isRateLimited: boolean
143+
setIsRateLimited: (limited: boolean) => void
139144
focusRef: RefObject<FocusHandle>
140145
isPreview: boolean
141146
dbManager?: DbManager

apps/postgres-new/components/chat.tsx

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { Message, generateId } from 'ai'
44
import { useChat } from 'ai/react'
55
import { AnimatePresence, m } from 'framer-motion'
6-
import { ArrowDown, ArrowUp, Paperclip, Square } from 'lucide-react'
6+
import { ArrowDown, ArrowUp, Flame, Paperclip, Square } from 'lucide-react'
77
import {
88
ChangeEvent,
99
FormEventHandler,
@@ -48,7 +48,7 @@ export function getInitialMessages(tables: TablesData): Message[] {
4848
}
4949

5050
export default function Chat() {
51-
const { user, isLoadingUser, focusRef, setIsSignInDialogOpen } = useApp()
51+
const { user, isLoadingUser, focusRef, setIsSignInDialogOpen, isRateLimited } = useApp()
5252
const [inputFocusState, setInputFocusState] = useState(false)
5353

5454
const {
@@ -261,6 +261,32 @@ export default function Chat() {
261261
isLast={i === messages.length - 1}
262262
/>
263263
))}
264+
<AnimatePresence initial={false}>
265+
{isRateLimited && !isLoading && (
266+
<m.div
267+
layout="position"
268+
className="flex flex-col gap-4 justify-start items-center max-w-96 p-4 bg-destructive rounded-md text-sm"
269+
variants={{
270+
hidden: { scale: 0 },
271+
show: { scale: 1, transition: { delay: 0.5 } },
272+
}}
273+
initial="hidden"
274+
animate="show"
275+
exit="hidden"
276+
>
277+
<Flame size={64} strokeWidth={1} />
278+
<div className="flex flex-col items-center text-start gap-4">
279+
<h3 className="font-bold">Hang tight!</h3>
280+
<p>
281+
We&apos;re seeing a lot of AI traffic from your end and need to temporarily
282+
pause your chats to make sure our servers don&apos;t melt.
283+
</p>
284+
285+
<p>Have a quick coffee break and try again in a few minutes!</p>
286+
</div>
287+
</m.div>
288+
)}
289+
</AnimatePresence>
264290
<AnimatePresence>
265291
{isLoading && (
266292
<m.div

apps/postgres-new/components/workspace.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { useTablesQuery } from '~/data/tables/tables-query'
88
import { useOnToolCall } from '~/lib/hooks'
99
import { useBreakpoint } from '~/lib/use-breakpoint'
1010
import { ensureMessageId, ensureToolResult } from '~/lib/util'
11+
import { useApp } from './app-provider'
1112
import Chat, { getInitialMessages } from './chat'
1213
import IDE from './ide'
1314

@@ -51,6 +52,7 @@ export default function Workspace({
5152
onReply,
5253
onCancelReply,
5354
}: WorkspaceProps) {
55+
const { setIsRateLimited } = useApp()
5456
const isSmallBreakpoint = useBreakpoint('lg')
5557
const onToolCall = useOnToolCall(databaseId)
5658
const { mutateAsync: saveMessage } = useMessageCreateMutation(databaseId)
@@ -76,6 +78,9 @@ export default function Workspace({
7678
await onReply?.(message, append)
7779
await saveMessage({ message })
7880
},
81+
async onResponse(response) {
82+
setIsRateLimited(response.status === 429)
83+
},
7984
})
8085

8186
const appendMessage = useCallback(

apps/postgres-new/docker-compose.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
services:
2+
redis:
3+
image: redis
4+
local-vercel-kv:
5+
image: hiett/serverless-redis-http:latest
6+
ports:
7+
- 8080:80
8+
environment:
9+
SRH_MODE: env
10+
SRH_TOKEN: local_token
11+
SRH_CONNECTION_STRING: redis://redis:6379

apps/postgres-new/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
"@supabase/ssr": "^0.4.0",
2727
"@supabase/supabase-js": "^2.45.0",
2828
"@tanstack/react-query": "^5.45.0",
29+
"@upstash/ratelimit": "^2.0.1",
30+
"@vercel/kv": "^2.0.0",
2931
"@xenova/transformers": "^2.17.2",
3032
"ai": "^3.2.8",
3133
"chart.js": "^4.4.3",

package-lock.json

Lines changed: 40 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
"scripts": {
44
"dev": "npm run dev --workspace postgres-new"
55
},
6-
"workspaces": ["apps/*"],
6+
"workspaces": [
7+
"apps/*"
8+
],
79
"devDependencies": {
810
"supabase": "^1.187.8"
911
}

0 commit comments

Comments
 (0)