firebase
Firebaseは認証・データベース・ストレージ・Functions・ホスティングを数分で構築できる強力なバックエンドを提供しますが、その手軽さの裏には本質的な複雑さが潜んでいます。セキュリティルールはシステムの最後の砦であり、誤った設定が致命的な脆弱性につながるため、正確な理解と実装が不可欠です。
description の原文を見る
Firebase gives you a complete backend in minutes - auth, database, storage, functions, hosting. But the ease of setup hides real complexity. Security rules are your last line of defense, and they're often wrong.
SKILL.md 本文
Firebase
Firebaseは、数分で完全なバックエンドを実現できます。認証、データベース、ストレージ、関数、ホスティングなど全てが揃っています。しかし、セットアップの簡単さは本来の複雑性を隠しています。セキュリティルールは最後の砦ですが、多くの場合、正しく設定されていません。Firestoreのクエリには制限があり、あなたのデータモデルを設計した後で気づくことになります。
このスキルはFirebase認証、Firestore、リアルタイムデータベース、Cloud Functions、Cloud Storage、およびFirebase Hostingをカバーしています。重要な洞察:Firebaseは読み取り頻度が高く、非正規化されたデータに最適化されています。リレーショナル的に考えている場合、それは間違った考え方です。
2025年の教訓:Firestoreの価格設定は想定外の結果をもたらすことがあります。読み取りは安いのですが、やがてそうではなくなります。設計が悪いリスナーは、専用データベースよりもコストがかかる可能性があります。データの関係性ではなく、クエリパターンに合わせてデータモデルを計画してください。
Principles
- クエリに合わせてデータを設計する。関係性に合わせてではなく
- セキュリティルールは必須です。オプションではありません
- 積極的に非正規化する。重複は安く、JOINは高コスト
- 一貫性のため、バッチ書き込みとトランザクションを使用
- オフライン永続化は慎重に使用する。無料ではありません
- クライアントがすべきではないことはCloud Functionsで実装
- 環境ごとの設定を使用。クライアントにキーをハードコードしない
Capabilities
- firebase-auth
- firestore
- firebase-realtime-database
- firebase-cloud-functions
- firebase-storage
- firebase-hosting
- firebase-security-rules
- firebase-admin-sdk
- firebase-emulators
Scope
- general-backend-architecture -> backend
- payment-processing -> stripe
- email-sending -> email
- advanced-auth-flows -> authentication-oauth
- kubernetes-deployment -> devops
Tooling
Core
- firebase - 使用時期:クライアント側SDK 注:モジュラーSDK - ツリーシェイク可能
- firebase-admin - 使用時期:サーバー側 / Cloud Functions 注:フルアクセス。セキュリティルールをバイパス
- firebase-functions - 使用時期:Cloud Functions v2 注:v2関数は推奨されます
Testing
- @firebase/rules-unit-testing - 使用時期:セキュリティルールのテスト 注:必須。ルールバグはセキュリティバグ
- firebase-tools - 使用時期:エミュレータスイート 注:本番環境に接続せずにローカル開発
Frameworks
- reactfire - 使用時期:React + Firebase 注:Hooksベース。購読を処理
- vuefire - 使用時期:Vue + Firebase 注:Vue固有のバインディング
- angularfire - 使用時期:Angular + Firebase 注:公式Angularバインディング
Patterns
モジュラーSDKのインポート
必要なものだけをインポートして、バンドルサイズを削減
使用時期:クライアント側のFirebase利用
/*
Firebase v9以降はモジュラーSDKを使用します。必要なものだけをインポートします。
これにより、ツリーシェイキングが可能になり、バンドルが小さくなります。
*/
// 間違い:v8互換スタイル(バンドルが大きい)
import firebase from 'firebase/compat/app';
import 'firebase/compat/firestore';
const db = firebase.firestore();
// 正解:v9以降のモジュラー(ツリーシェイク可能)
import { initializeApp } from 'firebase/app';
import { getFirestore, collection, doc, getDoc } from 'firebase/firestore';
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
// ドキュメントを取得
const docRef = doc(db, 'users', 'userId');
const docSnap = await getDoc(docRef);
if (docSnap.exists()) {
console.log(docSnap.data());
}
// 制約付きクエリ
import { query, where, orderBy, limit } from 'firebase/firestore';
const q = query(
collection(db, 'posts'),
where('published', '==', true),
orderBy('createdAt', 'desc'),
limit(10)
);
セキュリティルールの設計
最初からプロパーなルールでデータを保護
使用時期:任意のFirestoreデータベース
/*
ルールは最後の砦です。すべての読み取りと書き込みはルールを通ります。
ルールを誤ると、データが公開されてしまいます。
*/
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// ヘルパー関数
function isSignedIn() {
return request.auth != null;
}
function isOwner(userId) {
return request.auth.uid == userId;
}
function isAdmin() {
return request.auth.token.admin == true;
}
// ユーザーコレクション
match /users/{userId} {
// 誰でも公開プロフィールを読める
allow read: if true;
// 本人のみが自分のデータを書き込める
allow write: if isOwner(userId);
// プライベートサブコレクション
match /private/{document=**} {
allow read, write: if isOwner(userId);
}
}
// 投稿コレクション
match /posts/{postId} {
// 誰でも公開投稿を読める
allow read: if resource.data.published == true
|| isOwner(resource.data.authorId);
// 認証ユーザーのみ作成可能
allow create: if isSignedIn()
&& request.resource.data.authorId == request.auth.uid;
// 著者のみ更新/削除可能
allow update, delete: if isOwner(resource.data.authorId);
}
// 管理者のみのコレクション
match /admin/{document=**} {
allow read, write: if isAdmin();
}
}
}
クエリのためのデータモデリング
Firestoreのデータ構造をクエリパターンの周りに設計
使用時期:Firestoreスキーマ設計時
/*
Firestoreはリレーショナルではありません。JOINできません。
データをどのように「クエリする」かに合わせて設計します。どのように関係するかではなく。
*/
// 間違い:正規化(SQLの考え方)
// users/{userId}
// posts/{postId} with authorId field
// 「ユーザーの投稿を取得」には、postsコレクションをクエリする必要があります
// 正解:クエリのための非正規化
// users/{userId}/posts/{postId} - サブコレクション
// または
// posts/{postId} 埋め込み著者データ付き
// 投稿のドキュメント構造
const post = {
id: 'post123',
title: 'My Post',
content: '...',
// よく必要な著者データを埋め込む
author: {
id: 'user456',
name: 'Jane Doe',
avatarUrl: '...'
},
// INクエリ用の配列('in'の場合は最大30アイテム)
tags: ['javascript', 'firebase'],
// 複合クエリ用のマップ
stats: {
likes: 42,
comments: 7,
views: 1000
},
// タイムスタンプ
createdAt: serverTimestamp(),
updatedAt: serverTimestamp(),
// フィルタリング用のブール値
published: true,
featured: false
};
// このクエリパターンを有効にします:
// - 著者情報付き投稿を取得:1読み取り(JOINは不要)
// - タグ別投稿:where('tags', 'array-contains', 'javascript')
// - おすすめの投稿:where('featured', '==', true)
// - 最新の投稿:orderBy('createdAt', 'desc')
// 著者が名前を更新したら、彼らのすべての投稿を更新
// これがトレードオフです:書き込みはより複雑で、読み取りは高速です
リアルタイムリスナー
適切なクリーンアップでデータ変更をサブスクライブ
使用時期:リアルタイム機能
/*
onSnapshotは永続的な接続を作成します。メモリリークと余分な読み取りを防ぐため、
コンポーネントがアンマウントされるときは必ずサブスクリプション解除してください。
*/
// リアルタイムドキュメント用のReactフック
function useDocument(path) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const docRef = doc(db, path);
// ドキュメントをサブスクライブ
const unsubscribe = onSnapshot(
docRef,
(snapshot) => {
if (snapshot.exists()) {
setData({ id: snapshot.id, ...snapshot.data() });
} else {
setData(null);
}
setLoading(false);
},
(err) => {
setError(err);
setLoading(false);
}
);
// アンマウント時のクリーンアップ
return () => unsubscribe();
}, [path]);
return { data, loading, error };
}
// 使用方法
function UserProfile({ userId }) {
const { data: user, loading } = useDocument(`users/${userId}`);
if (loading) return <Spinner />;
return <div>{user?.name}</div>;
}
// クエリ付きコレクション
function usePosts(limit = 10) {
const [posts, setPosts] = useState([]);
useEffect(() => {
const q = query(
collection(db, 'posts'),
where('published', '==', true),
orderBy('createdAt', 'desc'),
limit(limit)
);
const unsubscribe = onSnapshot(q, (snapshot) => {
const results = snapshot.docs.map(doc => ({
id: doc.id,
...doc.data()
}));
setPosts(results);
});
return () => unsubscribe();
}, [limit]);
return posts;
}
Cloud Functionsパターン
Cloud Functions v2でのサーバー側ロジック
使用時期:バックエンドロジック、トリガー、スケジュール実行タスク
/*
Cloud Functionsはイベントによってトリガーされるサーバー側コードを実行します。
V2はより標準的なNode.jsパターンを使用し、スケーリングが向上しています。
*/
import { onRequest } from 'firebase-functions/v2/https';
import { onDocumentCreated } from 'firebase-functions/v2/firestore';
import { onSchedule } from 'firebase-functions/v2/scheduler';
import { getFirestore } from 'firebase-admin/firestore';
import { initializeApp } from 'firebase-admin/app';
initializeApp();
const db = getFirestore();
// HTTPファンクション
export const api = onRequest(
{ cors: true, region: 'us-central1' },
async (req, res) => {
// 認証トークンを検証
const token = req.headers.authorization?.split('Bearer ')[1];
if (!token) {
res.status(401).json({ error: 'Unauthorized' });
return;
}
try {
const decoded = await getAuth().verifyIdToken(token);
// decoded.uidでリクエストを処理
res.json({ userId: decoded.uid });
} catch (error) {
res.status(401).json({ error: 'Invalid token' });
}
}
);
// Firestoreトリガー - ドキュメント作成時
export const onUserCreated = onDocumentCreated(
'users/{userId}',
async (event) => {
const snapshot = event.data;
const userId = event.params.userId;
if (!snapshot) return;
const userData = snapshot.data();
// ウェルカムメール送信、関連ドキュメント作成など
await db.collection('notifications').add({
userId,
type: 'welcome',
message: `Welcome, ${userData.name}!`,
createdAt: FieldValue.serverTimestamp()
});
}
);
// スケジュール実行ファンクション(毎日真夜中)
export const dailyCleanup = onSchedule(
{ schedule: '0 0 * * *', timeZone: 'UTC' },
async (event) => {
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - 30);
// 古いドキュメントを削除
const oldDocs = await db.collection('logs')
.where('createdAt', '<', cutoff)
.limit(500)
.get();
const batch = db.batch();
oldDocs.docs.forEach(doc => batch.delete(doc.ref));
await batch.commit();
console.log(`Deleted ${oldDocs.size} old logs`);
}
);
バッチ操作
一貫性のためのアトミック書き込みとトランザクション
使用時期:一緒に成功する必要のある複数ドキュメント更新
/*
バッチ:複数の書き込みがすべて成功するか、すべて失敗します。
トランザクション:一貫性を持つ読み取り後の書き込み操作。
バッチ/トランザクションごとに最大500操作です。
*/
import {
writeBatch, runTransaction, doc, getDoc,
increment, serverTimestamp
} from 'firebase/firestore';
// バッチ書き込み - 読み取りなし、書き込みのみ
async function createPostWithTags(post, tags) {
const batch = writeBatch(db);
// 投稿を作成
const postRef = doc(collection(db, 'posts'));
batch.set(postRef, {
...post,
createdAt: serverTimestamp()
});
// タグカウントを更新
for (const tag of tags) {
const tagRef = doc(db, 'tags', tag);
batch.set(tagRef, {
count: increment(1),
lastUsed: serverTimestamp()
}, { merge: true });
}
await batch.commit();
return postRef.id;
}
// トランザクション - アトミックに読み取りと書き込み
async function likePost(postId, userId) {
return runTransaction(db, async (transaction) => {
const postRef = doc(db, 'posts', postId);
const likeRef = doc(db, 'posts', postId, 'likes', userId);
const postSnap = await transaction.get(postRef);
if (!postSnap.exists()) {
throw new Error('Post not found');
}
const likeSnap = await transaction.get(likeRef);
if (likeSnap.exists()) {
throw new Error('Already liked');
}
// いいね数を増やし、いいねドキュメントを追加
transaction.update(postRef, {
likeCount: increment(1)
});
transaction.set(likeRef, {
userId,
createdAt: serverTimestamp()
});
return postSnap.data().likeCount + 1;
});
}
ソーシャルログイン(Google、GitHubなど)
OAuthプロバイダーのセットアップと認証フロー
使用時期:ソーシャルログインの実装
import {
getAuth, signInWithPopup, signInWithRedirect,
GoogleAuthProvider, GithubAuthProvider, OAuthProvider
} from "firebase/auth";
const auth = getAuth();
// GOOGLE
const googleProvider = new GoogleAuthProvider();
googleProvider.addScope("email");
googleProvider.setCustomParameters({ prompt: "select_account" });
async function signInWithGoogle() {
try {
const result = await signInWithPopup(auth, googleProvider);
return result.user;
} catch (error) {
if (error.code === "auth/account-exists-with-different-credential") {
return handleAccountConflict(error);
}
throw error;
}
}
// GITHUB
const githubProvider = new GithubAuthProvider();
githubProvider.addScope("read:user");
// APPLE(iOSアプリでは必須!)
const appleProvider = new OAuthProvider("apple.com");
appleProvider.addScope("email");
appleProvider.addScope("name");
ポップアップ vs リダイレクト認証
OAuthでポップアップ vs リダイレクトを使い分ける時期
使用時期:認証フローの選択
// ポップアップ:デスクトップ、SPA(より簡単。ブロック可能)
// リダイレクト:モバイル、iOSサファリ(常に機能)
async function signIn(provider) {
if (/iPhone|iPad|Android/i.test(navigator.userAgent)) {
return signInWithRedirect(auth, provider);
}
try {
return await signInWithPopup(auth, provider);
} catch (e) {
if (e.code === "auth/popup-blocked") {
return signInWithRedirect(auth, provider);
}
throw e;
}
}
// ページ読み込み時にリダイレクト結果を確認
useEffect(() => {
getRedirectResult(auth).then(r => r && setUser(r.user));
}, []);
アカウントリンク
複数のプロバイダーを1つのアカウントにリンク
使用時期:ユーザーが異なるプロバイダーのアカウントを持っている場合
import { fetchSignInMethodsForEmail, linkWithCredential } from "firebase/auth";
async function handleAccountConflict(error) {
const email = error.customData?.email;
const pendingCred = OAuthProvider.credentialFromError(error);
const methods = await fetchSignInMethodsForEmail(auth, email);
if (methods.includes("google.com")) {
alert("Sign in with Google to link accounts");
const result = await signInWithPopup(auth, new GoogleAuthProvider());
await linkWithCredential(result.user, pendingCred);
return result.user;
}
}
// 新しいプロバイダーをリンク
await linkWithPopup(auth.currentUser, new GithubAuthProvider());
// プロバイダーをアンリンク(少なくとも1つは保つ!)
await unlink(auth.currentUser, "github.com");
認証状態永続化
セッション有効期間を制御
使用時期:ユーザーセッションの管理
import { setPersistence, browserLocalPersistence, browserSessionPersistence } from "firebase/auth";
// LOCAL:ブラウザ閉鎖後も保持(デフォルト)
// SESSION:タブ閉鎖時にクリア
async function signInWithRememberMe(email, pass, remember) {
await setPersistence(auth, remember ? browserLocalPersistence : browserSessionPersistence);
return signInWithEmailAndPassword(auth, email, pass);
}
// React認証フック
function useAuth() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => onAuthStateChanged(auth, u => { setUser(u); setLoading(false); }), []);
return { user, loading };
}
メール確認とパスワードリセット
完全なメール認証フロー
使用時期:メール/パスワード認証
import { sendEmailVerification, sendPasswordResetEmail, reauthenticateWithCredential } from "firebase/auth";
// 確認付きサインアップ
async function signUp(email, password) {
const result = await createUserWithEmailAndPassword(auth, email, password);
await sendEmailVerification(result.user);
return result.user;
}
// パスワードリセット
await sendPasswordResetEmail(auth, email);
// パスワード変更(最近の認証が必要)
const cred = EmailAuthProvider.credential(user.email, currentPass);
await reauthenticateWithCredential(user, cred);
await updatePassword(user, newPass);
APIのトークン管理
バックエンドAPI呼び出しのためのIDトークン処理
使用時期:バックエンドAPIの認証
import { getIdToken, onIdTokenChanged } from "firebase/auth";
// トークンを取得(期限切れの場合は自動更新)
const token = await getIdToken(auth.currentUser);
// 自動リトライ付きAPIヘルパー
async function apiCall(url, opts = {}) {
const token = await getIdToken(auth.currentUser);
const res = await fetch(url, {
...opts,
headers: { ...opts.headers, Authorization: "Bearer " + token }
});
if (res.status === 401) {
const newToken = await getIdToken(auth.currentUser, true);
return fetch(url, { ...opts, headers: { ...opts.headers, Authorization: "Bearer " + newToken }});
}
return res;
}
// SSRのためのクッキーへの同期
onIdTokenChanged(auth, async u => {
document.cookie = u ? "__session=" + await u.getIdToken() : "__session=; max-age=0";
});
// 管理者クレームをチェック
const { claims } = await auth.currentUser.getIdTokenResult();
const isAdmin = claims.admin === true;
Collaboration
Delegation Triggers
- ユーザーが複雑なOAuthフローを必要とする -> authentication-oauth(Firebase認証は基本をカバーしますが、複雑なフローはOAuthスキルが必要)
- ユーザーが支払い統合を必要とする -> stripe(Firebase + Stripeは一般的なパターン)
- ユーザーがメール機能を必要とする -> email(Firebaseはメールを含みません。SendGrid、Resendなどを使用)
- ユーザーがコンテナデプロイを必要とする -> devops(Firebase Hostingを超えた範囲。Kubernetes、Docker)
- ユーザーがリレーショナルデータモデルを必要とする -> postgres-wizard(Firestoreは強いリレーショナルデータには不適切)
- ユーザーが全文検索を必要とする -> elasticsearch-search(Firestoreは全文検索をサポートしていません。Algolia/Elasticを使用)
Related Skills
相性の良いスキル:nextjs-app-router、react-patterns、authentication-oauth、stripe
When to Use
- ユーザーが言及または暗に示す:firebase
- ユーザーが言及または暗に示す:firestore
- ユーザーが言及または暗に示す:firebase auth
- ユーザーが言及または暗に示す:cloud functions
- ユーザーが言及または暗に示す:firebase storage
- ユーザーが言及または暗に示す:realtime database
- ユーザーが言及または暗に示す:firebase hosting
- ユーザーが言及または暗に示す:firebase emulator
- ユーザーが言及または暗に示す:security rules
- ユーザーが言及または暗に示す:firebase admin
Limitations
- 上述のスコープと明確に一致する場合のみこのスキルを使用してください。
- 出力を環境固有の検証、テスト、または専門家レビューの代替として扱わないでください。
- 必要な入力、権限、セーフティ境界、または成功基準が不足している場合は、停止して明確化を求めてください。
ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- sickn33
- ライセンス
- MIT
- 最終更新
- 不明
Source: https://github.com/sickn33/antigravity-awesome-skills / ライセンス: MIT
関連スキル
doubt-driven-development
重要な判断はすべて、本番環境への展開前に新しい視点から対抗的レビューを実施します。速度より正確性が重要な場合、不慣れなコードを扱う場合、本番環境・セキュリティに関わるロジック・取り消し不可の操作など影響度が高い場合、または後でバグを修正するよりも今検証する方が効率的な場合に活用してください。
apprun-skills
TypeScriptを使用したAppRunアプリケーションのMVU設計に関する総合的なガイダンスが得られます。コンポーネントパターン、イベントハンドリング、状態管理(非同期ジェネレータを含む)、パラメータと保護機能を備えたルーティング・ナビゲーション、vistestを使用したテストに対応しています。AppRunコンポーネントの設計・レビュー、ルートの配線、状態フローの管理、AppRunテストの作成時に活用してください。
desloppify
コードベースのヘルスチェックと技術負債の追跡ツールです。コード品質、技術負債、デッドコード、大規模ファイル、ゴッドクラス、重複関数、コードスメル、命名規則の問題、インポートサイクル、結合度の問題についてユーザーが質問した場合に使用してください。また、ヘルススコアの確認、次の改善項目の提案、クリーンアップ計画の作成をリクエストされた際にも対応します。29言語に対応しています。
debugging-and-error-recovery
テストが失敗したり、ビルドが壊れたり、動作が期待と異なったり、予期しないエラーが発生したりした場合に、体系的な根本原因デバッグをガイドします。推測ではなく、根本原因を見つけて修正するための体系的なアプローチが必要な場合に使用してください。
test-driven-development
テスト駆動開発により実装を進めます。ロジックの実装、バグの修正、動作の変更など、あらゆる場面で活用できます。コードが正常に動作することを証明する必要がある場合、バグ報告を受けた場合、既存機能を修正する予定がある場合に使用してください。
incremental-implementation
変更を段階的に実施します。複数のファイルに影響する機能や変更を実装する場合に使用してください。大量のコードを一度に書こうとしている場合や、タスクが一度では完結できないほど大きい場合に活用します。