UpstashがPlanTripAIの急成長を支える技術戦略と成功事例を徹底解説
本記事では、AI駆動型の旅行プランニングスタートアップ「PlanTripAI」の開発において、Upstashが果たした重要な役割について詳しく解説します。Upstash Redisの活用は、ユーザーアクセス用のライセンスキーの保存や、旅行コンテンツの効率的な保存・キャッシュといった重要な要素の管理に不可欠でした。さらに、UpstashのRate Limit機能はシステム保護において重要な役割を担っています。リクエスト頻度を適切に制御することで、潜在的なセキュリティ脅威から堅牢に守り、ユーザーへ快適で中断のないサービスを提供することを可能にしています。
plantripai.comは、目的地、滞在日数、旅のスタイル、予算などのユーザーの好みに基づいて、パーソナライズされた旅程表を瞬時に作成できるAIベースの旅行プランナーです。無制限の旅程作成に対応しており、さまざまなダウンロード形式も提供しています。
- @upstash/redis
- @upstash/ratelimit
- next.js
仕組みの概要
AI企業では通常、ユーザーが製品を試せる複数の手段を用意しています。PlanTripAIでも、フリーミアムプランと有料プランの両方を提供しています。
最も人気のあるフリーミアムプランでは、有効なライセンスを持たないユーザーが旅行プランを作成するたびに、新しいランダムキーが生成され、旅行コンテンツとともにRedisハッシュとして保存されます(redis.hset(trip:${key}, data))。このキーにより、旅行詳細ページへの一時的なアクセスが可能になります。アクセス期限が切れた後、ユーザーはその旅行プランへの永続的なアクセスを維持するかどうかを選択できます。
また、フリーミアムユーザーには10秒あたり3リクエストというレート制限(fixedWindow)が適用されており、アプリケーションへの潜在的な攻撃を防ぐのに役立っています。

有料プランについては、ユーザーは決済ゲートウェイを通じて製品を購入します。取引が正常に完了すると、決済ゲートウェイのWebhookが成功イベントを発行し、有効なキーとしてRedisに保存されます(redis.set(${licenseKey}, true))。
有効なライセンスを持つユーザーが旅行プランを作成する場合も、ランダムキーが生成されRedisに保存されます。加えて、新しい旅行プランはユーザーオブジェクトに格納され、ライセンスキーと紐付けられます。
有料ユーザーはフリーミアムユーザーよりも寛容なレート制限の恩恵を受けられます。この強化された機能は、Redisで有効なキーの存在を確認することで動的に提供されます。

