Update guidance around KV storage for rate limiting

This commit is contained in:
Sam Becker 2025-02-16 19:44:55 -06:00
parent 73ec9b8f87
commit 7c98c55853
10 changed files with 48 additions and 61 deletions

View File

@ -77,7 +77,7 @@ _⚠ READ BEFORE PROCEEDING_
- Generate an API key and store in environment variable `OPENAI_SECRET_KEY`
- Setup usage limits to avoid unexpected charges (_recommended_)
2. Add rate limiting (_recommended_)
- As an additional precaution, create a [Vercel KV](https://vercel.com/docs/storage/vercel-kv/quickstart#create-a-kv-database) store and link it to your project in order to enable rate limiting—no further configuration necessary
- As an additional precaution, create an Upstash Redis store from the storage tab of the Vercel dashboard and link it to your project in order to enable rate limiting—no further configuration necessary
3. Configure auto-generated fields (optional)
- Set which text fields auto-generate when uploading a photo by storing a comma-separated list, e.g., `AI_TEXT_AUTO_GENERATED_FIELDS = title, semantic`
- Accepted values:

View File

@ -17,9 +17,9 @@
"@radix-ui/react-tooltip": "^1.1.8",
"@radix-ui/react-visually-hidden": "^1.1.2",
"@upstash/ratelimit": "^2.0.5",
"@upstash/redis": "^1.34.4",
"@vercel/analytics": "^1.5.0",
"@vercel/blob": "^0.27.1",
"@vercel/kv": "^3.0.0",
"@vercel/speed-insights": "^1.2.0",
"ai": "^4.1.34",
"camelcase-keys": "^9.1.3",

35
pnpm-lock.yaml generated
View File

@ -31,16 +31,16 @@ importers:
version: 1.1.2(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@upstash/ratelimit':
specifier: ^2.0.5
version: 2.0.5(@upstash/redis@1.34.3)
version: 2.0.5(@upstash/redis@1.34.4)
'@upstash/redis':
specifier: ^1.34.4
version: 1.34.4
'@vercel/analytics':
specifier: ^1.5.0
version: 1.5.0(next@15.1.7(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(svelte@4.2.17)(vue@3.4.27(typescript@5.7.3))
'@vercel/blob':
specifier: ^0.27.1
version: 0.27.1
'@vercel/kv':
specifier: ^3.0.0
version: 3.0.0
'@vercel/speed-insights':
specifier: ^1.2.0
version: 1.2.0(next@15.1.7(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(svelte@4.2.17)(vue@3.4.27(typescript@5.7.3))
@ -1763,11 +1763,8 @@ packages:
peerDependencies:
'@upstash/redis': ^1.34.3
'@upstash/redis@1.31.3':
resolution: {integrity: sha512-KtVgWBUEx/LGbR8oRwYexwzHh3s5DNqYW0bjkD+gjFZVOnREJITvK+hC4PjSSD+8D4qJ+Xbkfmy8ANADZ9EUFg==}
'@upstash/redis@1.34.3':
resolution: {integrity: sha512-VT25TyODGy/8ljl7GADnJoMmtmJ1F8d84UXfGonRRF8fWYJz7+2J6GzW+a6ETGtk4OyuRTt7FRSvFG5GvrfSdQ==}
'@upstash/redis@1.34.4':
resolution: {integrity: sha512-AZx2iD5s1Pu/KCrRA7KVCffu3NSoaYnNY7N9YI7aLAYhcJfsriQKTe+8OxQWJqGqFbrvm17Lyr9HFnDLvqNpfA==}
'@vercel/analytics@1.5.0':
resolution: {integrity: sha512-MYsBzfPki4gthY5HnYN7jgInhAZ7Ac1cYDoRWFomwGHWEX7odTEzbtg9kf/QSo7XEsEAqlQugA6gJ2WS2DEa3g==}
@ -1799,10 +1796,6 @@ packages:
resolution: {integrity: sha512-X5EG9W1cZW+Nbt/XdrJJSl5DzCXXn1JRP5nfFwkpFD03nB6uh6BldwX4syElHcciM4Pih8CS7Ri1mtLCJvxSHA==}
engines: {node: '>=16.14'}
'@vercel/kv@3.0.0':
resolution: {integrity: sha512-pKT8fRnfyYk2MgvyB6fn6ipJPCdfZwiKDdw7vB+HL50rjboEBHDVBEcnwfkEpVSp2AjNtoaOUH7zG+bVC/rvSg==}
engines: {node: '>=14.6'}
'@vercel/speed-insights@1.2.0':
resolution: {integrity: sha512-y9GVzrUJ2xmgtQlzFP2KhVRoCglwfRQgjyfY607aU0hh0Un6d0OUyrJkjuAlsV18qR4zfoFPs/BiIj9YDS6Wzw==}
peerDependencies:
@ -6500,18 +6493,14 @@ snapshots:
'@upstash/core-analytics@0.0.10':
dependencies:
'@upstash/redis': 1.31.3
'@upstash/redis': 1.34.4
'@upstash/ratelimit@2.0.5(@upstash/redis@1.34.3)':
'@upstash/ratelimit@2.0.5(@upstash/redis@1.34.4)':
dependencies:
'@upstash/core-analytics': 0.0.10
'@upstash/redis': 1.34.3
'@upstash/redis': 1.34.4
'@upstash/redis@1.31.3':
dependencies:
crypto-js: 4.2.0
'@upstash/redis@1.34.3':
'@upstash/redis@1.34.4':
dependencies:
crypto-js: 4.2.0
@ -6530,10 +6519,6 @@ snapshots:
throttleit: 2.1.0
undici: 5.28.4
'@vercel/kv@3.0.0':
dependencies:
'@upstash/redis': 1.34.3
'@vercel/speed-insights@1.2.0(next@15.1.7(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(svelte@4.2.17)(vue@3.4.27(typescript@5.7.3))':
optionalDependencies:
next: 15.1.7(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)

View File

@ -33,7 +33,7 @@ export default function AdminAppConfigurationClient({
hasDatabase,
isPostgresSslEnabled,
hasVercelPostgres,
hasVercelKv,
hasRedisStorage,
hasStorageProvider,
hasVercelBlobStorage,
hasCloudflareR2Storage,
@ -95,7 +95,7 @@ export default function AdminAppConfigurationClient({
// Connection status
databaseError,
storageError,
kvError,
redisError,
aiError,
// Component props
simplifiedView,
@ -358,25 +358,19 @@ export default function AdminAppConfigurationClient({
{renderEnvVars(['OPENAI_SECRET_KEY'])}
</ChecklistRow>
<ChecklistRow
title={hasVercelKv && isAnalyzingConfiguration
? 'Testing KV connection'
title={hasRedisStorage && isAnalyzingConfiguration
? 'Testing Redis connection'
: 'Enable rate limiting'}
status={hasVercelKv}
isPending={hasVercelKv && isAnalyzingConfiguration}
status={hasRedisStorage}
isPending={hasRedisStorage && isAnalyzingConfiguration}
optional
>
{kvError && renderError({
connection: { provider: 'Vercel KV', error: kvError},
{redisError && renderError({
connection: { provider: 'Redis', error: redisError},
})}
<AdminLink
// eslint-disable-next-line max-len
href="https://vercel.com/docs/storage/vercel-kv/quickstart#create-a-kv-database"
externalIcon
>
Create Vercel KV store
</AdminLink>
{' '}
and connect to project in order to enable rate limiting
Create Upstash Redis store from storage tab
on Vercel dashboard and connect to this project
to enable rate limiting
</ChecklistRow>
<ChecklistRow
// eslint-disable-next-line max-len

View File

@ -1,7 +1,7 @@
'use server';
import { runAuthenticatedAdminServerAction } from '@/auth';
import { testKvConnection } from '@/platforms/kv';
import { testRedisConnection } from '@/platforms/redis';
import { testOpenAiConnection } from '@/platforms/openai';
import { testDatabaseConnection } from '@/platforms/postgres';
import { testStorageConnection } from '@/platforms/storage';
@ -22,26 +22,26 @@ export const testConnectionsAction = async () =>
const {
hasDatabase,
hasStorageProvider,
hasVercelKv,
hasRedisStorage,
isAiTextGenerationEnabled,
} = APP_CONFIGURATION;
const [
databaseError,
storageError,
kvError,
redisError,
aiError,
] = await Promise.all([
scanForError(hasDatabase, testDatabaseConnection),
scanForError(hasStorageProvider, testStorageConnection),
scanForError(hasVercelKv, testKvConnection),
scanForError(hasRedisStorage, testRedisConnection),
scanForError(isAiTextGenerationEnabled, testOpenAiConnection),
]);
return {
databaseError,
storageError,
kvError,
redisError,
aiError,
};
});

View File

@ -234,8 +234,9 @@ export default function AdminAppInsightsClient({
'yellow',
)}
expandContent={<>
Create Vercel KV store and link to this project
in order prevent abuse by to enabling rate limiting.
Create Upstash Redis store from storage tab on
Vercel dashboard and link to this project to
prevent abuse by enabling rate limiting.
</>}
/>}
{(noStaticOptimization || debug) && <ScoreCardRow

View File

@ -111,8 +111,8 @@ export const HAS_DATABASE =
export const POSTGRES_SSL_ENABLED =
process.env.DISABLE_POSTGRES_SSL === '1' ? false : true;
// STORAGE: VERCEL KV
export const HAS_VERCEL_KV =
// STORAGE: REDIS
export const HAS_REDIS_STORAGE =
Boolean(process.env.KV_URL);
// STORAGE: VERCEL BLOB
@ -261,7 +261,7 @@ export const APP_CONFIGURATION = {
/\/verceldb\?/.test(process.env.POSTGRES_URL ?? '') ||
/\.vercel-storage\.com\//.test(process.env.POSTGRES_URL ?? '')
),
hasVercelKv: HAS_VERCEL_KV,
hasRedisStorage: HAS_REDIS_STORAGE,
hasVercelBlobStorage: HAS_VERCEL_BLOB_STORAGE,
hasCloudflareR2Storage: HAS_CLOUDFLARE_R2_STORAGE,
hasAwsS3Storage: HAS_AWS_S3_STORAGE,

View File

@ -1,3 +0,0 @@
import { kv } from '@vercel/kv';
export const testKvConnection = () => kv.get('test');

View File

@ -1,12 +1,17 @@
import { generateText, streamText } from 'ai';
import { createStreamableValue } from 'ai/rsc';
import { createOpenAI } from '@ai-sdk/openai';
import { kv } from '@vercel/kv';
import { Redis } from '@upstash/redis';
import { Ratelimit } from '@upstash/ratelimit';
import { AI_TEXT_GENERATION_ENABLED, HAS_VERCEL_KV } from '@/app-core/config';
import {
AI_TEXT_GENERATION_ENABLED,
HAS_REDIS_STORAGE,
} from '@/app-core/config';
import { removeBase64Prefix } from '@/utility/image';
import { cleanUpAiTextResponse } from '@/photo/ai';
const redis = Redis.fromEnv();
const RATE_LIMIT_IDENTIFIER = 'openai-image-query';
const RATE_LIMIT_MAX_QUERIES_PER_HOUR = 100;
const MODEL = 'gpt-4o';
@ -15,9 +20,9 @@ const openai = AI_TEXT_GENERATION_ENABLED
? createOpenAI({ apiKey: process.env.OPENAI_SECRET_KEY })
: undefined;
const ratelimit = HAS_VERCEL_KV
const ratelimit = HAS_REDIS_STORAGE
? new Ratelimit({
redis: kv,
redis,
limiter: Ratelimit.slidingWindow(RATE_LIMIT_MAX_QUERIES_PER_HOUR, '1h'),
})
: undefined;

5
src/platforms/redis.ts Normal file
View File

@ -0,0 +1,5 @@
import { Redis } from '@upstash/redis';
const redis = Redis.fromEnv();
export const testRedisConnection = () => redis.get('test');