アプリを開発しています。ユーザーはサインインし、データはリアルタイムで同期され、ファイルは保存される必要があります。サーバーを立ち上げ、データベースを設定し、インフラを何週間も管理することもできます。あるいは、Firebaseを使うこともできます。
Firebaseは、The New York Times、Duolingo、Alibabaを含む150万以上のアプリを支えています。開発者がこれを選ぶのは、バックエンドの複雑さを解消してくれるからです。サーバーの保守ではなく、機能に集中できます。しかし、Firebase APIには癖があります。認証フローは初心者には混乱を招き、データベースのルールは経験豊富な開発者でもつまずかせます。Cloud Functionsはトリガーを理解するまでは魔法のように思えるでしょう。
私はFirebaseを何百万ものユーザーにサービスを提供する本番アプリに統合してきました。サービスアカウントキーを公開したり、非効率なクエリを書いたり、壊れた関数をデプロイしたりと、あらゆる可能な間違いを経験しました。このガイドでは、それらの教訓を凝縮しています。
認証、データベース操作、Cloud Functions、ストレージについて学ぶことができます。理論だけでなく、動作するコードも確認できます。本番環境での問題を引き起こす落とし穴を避けることができるでしょう。
Firebase APIとは何か、そしてそれが重要な理由
Firebaseは単一のAPIではありません。統合されたSDKとRESTエンドポイントを通じてアクセスされるバックエンドサービスのスイートです。
Firebaseの主要サービス
| サービス | 目的 | APIタイプ |
|---|---|---|
| Authentication | ユーザーサインインとID | SDK + REST |
| Firestore Database | NoSQLドキュメントデータベース | SDK + REST |
| Realtime Database | JSONリアルタイム同期 | SDK + REST |
| Cloud Storage | ファイルストレージとCDN | SDK + REST |
| Cloud Functions | サーバーレスコンピューティング | デプロイCLI |
| Hosting | 静的ウェブホスティング | デプロイCLI |
| Cloud Messaging | プッシュ通知 | HTTP v1 API |
Firebaseが適している場合
Firebaseは特定の課題をうまく解決します。
Firebaseを使用する場合:
- リアルタイム同期が必要な場合(チャット、コラボレーション、ライブ更新)
- サーバーレスアーキテクチャを望む場合(インフラ管理不要)
- モバイルまたはウェブアプリを構築している場合(SDKがプラットフォームの違いを処理)
- オフラインサポートが必要な場合(SDKがデータを自動キャッシュ)
- 組み込みの認証(Google、Apple、メール、電話サインイン)を望む場合
Firebaseをスキップする場合:
- 複雑なリレーショナルクエリが必要な場合(代わりにPostgreSQLを使用)
- 厳格なデータ所在地の要件がある場合(Firebaseのリージョンは限られている)
- 完全なSQL機能が必要な場合(Firestoreにはクエリの制限がある)
- 開発速度よりも大規模なコストが重要な場合(自己ホスティングの方が安価)
Firebase APIのアーキテクチャ
Firebaseはハイブリッドなアプローチを使用します。
┌─────────────────────────────────────────────────────────┐
│ あなたのアプリケーション │
├─────────────────────────────────────────────────────────┤
│ Firebase SDK (クライアント) │
│ - 認証トークンを自動処理 │
│ - オフラインキャッシュを管理 │
│ - リアルタイムリスナー │
└─────────────────────────────────────────────────────────┘
│
│ HTTPS + WebSocket
▼
┌─────────────────────────────────────────────────────────┐
│ Firebase バックエンド │
├──────────────┬──────────────┬──────────────┬────────────┤
│ Auth │ Firestore │ Storage │ Functions │
│ サービス │ データベース │ サービス │ ランタイム │
└──────────────┴──────────────┴──────────────┴────────────┘
クライアントSDKはHTTPレイヤーを抽象化します。内部的には、すべての操作はJWT認証を伴うREST API呼び出しに変換されます。
Firebase認証:完全なセットアップ
認証は最初のFirebase統合です。これを間違えると、他のすべてが失敗します。
ステップ1:Firebaseプロジェクトを作成する
- Firebaseコンソールに移動します