旅行データの保存
PlanTripAIは、ユーザーが作成した旅行プランの保存にRedisを活用しています。高速なキーバリューストアとして高い効率性を発揮するため、Redisは私たちにとって理想的な選択肢となりました。一度作成された旅行プランは不変であること、そしてハッシュを使用することでキーデータと一緒にメタデータも保存できることがその理由です。
旅行データは、以下のような構造でRedisハッシュに格納されます:
{
"itinerary": [
{ "day": "Day 1", "data": [...] }
],
"info": "This itinerary is designed for a city explorer visiting Paris, France for 2 days.",
"inputs": {
"city": "Paris, France",
"days": 2,
"accommodation": "Paris France Hotel",
"kind": "city explorer",
"currency": "USD",
"budget": 3000,
"transportation": "bus"
},
"createdAt": ...,
"shareable": false
}
HTTP経由のUpstash Redis APIを使用することで、データは無料のRedisデータベースに保存されます。無料枠では1日10,000リクエストまで利用でき、私たちのニーズに対して十分すぎるほどの容量です。
import { Redis } from "@upstash/redis";
const redis = new Redis({
url: "..." // UPSTASH_REDIS_REST_URL
token: "..." // UPSTASH_REDIS_REST_TOKEN
});
redis.hset(`trip:${id}`, data);
有効なライセンスキーの保存
前述のとおり、PlanTripAIのすべての機能にアクセスする方法の一つが有料サブスクリプションです。各有料ユーザーには有効なライセンスキーが割り当てられ、Redisに保存されます。キーバリューデータベースとして優れた効率性を持つRedisは、迅速な検証チェックに最適な仕組みです。
ユーザーを有効な状態としてマークするには、次のコマンドを実行します:
import { Redis } from "@upstash/redis";
const redis = new Redis({
url: "..." // UPSTASH_REDIS_REST_URL
token: "..." // UPSTASH_REDIS_REST_TOKEN
});
await redis.set(licenseKey, true);
ユーザーの有効性を検証する際は、以下のコマンドを使用します:
import { Redis } from "@upstash/redis";
const redis = new Redis({
url: "..." // UPSTASH_REDIS_REST_URL
token: "..." // UPSTASH_REDIS_REST_TOKEN
});
const redisLicenseKey = await redis.get(licenseKey);
const valid = Boolean(redisLicenseKey);
if (!valid) {
return new Response(
JSON.stringify({ message: "The license key is invalid!" }),
{
status: 401,
}
);
}
// successful code here...
レート制限の実装
アプリケーションを保護するうえでの重要な機能が、Upstash Rate Limitによって支えられたレート制限ロジックです。詳細については公式ドキュメントをご覧ください。このサービスは、フリー・有料を問わずすべてのユーザーにシームレスな体験を提供するために不可欠であり、アプリケーション全体に影響を与えることなく、悪意あるユーザーからの攻撃を数多く防いできました。
以下は、Next.jsをミドルウェアとして使用したコードスニペットです。このコードにより、ユーザーのIPチェックを管理し、後続のリクエストを処理するかどうかを判断できます。
import {
NextResponse,
type NextFetchEvent,
type NextRequest,
} from "next/server";
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
const cache = new Map();
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.fixedWindow(3, "10s"),
ephemeralCache: cache,
analytics: true,
});
export default async function middleware(
request: NextRequest,
event: NextFetchEvent
): Promise<Response | undefined> {
const id = request.ip ?? "anonymous";
// optional hard coded IPs
const blockeds = [];
if (blockeds.includes(id.trim())) {
new NextResponse(
JSON.stringify({
message: "Blocked :)",
}),
{ status: 429, headers: { "Content-Type": "application/json" } }
);
}
const { success, pending, limit, reset, remaining } = await ratelimit.limit(
id
);
event.waitUntil(pending);
request.headers.set("X-RateLimit-Limit", limit.toString());
request.headers.set("X-RateLimit-Remaining", remaining.toString());
request.headers.set("X-RateLimit-Reset", reset.toString());
return success
? NextResponse.next()
: new NextResponse(
JSON.stringify({
message:
"Request cannot be processed! You sent too many requests in a given amount of time.",
}),
{ status: 429, headers: { "Content-Type": "application/json" } }
);
}
export const config = {
matcher: ["/api/generate-trip", "/api/get-license/(.*)"],
};
まとめ
最後までお読みいただきありがとうございます。PlanTripAIは、SaaSフレームワークにおいてUpstash RedisとNext.jsを組み合わせることに興味を持ったことから生まれたサイドプロジェクトでした。
本記事が有益で示唆に富むものだったことを心より願っています。ご質問やフィードバックは大歓迎です。ご質問やコメントがありましたら、Twitterでお気軽にご連絡ください。
-
Redis GEOHASHコマンドの使い方 – 地理空間データのメンバーからジオハッシュ文字列を取得する方法
このチュートリアルでは、Redisに保存された地理空間値(ジオスペーシャル値)に含まれる1つまたは複数のメンバー(要素)のGeohash文字列を取得する方法を解説します。この目的には、RedisのGEOHASHコマンドを使用します。 GEOHASHコマンドとは GEOHASHコマンドは、キーに保存された地理空間値の中から指定された1つ以上のメンバーに対して、有効なGeohash文字列を返すためのコマンドです。地理空間値はソート済みセット(sorted set)として表現され、GEOADDコマンドを使って登録されます。 Redisは地理空間要素の位置情報(経度・緯度)を、Geohash技術の変種
-
【Redis入門】LPOP・BLPOPコマンドでリストの先頭要素を削除して取得する方法
このチュートリアルでは、Redisデータストア内のキーに保存されたリスト値の先頭(最初)の要素を削除して取得する方法を解説します。そのために、RedisのLPOPコマンドとBLPOPコマンドを使用します。 LPOPコマンドとは LPOPコマンドは、指定されたキーに格納されているリスト値の先頭要素を削除し、その要素を返します。キーが存在しない場合はnilが返され、キーは存在するものの格納されている値がリスト型でない場合にはエラーが返されます。Redis LPOPコマンドの構文は以下のとおりです。 構文 redis host:post> LPOP <keyname> 戻り値 -