Astro・Upstash・GitHubで作る無料オープンソースのLinkTree代替サービス構築ガイド
この記事では、LinkTreeのオープンソース代替サービスであるitsmy.fyiが、Upstash・Astro・GitHub・Edgioをどのように組み合わせて構築されているのかを詳しく解説します。Upstashは全ユーザーのデータ管理(CRUD操作)を担い、GitHub APIと比べてはるかに寛容なレート制限を提供してくれるため、きめ細かなレートリミットの実装も容易になりました。

使用する技術スタック
- Astro — フロントエンドおよびバックエンド
- Upstash — レート制限とCRUD操作
- GitHub Issue & Webhooks — ユーザープロフィールを管理する公開CMSとして
- Tailwind CSS — スタイリング
- Edgio — デプロイメント
事前に必要なもの
- GitHubアカウント
- データベース作成用のUpstashアカウント
Upstash Redisのセットアップ
Upstashアカウントを作成してログインしたら、「Redis」タブに移動し、データベースを作成します。


データベースを作成したら、「Details」タブへ移動します。スクロールしてConnect your databaseセクションを見つけたら、その内容をコピーして安全な場所に保存しておきましょう。

さらに下にスクロールして「REST API」セクションを見つけ、「.env」ボタンを選択します。表示された内容も同様にコピーして保存してください。

プロジェクトのセットアップ
セットアップは、アプリのリポジトリをクローンするだけです。このチュートリアルを読み進めることで、リポジトリの中身をすべて理解できるようになります。プロジェクトをフォークするには、以下のコマンドを実行します。
git clone https://github.com/rishi-raj-jain/itsmy.fyi
cd itsmy.fyi
yarn install
リポジトリをクローンしたら、.envファイルを作成し、前述の手順で保存した項目を追加します。ファイルは次のようになります。
.env
# Obtained from your GitHub repo
GITHUB_API_TOKEN="to_create_and_update_github_comments"
GITHUB_WEBHOOK_SECRET="if_you_are_matching_github_webhooks_sha256"
# Obtained from the steps as above
UPSTASH_DB="your_upstash_redis_from_above"
UPSTASH_REDIS_REST_URL="your_upstash_redis_rest__url_from_above"
UPSTASH_REDIS_REST_TOKEN="your_upstash_redis_rest__token_from_above"
ここまでの作業が完了すれば、以下のコマンドでローカル環境を起動できます。
yarn run edgio:dev
リポジトリの構成
これはプロジェクトのメインとなるフォルダ構成です。赤丸で囲んだファイルは、本記事で後ほど解説するCRUD操作やレート制限に関わるもの、そしてそれらが参照されるファイルです。

データフローの全体像
データがどのように流れるかを示すハイレベルな図がこちらです。
- ユーザーが
itsmy.fyi/me/slugにアクセスし、そのページのレスポンスがキャッシュされていない(または再検証中の)場合、getUserInfo関数が呼び出され、Upstash DBからユーザーのJSONデータを取得します。 - ユーザーがGitHub Issueを作成・更新・削除すると、GitHubがWebhookを発火させ、エンドポイントにPOSTリクエストを送信します。そのエンドポイント内では、まずUpstashのレート制限によってリクエストされた変更が可能かどうかを判定し、その後Upstashを使ってユーザーのJSONデータの作成・更新・削除を行います。