「プロジェクトを追加」をクリックし、プロジェクト名を入力します(スペースなし)。

Googleアナリティクスを有効にする(任意ですが推奨)

「プロジェクトを作成」をクリックします

プロビジョニングに30秒待ちます。プロジェクトダッシュボードが表示されます。
ステップ2:アプリを登録する
Webアプリの場合:
// Firebaseコンソール > プロジェクト設定 > 全般
// 「アプリを追加」> Webアイコンをクリック
// Webアプリを登録
const firebaseConfig = {
apiKey: "AIzaSyDxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
authDomain: "your-app.firebaseapp.com",
projectId: "your-app",
storageBucket: "your-app.appspot.com",
messagingSenderId: "123456789012",
appId: "1:123456789012:web:abc123def456"
};
// Firebaseを初期化
import { initializeApp } from 'firebase/app';
const app = initializeApp(firebaseConfig);
iOSアプリの場合:
GoogleService-Info.plistをダウンロードし、Xcodeプロジェクトに追加します。「Target Membership」にアプリが含まれていることを確認してください。
Androidアプリの場合:
google-services.jsonをダウンロードし、app/ディレクトリに配置します。build.gradleに追加します。
// プロジェクトレベルの build.gradle
buildscript {
dependencies {
classpath 'com.google.gms:google-services:4.4.0'
}
}
// アプリレベルの build.gradle
plugins {
id 'com.google.gms.google-services'
}
ステップ3:認証方法を有効にする
Firebaseコンソール > 認証 > サインイン方法で:
- メール/パスワード: 従来のサインアップ用に有効にします
- Google: SHA-1証明書のフィンガープリント(Android)またはバンドルID(iOS)を追加します
- Apple: ソーシャルログインを有効にする場合、iOSアプリに必須です
- 電話番号: SMS認証用に有効にします(課金が必要)
ステップ4:認証フローを実装する
メール/パスワードでのサインアップ:
import {
createUserWithEmailAndPassword,
getAuth,
updateProfile
} from 'firebase/auth';
const auth = getAuth(app);
async function signUp(email, password, displayName) {
try {
const userCredential = await createUserWithEmailAndPassword(
auth,
email,
password
);
// 表示名を設定
await updateProfile(userCredential.user, {
displayName: displayName
});
console.log('ユーザーが作成されました:', userCredential.user.uid);
return userCredential.user;
} catch (error) {
// 特定のエラーコードを処理
switch (error.code) {
case 'auth/email-already-in-use':
throw new Error('このメールアドレスはすでに登録されています');
case 'auth/weak-password':
throw new Error('パスワードは6文字以上にする必要があります');
case 'auth/invalid-email':
throw new Error('無効なメールアドレスです');
default:
throw new Error('サインアップに失敗しました: ' + error.message);
}
}
}
メール/パスワードでのサインイン:
import {
signInWithEmailAndPassword,
signOut
} from 'firebase/auth';
async function signIn(email, password) {
try {
const userCredential = await signInWithEmailAndPassword(
auth,
email,
password
);
const user = userCredential.user;
// API呼び出し用のIDトークンを取得
const idToken = await user.getIdToken();
console.log('認証トークン:', idToken);
return user;
} catch (error) {
switch (error.code) {
case 'auth/user-not-found':
throw new Error('このメールアドレスのアカウントは見つかりません');
case 'auth/wrong-password':
throw new Error('パスワードが正しくありません');
case 'auth/too-many-requests':
throw new Error('試行回数が多すぎます。後でもう一度お試しください');
default:
throw new Error('サインインに失敗しました');
}
}
}
async function logOut() {
await signOut(auth);
console.log('ユーザーがサインアウトしました');
}
Googleサインイン(ウェブ):
import {
GoogleAuthProvider,
signInWithPopup
} from 'firebase/auth';
async function signInWithGoogle() {
const provider = new GoogleAuthProvider();
// 追加スコープをリクエスト
provider.addScope('email');
provider.addScope('profile');
try {
const result = await signInWithPopup(auth, provider);
const user = result.user;
// Google OAuthトークンにアクセス
const credential = GoogleAuthProvider.credentialFromResult(result);
const googleAccessToken = credential.accessToken;
return user;
} catch (error) {
if (error.code === 'auth/popup-closed-by-user') {
throw new Error('サインインがキャンセルされました');
}
throw new Error('Googleサインインに失敗しました');
}
}
ステップ5:認証状態によってルートを保護する
import { onAuthStateChanged } from 'firebase/auth';
// 認証状態の変更を購読
onAuthStateChanged(auth, (user) => {
if (user) {
// ユーザーはサインインしている
console.log('ユーザー:', user.email);
// ダッシュボードにリダイレクト
window.location.href = '/dashboard';
} else {
// ユーザーはサインアウトしている
console.log('ユーザーなし');
// ログインにリダイレクト
window.location.href = '/login';
}
});
よくある認証の間違い
間違い1:トークンの更新を処理しない
Firebase SDKはトークンを自動的に更新しますが、サーバー側でトークンをキャッシュしている場合、1時間で期限切れになります。常に各リクエストでトークンを検証するか、更新ロジックを実装してください。
間違い2:クライアントコードで管理者認証情報を公開する
クライアントアプリでサービスアカウントキーを決して使用しないでください。サービスアカウントはセキュリティルールをバイパスします。信頼できるサーバー環境でのみ使用してください。
間違い3:メール認証をスキップする
import { sendEmailVerification } from 'firebase/auth';
async function sendVerificationEmail(user) {
await sendEmailVerification(user);
console.log('認証メールを送信しました');
}
// 認証状態を確認
if (!auth.currentUser.emailVerified) {
console.log('メールが認証されていません');
// アクセスを制限
}
Firestoreデータベース:操作とクエリ
FirestoreはFirebaseのNoSQLデータベースです。ドキュメントはコレクションに整理されます。クエリは自動的にスケーリングされます。
データ構造
your-project (ルート)
└── users (コレクション)
├── userId123 (ドキュメント)
│ ├── name: "John"
│ ├── email: "john@example.com"
│ └── posts (サブコレクション)
│ ├── postId1 (ドキュメント)
│ └── postId2 (ドキュメント)
└── userId456 (ドキュメント)
Firestoreを初期化する
import { getFirestore } from 'firebase/firestore';
const db = getFirestore(app);
ドキュメントを作成する
import {
collection,
addDoc,
setDoc,
doc
} from 'firebase/firestore';
// オプション1:自動生成ID
async function createUser(userData) {
const docRef = await addDoc(collection(db, 'users'), userData);
console.log('IDが書き込まれたドキュメント:', docRef.id);
return docRef.id;
}
// オプション2:カスタムID
async function createUserWithId(userId, userData) {
await setDoc(doc(db, 'users', userId), userData);
console.log('カスタムIDが書き込まれたドキュメント:', userId);
}
// 使用例
const userId = await createUser({
name: 'Alice',
email: 'alice@example.com',
createdAt: new Date(),
role: 'user'
});
ドキュメントを読み込む
import {
getDoc,
getDocs,
query,
where,
orderBy,
limit
} from 'firebase/firestore';
// 単一ドキュメントを取得
async function getUser(userId) {
const docRef = doc(db, 'users', userId);
const docSnap = await getDoc(docRef);
if (docSnap.exists()) {
return docSnap.data();
} else {
throw new Error('ユーザーが見つかりません');
}
}
// フィルター付きクエリ
async function getUsersByRole(role) {
const q = query(
collection(db, 'users'),
where('role', '==', role),
orderBy('createdAt', 'desc'),
limit(10)
);
const querySnapshot = await getDocs(q);
const users = [];
querySnapshot.forEach((doc) => {
users.push({ id: doc.id, ...doc.data() });
});
return users;
}
// 使用例
const adminUsers = await getUsersByRole('admin');
console.log('管理者ユーザー:', adminUsers);
ドキュメントを更新する
import {
updateDoc,
increment,
arrayUnion,
arrayRemove
} from 'firebase/firestore';
async function updateUser(userId, updates) {
const userRef = doc(db, 'users', userId);
await updateDoc(userRef, updates);
}
// アトミック操作
await updateUser('userId123', {
loginCount: increment(1),
tags: arrayUnion('premium', 'beta-tester'),
lastLogin: new Date()
});
// 配列から削除
await updateUser('userId123', {
tags: arrayRemove('beta-tester')
});
ドキュメントを削除する
import { deleteDoc } from 'firebase/firestore';
async function deleteUser(userId) {
await deleteDoc(doc(db, 'users', userId));
console.log('ユーザーを削除しました');
}
リアルタイムリスナー
import { onSnapshot } from 'firebase/firestore';
// 単一ドキュメントをリッスン
const unsubscribe = onSnapshot(
doc(db, 'users', userId),
(doc) => {
console.log('ユーザーが更新されました:', doc.data());
},
(error) => {
console.error('リッスンエラー:', error);
}
);
// クエリ結果をリッスン
const q = query(collection(db, 'posts'), where('published', '==', true));
const unsubscribeQuery = onSnapshot(q, (snapshot) => {
const posts = snapshot.docs.map(doc => ({
id: doc.id,
...doc.data()
}));
console.log('公開された投稿:', posts);
});
// リッスンを停止
unsubscribe();
unsubscribeQuery();
Firestoreセキュリティルール
適切なルールがなければ、誰でもあなたのデータを読み取ることができます。Firebaseコンソール > Firestore > ルールでルールを設定してください。
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// ヘルパー関数
function isAuthenticated() {
return request.auth != null;
}
function isOwner(userId) {
return request.auth.uid == userId;
}
// usersコレクション
match /users/{userId} {
allow read: if isAuthenticated();
allow create: if isAuthenticated() && isOwner(userId);
allow update, delete: if isOwner(userId);
}
// postsコレクション
match /posts/{postId} {
allow read: if true; // 公開読み取り
allow create: if isAuthenticated();
allow update, delete: if resource.data.authorId == request.auth.uid;
}
// プライベートサブコレクション
match /users/{userId}/private/{document} {
allow read, write: if isOwner(userId);
}
}
}
クエリの制限
Firestoreには制約があります。
- ORクエリがない(配列または複数クエリで
inを使用) - ワイルドカード検索がない(全文検索にはAlgoliaまたはMeilisearchを使用)
- 複合クエリにはインデックスが必要(Firestoreは単一フィールドを自動作成)
inクエリでの30個までの非連結(disjunction)に制限
ORクエリの回避策:
// 代わりに: where('status', '==', 'active') OR where('status', '==', 'pending')
const activeQuery = query(
collection(db, 'tasks'),
where('status', '==', 'active')
);
const pendingQuery = query(
collection(db, 'tasks'),
where('status', '==', 'pending')
);
const [activeSnap, pendingSnap] = await Promise.all([
getDocs(activeQuery),
getDocs(pendingQuery)
]);
// クライアント側で結果をマージ
Cloud Functions:サーバーレスバックエンドロジック
Cloud Functionsは、サーバーを管理することなくバックエンドコードを実行します。データベースの変更、HTTPリクエスト、またはスケジュールされたイベントでトリガーされます。
セットアップ
# Firebase CLIをインストール
npm install -g firebase-tools
# ログイン
firebase login
# プロジェクトでFunctionsを初期化
firebase init functions
# 選択: JavaScript, ESLint yes, Express.js no
HTTP Functions(APIエンドポイント)
// functions/index.js
const { onRequest } = require('firebase-functions/v2/https');
const admin = require('firebase-admin');
admin.initializeApp();
const db = admin.firestore();
// 公開エンドポイント
exports.getPublicData = onRequest(async (req, res) => {
res.set('Access-Control-Allow-Origin', '*');
try {
const snapshot = await db.collection('public').get();
const data = snapshot.docs.map(doc => doc.data());
res.json({ success: true, data });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 保護されたエンドポイント(認証トークンを検証)
exports.getUserProfile = onRequest(async (req, res) => {
res.set('Access-Control-Allow-Origin', '*');
// Authorizationヘッダーからトークンを取得
const authHeader = req.headers.authorization || '';
const token = authHeader.split('Bearer ')[1];
if (!token) {
return res.status(401).json({ error: '認証されていません' });
}
try {
// トークンを検証
const decodedToken = await admin.auth().verifyIdToken(token);
const userId = decodedToken.uid;
// ユーザーデータを取得
const userDoc = await db.collection('users').doc(userId).get();
if (!userDoc.exists) {
return res.status(404).json({ error: 'ユーザーが見つかりません' });
}
res.json({
success: true,
data: { id: userId, ...userDoc.data() }
});
} catch (error) {
res.status(401).json({ error: '無効なトークンです' });
}
});
デプロイ:
firebase deploy --only functions:getUserProfile
クライアントからの呼び出し:
async function getUserProfile(token) {
const response = await fetch(
'https://us-central1-your-app.cloudfunctions.net/getUserProfile',
{
headers: {
'Authorization': `Bearer ${token}`
}
}
);
const data = await response.json();
return data;
}
データベーストリガー
const { onDocumentWritten } = require('firebase-functions/v2/firestore');
// ユーザー文書が変更されたときにトリガー
exports.onUserUpdate = onDocumentWritten(
'users/{userId}',
async (event) => {
const userId = event.params.userId;
const before = event.data?.before?.data();
const after = event.data?.after?.data();
// メールが変更されたかを確認
if (before?.email !== after?.email) {
console.log(`ユーザー ${userId} のメールが変更されました: ${before?.email} → ${after?.email}`);
// 通知メールを送信
await admin.auth().getUser(userId);
// ここにメールロジックを追加
}
}
);
// 新しい投稿が作成されたときにトリガー
exports.onNewPost = onDocumentWritten(
'posts/{postId}',
async (event) => {
const post = event.data?.after?.data();
if (!post) return; // ドキュメントが削除された
// これが新しいドキュメントであるかを確認
if (!event.data?.before?.exists) {
console.log('新しい投稿が作成されました:', post.title);
// フォロワーに通知
const followersSnap = await admin.firestore()
.collection('users')
.where('following', 'array-contains', post.authorId)
.get();
const notifications = followersSnap.docs.map(doc => ({
userId: doc.id,
postId: event.params.postId,
type: 'new_post',
createdAt: admin.firestore.FieldValue.serverTimestamp()
}));
const batch = admin.firestore().batch();
notifications.forEach(notif => {
const ref = admin.firestore().collection('notifications').doc();
batch.set(ref, notif);
});
await batch.commit();
}
}
);
スケジュールされた関数(Cronジョブ)
const { onSchedule } = require('firebase-functions/v2/scheduler');
// UTCの毎日深夜に実行
exports.dailyCleanup = onSchedule('every 24 hours', async (event) => {
console.log('デイリークリーンアップを実行しています');
// 古い通知を削除(30日以上前)
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const oldNotifs = await admin.firestore()
.collection('notifications')
.where('createdAt', '<', thirtyDaysAgo)
.get();
const batch = admin.firestore().batch();
oldNotifs.forEach(doc => batch.delete(doc.ref));
await batch.commit();
console.log(`${oldNotifs.size} 件の古い通知を削除しました`);
});
環境設定
# 環境変数を設定
firebase functions:config:set \
stripe.secret="sk_test_xxx" \
email.api_key="key_xxx"
# 関数内でアクセス
const config = require('firebase-functions/config');
const stripe = require('stripe')(config.stripe.secret);
Cloud Storage:ファイルのアップロードと管理
ユーザーのアップロード、画像、ファイルを自動CDN配信で保存します。
ストレージルールを設定する
// Firebaseコンソール > ストレージ > ルール
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
// ユーザーアップロードフォルダ
match /users/{userId}/{allPaths=**} {
allow read: if true; // 公開読み取り
allow write: if request.auth.uid == userId;
allow delete: if request.auth.uid == userId;
}
// 公開アセット
match /public/{allPaths=**} {
allow read: if true;
allow write: if false; // Firebaseコンソール経由での管理者のみ
}
}
}
ファイルをアップロードする(クライアント)
import {
getStorage,
ref,
uploadBytesResumable,
getDownloadURL
} from 'firebase/storage';
const storage = getStorage(app);
async function uploadProfileImage(userId, file) {
// ストレージ参照を作成
const storageRef = ref(storage, `users/${userId}/profile/${file.name}`);
// ファイルをアップロード
const uploadTask = uploadBytesResumable(storageRef, file);
return new Promise((resolve, reject) => {
uploadTask.on(
'state_changed',
(snapshot) => {
// 進捗を追跡
const progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
console.log(`アップロード: ${progress.toFixed(0)}%`);
},
(error) => {
// エラーを処理
switch (error.code) {
case 'storage/unauthorized':
reject(new Error('権限がありません'));
break;
case 'storage/canceled':
reject(new Error('アップロードがキャンセルされました'));
break;
default:
reject(new Error('アップロードに失敗しました'));
}
},
async () => {
// アップロードが完了
const downloadURL = await getDownloadURL(uploadTask.snapshot.ref);
console.log('ファイルは以下から利用可能です:', downloadURL);
resolve(downloadURL);
}
);
});
}
// 使用例
const fileInput = document.querySelector('input[type="file"]');
const file = fileInput.files[0];
if (file) {
const imageUrl = await uploadProfileImage(auth.currentUser.uid, file);
// URLをFirestoreに保存
await updateDoc(doc(db, 'users', auth.currentUser.uid), {
profileImage: imageUrl
});
}
ファイルをダウンロードする
import { getDownloadURL } from 'firebase/storage';
async function getProfileImage(userId) {
const imageRef = ref(storage, `users/${userId}/profile/avatar.png`);
try {
const url = await getDownloadURL(imageRef);
return url;
} catch (error) {
if (error.code === 'storage/object-not-found') {
return null; // プロフィール画像なし
}
throw error;
}
}
ファイルを削除する
import { deleteObject } from 'firebase/storage';
async function deleteProfileImage(userId) {
const imageRef = ref(storage, `users/${userId}/profile/avatar.png`);
await deleteObject(imageRef);
console.log('プロフィール画像を削除しました');
}
ApidogでFirebase APIをテストする
Firebaseはすべてのサービスに対してREST APIを提供しています。これらを直接テストすることで、問題のデバッグや基盤となるリクエストの理解に役立ちます。
Firebase REST APIをインポートする
- Apidogを開く
- 新しいプロジェクトを作成:「Firebase API」
- FirebaseドキュメントからOpenAPI仕様をインポートする
- または、エンドポイントを手動で追加する:
Firestore RESTエンドポイント:
POST https://firestore.googleapis.com/v1/projects/{projectId}/databases/(default)/documents
Authorization: Bearer {oauth2_token}
Content-Type: application/json
{
"fields": {
"name": { "stringValue": "John" },
"email": { "stringValue": "john@example.com" },
"age": { "integerValue": 30 }
}
}
認証エンドポイント:
POST https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key={api_key}
Content-Type: application/json
{
"email": "user@example.com",
"password": "secret123",
"returnSecureToken": true
}
認証フローをテストする
- リクエストを作成:「サインイン」
- メソッドを設定:POST
- 本文にメール/パスワードを追加
- レスポンストークンを環境変数として保存
- 後続のリクエストで
{{token}}を使用
セキュリティルールをデバッグする
ローカルテストにはFirebase Emulator Suiteを使用します。
# エミュレーターを起動
firebase emulators:start
# ローカルのFirestoreに対してテスト
# http://localhost:8080
本番環境でのベストプラクティス
1. 適切なエラー処理を実装する
// 一時的な障害に対する再試行ロジック
async function firestoreWithRetry(operation, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await operation();
} catch (error) {
if (
error.code === 'unavailable' ||
error.code === 'deadline-exceeded'
) {
const delay = Math.pow(2, i) * 1000; // 指数関数的バックオフ
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
throw error;
}
}
}
2. クエリのパフォーマンスを最適化する
複数フィールドのクエリには複合インデックスを追加します。
// このクエリには複合インデックスが必要です
const q = query(
collection(db, 'posts'),
where('category', '==', 'tech'),
where('views', '>', 1000),
orderBy('views', 'desc')
);
このクエリを実行すると、Firestoreは直接リンクを提供してインデックスの作成を促します。
3. バッチ操作
import { writeBatch } from 'firebase/firestore';
async function bulkUpdate(userIds, updates) {
const batch = writeBatch(db);
userIds.forEach(id => {
const ref = doc(db, 'users', id);
batch.update(ref, updates);
});
await batch.commit();
console.log(`${userIds.length} 人のユーザーを更新しました`);
}
// 1バッチあたり最大500件の操作
4. コストを監視する
Firebaseの料金:
| サービス | 無料枠 | 有料 |
|---|---|---|
| Firestore | 5万読み取り/日 | $0.036/10万読み取り |
| Storage | 5GB | $0.023/GB |
| Functions | 200万呼び出し | $0.40/100万 |
| Auth | 1万/月 | $0.0055/10万 |
Google Cloudコンソールで予算アラートを設定してください。
5. サービスアカウントを保護する
// 誤り: クライアントコードでこれを行わないでください
admin.initializeApp({
credential: admin.credential.cert(require('./serviceAccountKey.json'))
});
// 正しい: サーバー環境でのみ使用
const serviceAccount = JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT);
admin.initializeApp({
credential: admin.credential.cert(serviceAccount)
});
6. オフラインシナリオを処理する
// オフライン永続化を有効にする (ウェブ)
import { enableMultiTabIndexedDbPersistence } from 'firebase/firestore';
enableMultiTabIndexedDbPersistence(db)
.catch((err) => {
if (err.code === 'failed-precondition') {
// 複数のタブが開いている場合
} else if (err.code === 'unimplemented') {
// ブラウザがサポートしていない場合
}
});
// 接続状態をリッスン
import { onSnapshot } from 'firebase/firestore';
onSnapshot(doc(db, 'status', 'online'), (doc) => {
if (!doc.exists()) {
console.log('オフラインです');
// オフラインUIを表示
}
});
Firebase APIの一般的な問題と解決策
問題1:アクセス拒否エラー
症状: Error: 7 PERMISSION_DENIED
原因: セキュリティルールが操作をブロックしている
修正:
- Firebaseコンソールでルールを確認
request.auth.uidが期待されるユーザーと一致するか検証- ルールプレイグラウンドでルールをテスト
問題2:トークンの有効期限切れ
症状: Error: ID token expired
修正:
// トークンを強制的に更新
const user = auth.currentUser;
if (user) {
await user.getIdToken(true); // 強制更新
}
問題3:コールドスタートの遅延
症状: Cloud Functionsが最初の呼び出しで2〜5秒かかる
修正:
// スケジュールされたpingでFunctionsをウォームアップ
exports.keepWarm = onSchedule('every 60 seconds', async () => {
await fetch('https://your-function.cloudfunctions.net/health');
});
問題4:クエリが空の結果を返す
症状: クエリはデータを返すはずだが、空の配列を返す
原因: インデックスの欠落またはフィールド順序の間違い
修正: Firestoreコンソール > インデックスで必要な複合インデックスを確認してください。
実世界のユースケース
フィンテックアプリ:リアルタイムトランザクション更新
ある決済スタートアップは、リアルタイムのトランザクション通知を構築するためにFirebase Firestoreを使用しました。決済が処理されると、Cloud Functionsが接続されたすべての管理ダッシュボードに200ミリ秒以内に更新をトリガーします。結果:保留中のトランザクションに関するサポートチケットが40%削減されました。
Eコマース:在庫同期
あるオンライン小売業者は、Firestoreリスナーを使用してウェブ、iOS、Android間で在庫を同期しています。在庫が変更されると、すべてのクライアントが自動的に更新されます。オフライン永続化により、倉庫作業員は接続がなくても商品をスキャンでき、再接続時に自動的に同期されます。
SaaS:マルチテナント認証
あるB2Bプラットフォームは、ロールベースのアクセスにカスタムクレームを備えたFirebase Authを使用しています。管理者ユーザーは、Firestoreテナント設定に対して検証を行うCloud Functionsを通じて、より高い権限を取得します。単一のコードベースで500以上の組織に、データは分離された状態でサービスを提供しています。
結論
Firebase APIの統合には、4つの主要サービスが含まれます。
- 認証: メール、Google、AppleサインインとJWTトークン
- Firestore: リアルタイムリスナーとセキュリティルールを備えたNoSQLデータベース
- Cloud Functions: イベントまたはHTTPによってトリガーされるサーバーレスバックエンド
- Storage: CDN配信を備えたファイルアップロード
認証フロー、データベース操作、関数デプロイ、ファイル管理について学びました。エラー処理、バッチ処理、オフラインサポート、セキュリティといった本番環境のパターンも確認しました。
FAQ
Firebaseは無料で利用できますか?
はい、FirebaseはSparkプランと呼ばれる手厚い無料枠を提供しており、5GBのストレージ、Firestoreの5万回/日の読み取り、Cloud Functionの200万回呼び出し、Authの1万ユーザー/月が含まれます。有料プラン(Blaze)は従量課金制です。
既存のデータベースとFirebaseを併用できますか?
はい。Firebase Extensionsを使用してPostgreSQL、MySQL、またはMongoDBと同期できます。あるいは、Cloud Functionsから外部APIを呼び出して既存のシステムと統合することも可能です。
Firebaseから他のプラットフォームへ移行するにはどうすればよいですか?
Firestoreのエクスポート機能またはFirebase CLIを使用してデータをエクスポートします。大規模なデータセットの場合は、Dataflowエクスポートパイプラインを使用します。移行の複雑さは、データ構造によって異なります。
FirebaseはGraphQLをサポートしていますか?
ネイティブにはサポートしていません。firestore-graphqlのようなサードパーティソリューションを使用するか、Cloud FunctionsとApollo Serverを使用してGraphQLレイヤーを構築します。
Firebaseをオンプレミスで使用できますか?
いいえ。FirebaseはGoogle Cloud専用です。セルフホスト型の代替案としては、Appwrite、Supabase、Nhostなどを検討してください。
100MBを超えるファイルのアップロードはどのように処理しますか?
チャンク分割による再開可能なアップロードを使用します。Firebase SDKはこれを自動的に処理します。非常に大きなファイルの場合は、署名付きURLを使用してGoogle Cloud Storageを直接使用します。
Firestoreのクエリ制限を超過するとどうなりますか?
クエリはFAILED_PRECONDITIONエラーで失敗します。必要なインデックスを追加するか、クエリを再構築してください。Firestoreは、エラーメッセージに欠落しているインデックスを作成するための直接リンクを提供します。
FirebaseはGDPRに準拠していますか?
はい、FirebaseはGDPRに準拠したデータ処理を提供しています。特定のリージョンでのデータ所在地を有効にし、ユーザーデータのエクスポート/削除を実装し、Googleのデータ処理規約に署名します。