Upstash RedisによるユーザープロフィールのCRUD操作
このセクションでは、ユーザープロフィールのデータ取得・更新・削除がどのように実装されているのかを深掘りしていきます。データの取得と表示には、ioredis経由でUpstashを常時活用しています。
CRUD操作の基盤をGitHubからUpstashへ移行した理由
当初はGitHubをデータ管理の基盤としていました。GitHub Issuesをデータ入力フォームとして使い、GitHub Webhooks経由でリポジトリ内のユーザーJSONをCRUD操作する仕組みです。しかし、GitHub REST APIには「リポジトリあたり毎時1,000リクエスト」という制限があり、プラットフォームの本来の用途を制約し、むしろ退化させてしまう要因となっていました。
Upstashはこれと比べて格段に優れています。無料プランで1日10Kコマンドから始められ、利用量が拡大してもごくわずかなレートで済むからです。このアプローチにより、ほぼコストゼロでより多くのユーザー獲得が可能になり、スケーリングやデータベース運用コストを気にせず素早くイテレーションできるようになりました。
getUserInfo:ユーザープロフィール取得関数
getUserInfo関数は、ioredisのhgetをslugをキーとして使用し、一意のslugで識別される該当ユーザーのプロフィールページをUpstashにAPIリクエストして取得します。該当するプロフィールが存在しない場合(またはエラー発生時)、関数は{ code: 0 }を持つオブジェクトを返すようになっており、Astroの動的ルーティングで自動的に404ページへリダイレクトされます。
// File: lib/Upstash/users/get.js
// Read User Profile Code
import redis from "../setup";
export async function getUserInfo(slug) {
try {
const userData = await redis.hget("profiles", slug);
const parsedData = JSON.parse(userData);
if (parsedData.slug === slug) {
return { ...parsedData, code: 1 };
}
return {
code: 0,
error: `slug doesn't match for the user.`,
};
} catch (e) {
const error = e.message || e.toString();
console.log(error);
return {
code: 0,
error,
};
}
}
同様に、残りのCRUD操作は以下のようになります。
import redis from "../setup";
// File: @/lib/Upstash/users/delete.js
// Delete User Profile Code
export async function deleteUserInfo(slug) {
try {
await redis.hdel("profiles", slug);
return { code: 1 };
} catch (e) {
console.log(e.message || e.toString());
return {
code: 0,
};
}
}
// File: @/lib/Upstash/users/post.js
// Create/Update User Profile Code
export async function postUserInfo(data) {
try {
await redis.hset("profiles", data.slug, JSON.stringify(data));
return { code: 1 };
} catch (e) {
const error = e.message || e.toString();
console.log(error);
return {
code: 0,
error,
};
}
}
レート制限の実装
Edgio上のサーバーレス環境でレート制限を実装するために、Upstash Redisデータベースクライアントと、@upstash/ratelimitというレートリミッターライブラリを使用しています。
@/lib/Upstash/ratelimit.js
// Reference Function to ratelimiting
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
import { getENV } from "@/lib/env";
const url = getENV("UPSTASH_REDIS_REST_URL");
const token = getENV("UPSTASH_REDIS_REST_TOKEN");
export const ratelimit = (number, time) => {
if (url && token) {
return new Ratelimit({
redis: new Redis({
url,
token,
}),
limiter: Ratelimit.fixedWindow(number, time),
});
}
return;
};
レート制限を活用することで、以下を実現できました。
A. サービスを完全無料&無制限で利用可能に
レート制限のおかげで、プロフィール作成APIを公開APIとして提供できます!これにより、GUIから簡単にプロフィールを設定できるというシステムのメリットをアピールできます。誰でもWebサイト(itsmy.fyi)自体から週に3件のプロフィールを作成でき、プロフィール編集や無制限のプロフィール作成などの機能への無制限アクセスを求めるユーザーは、GitHub方式でのプロフィール作成に切り替えることができます。週3件というレート制限は、IPアドレスをキーとして強制しています。
// Rate limit 3 profiles in a week via the web for a user
const ratelimitUser = ratelimit(3, 7 * 24 * 60 * 60 + " s");
if (rateLimiter) {
// Look at the x-0-client-ip set by Edgio in serverless
const result = await rateLimiter.limit("x-0-client-ip");
limit = result.limit;
remaining = result.remaining;
if (!result.success) {
// Return a message
}
}
B. ユーザーごとの編集回数に対するきめ細かなモデレーション
さらに、レート制限によってGitHubユーザー名をキーに、1分あたりの編集回数を制御することもできます。現時点では、1分間に最大3回の変更を許可しています。これにより、予期しないスパムを抑制できます。
@/pages/github/hook/issue.js
const rateLimiter = ratelimit(3, "60 s");
if (rateLimiter) {
const result = await rateLimiter.limit(context.sender.login);
limit = result.limit;
remaining = result.remaining;
if (!result.success) {
return {
headers: {
"X-RateLimit-Limit": limit,
"X-RateLimit-Remaining": remaining,
},
body: JSON.stringify({
message:
"Too many updates in 1 minute. Please try again in a few minutes.",
}),
};
}
}
すべてのユーザープロフィールへのStale-While-Revalidateのエッジ実装
以下のコードは、Stale While Revalidate(SWR)の概念を活用してキャッシュヒット率を向上させる方法を示しています。routes.js内のコードでは、router.match関数を使って、/me/で始まるすべてのユーザープロフィールにマッチさせています。
cacheメソッドの中では、ブラウザ側でのページキャッシュを無効化し、エッジキャッシュのみを有効にすることで、常に高速かつ最新のコンテンツをユーザーに提供できるようにしています。edgeオプションはmaxAgeSeconds: 1に設定され、データはわずか1秒間のみキャッシュされます。一方、staleWhileRevalidateSecondsオプションは1年に設定されており、キャッシュの更新中でも古いデータを直接キャッシュから配信できます。
routes.js
// User path(s)
router.match("/me/:path", ({ cache, removeUpstreamResponseHeader }) => {
// Remove the cache-control header from Astro's standalone server
removeUpstreamResponseHeader("cache-control");
// Disable in browser caching, and use Edgio's edge to use SWR
cache({
edge: {
maxAgeSeconds: 1,
staleWhileRevalidateSeconds: 60 * 60 * 24 * 365,
},
browser: false,
});
});
Stale While Revalidateを活用することで、サーバーへの負荷を軽減しつつ、ユーザーにより高速なレスポンスを提供し、アプリ全体のパフォーマンス向上につながります。
動的なユーザープロフィールをオンザフライで生成
Astroを使うと、動的ルートの設定が非常に簡単です。アプリ内のsrc/pages/me/[slug].astroが、/me/で始まるページにマッピングされます。例えば/me/rishi-raj-jainや/me/some-other-userのようなURLです。
ユーザープロフィールの取得
現在のユーザーのデータは、Astroのparamsから抽出したslugクエリパラメータを使い、前述のgetUserInfo関数を呼び出すことで取得します。データが見つからない場合やエラーが発生した場合は、訪問者を404ページへリダイレクトします。
src/pages/me/[slug].astro
import { getUserInfo } from "@/lib/Upstash/users";
// Extract slug query
const { slug } = Astro.params;
// Get data from Upstash using the getUserInfo function
const {
name: userName,
image: userImage,
links = [],
socials = [],
about = "",
og = {},
background = {},
code = 1,
} = await getUserInfo(userSlug);
// In case the code: 0 is recevied, redirect to a 404
if (code === 0) {
return Astro.redirect("/404");
}
CLIからのデプロイ
以下のコマンドで、アプリの本番ビルドを行い、ローカルでテストできます。
yarn run edgio:build && yarn run edgio:production
デプロイにはEdgioのアカウントが必要です。こちらから無料でサインアップできます。アカウントを作成したら、プロジェクトのルートフォルダで以下のコマンドを実行するだけでEdgioへデプロイできます。
yarn run edgio:deploy
これでデプロイ完了です!そう、たったこれだけです。
まとめ
このプロジェクトを通じて、サーバーレス環境におけるきめ細かなレート制限の実装、CRUDデータ操作、GitHub IssuesをCMSとして活用する方法、そしてニーズに合わせてスケールするサービス(Upstash)を選択してMVPを迅速にリリースする判断の重要性など、多くの貴重な経験を得ることができました。
-
Redis Cluster と Redis Sentinel の違いを徹底解説:最適なアーキテクチャを選ぶための専門家ガイド
本番環境で Redis の利用が拡大していくと、開発チームは必ず重要なアーキテクチャ上の意思決定に直面します。Redis をスケールさせるのは「Redis Sentinel」にすべきか、それとも「Redis Cluster」にすべきか——この選択です。しかし、この判断は往々にして後回しにされ、パフォーマンス低下、メモリ枯渇、可用性インシデントといった問題が発生した後のプレッシャーの中で下されることが少なくありません。残念ながら、Sentinel と Cluster はまったく異なる課題を解決するための技術であり、誤った選択をしてしまうと、痛みを伴う再設計につながりかねません。シンプルに言えば、
-
Redisレプリケーションと高可用性をマスターする:本番環境のための実践ガイド
はじめにRedisは、システムが正常に動作している間は非常に安定した存在に感じられます。しかし、本当の試練が始まるのは、何かが壊れたときです。ノードのクラッシュ、仮想マシンの再起動、コンテナの消失、あるいはネットワーク障害によるシステムの分断——こうした事態はいつ発生してもおかしくありません。その後どうなるかは、レプリケーションと高可用性(HA)をどのように設計したかに大きく左右されます。多くのチームは「Redisは自動的に障害を処理してくれる」と思い込んでいますが、それは誤解です。Redisが提供するのは構成要素であり、完成されたソリューションではありません。レプリケーションと高可用性は、そ