firebase-patterns

star 8

Firebase開発のベストプラクティス。Authentication、Firestore、Storage、Cloud Functions、Security Rulesについて。Firebaseの実装やセキュリティルールについて質問があるときに使用。

ecorismap By ecorismap schedule Updated 1/23/2026

name: firebase-patterns description: Firebase開発のベストプラクティス。Authentication、Firestore、Storage、Cloud Functions、Security Rulesについて。Firebaseの実装やセキュリティルールについて質問があるときに使用。

Firebase パターン

EcorisMapにおけるFirebase(Authentication, Firestore, Storage, Functions)のベストプラクティスです。

Firebase Authentication

認証フロー

import auth from '@react-native-firebase/auth';

// メール/パスワード認証
const signInWithEmail = async (email: string, password: string) => {
  try {
    const userCredential = await auth().signInWithEmailAndPassword(email, password);

    // メール確認チェック
    if (!userCredential.user.emailVerified) {
      throw new Error('メールアドレスが確認されていません');
    }

    return userCredential.user;
  } catch (error) {
    handleAuthError(error);
  }
};

// 認証状態の監視
useEffect(() => {
  const unsubscribe = auth().onAuthStateChanged(user => {
    if (user && user.emailVerified) {
      setUser(user);
    } else {
      setUser(null);
    }
  });

  return unsubscribe;
}, []);

エラーハンドリング

const handleAuthError = (error: any) => {
  switch (error.code) {
    case 'auth/user-not-found':
      return 'ユーザーが見つかりません';
    case 'auth/wrong-password':
      return 'パスワードが間違っています';
    case 'auth/email-already-in-use':
      return 'このメールアドレスは既に使用されています';
    case 'auth/weak-password':
      return 'パスワードが弱すぎます';
    default:
      return '認証エラーが発生しました';
  }
};

Firestore

データ構造

// コレクション構造
// users/{userId}
// projects/{projectId}
// projects/{projectId}/layers/{layerId}
// projects/{projectId}/features/{featureId}

interface Project {
  id: string;
  name: string;
  ownerId: string;
  members: string[];
  permission: 'PRIVATE' | 'PUBLIC' | 'COMMON' | 'TEMPLATE';
  createdAt: Timestamp;
  updatedAt: Timestamp;
}

interface Feature {
  id: string;
  layerId: string;
  geometry: GeoJSON.Geometry;
  properties: Record<string, any>;
  encdata?: string;      // 暗号化データ
  encryptedAt?: Timestamp;
}

CRUD操作

import firestore from '@react-native-firebase/firestore';

// 作成
const createProject = async (project: Omit<Project, 'id'>) => {
  const ref = firestore().collection('projects').doc();
  await ref.set({
    ...project,
    id: ref.id,
    createdAt: firestore.FieldValue.serverTimestamp(),
    updatedAt: firestore.FieldValue.serverTimestamp(),
  });
  return ref.id;
};

// 読み取り
const getProject = async (projectId: string) => {
  const doc = await firestore().collection('projects').doc(projectId).get();
  return doc.exists ? doc.data() as Project : null;
};

// 更新
const updateProject = async (projectId: string, data: Partial<Project>) => {
  await firestore().collection('projects').doc(projectId).update({
    ...data,
    updatedAt: firestore.FieldValue.serverTimestamp(),
  });
};

// 削除
const deleteProject = async (projectId: string) => {
  await firestore().collection('projects').doc(projectId).delete();
};

リアルタイム監視

// ドキュメント監視
useEffect(() => {
  const unsubscribe = firestore()
    .collection('projects')
    .doc(projectId)
    .onSnapshot(doc => {
      if (doc.exists) {
        setProject(doc.data() as Project);
      }
    });

  return unsubscribe;
}, [projectId]);

// クエリ監視
useEffect(() => {
  const unsubscribe = firestore()
    .collection('projects')
    .where('members', 'array-contains', userId)
    .orderBy('updatedAt', 'desc')
    .onSnapshot(snapshot => {
      const projects = snapshot.docs.map(doc => doc.data() as Project);
      setProjects(projects);
    });

  return unsubscribe;
}, [userId]);

バッチ操作

const batchUpdate = async (features: Feature[]) => {
  const batch = firestore().batch();

  features.forEach(feature => {
    const ref = firestore().collection('features').doc(feature.id);
    batch.update(ref, {
      ...feature,
      updatedAt: firestore.FieldValue.serverTimestamp(),
    });
  });

  await batch.commit();
};

Security Rules

基本ルール

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // 認証済みユーザーのみ
    function isAuthenticated() {
      return request.auth != null && request.auth.token.email_verified == true;
    }

    // ドキュメント所有者チェック
    function isOwner(userId) {
      return request.auth.uid == userId;
    }

    // プロジェクトメンバーチェック
    function isMember(projectId) {
      let project = get(/databases/$(database)/documents/projects/$(projectId)).data;
      return request.auth.uid in project.members;
    }

    // プロジェクト
    match /projects/{projectId} {
      allow read: if isAuthenticated() && (
        resource.data.permission == 'PUBLIC' ||
        resource.data.permission == 'TEMPLATE' ||
        isMember(projectId)
      );
      allow create: if isAuthenticated();
      allow update, delete: if isAuthenticated() && isOwner(resource.data.ownerId);
    }
  }
}

ロールベースアクセス

// ロール定義
function getRole(projectId) {
  let project = get(/databases/$(database)/documents/projects/$(projectId)).data;
  return project.roles[request.auth.uid];
}

function isOwnerRole(projectId) {
  return getRole(projectId) == 'owner';
}

function isAdminRole(projectId) {
  return getRole(projectId) in ['owner', 'admin'];
}

function isMemberRole(projectId) {
  return getRole(projectId) in ['owner', 'admin', 'member'];
}

Storage

ファイルアップロード

import storage from '@react-native-firebase/storage';

const uploadFile = async (
  filePath: string,
  storagePath: string,
  onProgress?: (progress: number) => void
) => {
  const reference = storage().ref(storagePath);
  const task = reference.putFile(filePath);

  if (onProgress) {
    task.on('state_changed', snapshot => {
      const progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
      onProgress(progress);
    });
  }

  await task;
  return reference.getDownloadURL();
};

Storage Rules

rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /projects/{projectId}/{allPaths=**} {
      // 20MB制限
      allow read: if request.auth != null;
      allow write: if request.auth != null
        && request.resource.size < 20 * 1024 * 1024
        && request.resource.contentType.matches('image/.*|application/pdf|application/x-sqlite3');
    }
  }
}

Cloud Functions

呼び出し

import functions from '@react-native-firebase/functions';

const callFunction = async (name: string, data: any) => {
  const fn = functions().httpsCallable(name);
  const result = await fn(data);
  return result.data;
};

// 使用例
const result = await callFunction('processData', { projectId: 'abc123' });

エミュレータ接続

if (__DEV__) {
  auth().useEmulator('http://localhost:9099');
  firestore().useEmulator('localhost', 8080);
  storage().useEmulator('localhost', 9199);
  functions().useEmulator('localhost', 5001);
}

オフライン対応

Firestoreキャッシュ

// オフラインの永続化を有効化
await firestore().settings({
  persistence: true,
  cacheSizeBytes: firestore.CACHE_SIZE_UNLIMITED,
});

// キャッシュから読み取り
const getCachedData = async (path: string) => {
  try {
    const doc = await firestore().doc(path).get({ source: 'cache' });
    return doc.data();
  } catch {
    // キャッシュにない場合はサーバーから
    const doc = await firestore().doc(path).get({ source: 'server' });
    return doc.data();
  }
};

エラーハンドリング

const handleFirestoreError = (error: any) => {
  switch (error.code) {
    case 'permission-denied':
      return 'アクセス権限がありません';
    case 'not-found':
      return 'データが見つかりません';
    case 'already-exists':
      return 'データが既に存在します';
    case 'unavailable':
      return 'サービスが一時的に利用できません';
    default:
      return 'エラーが発生しました';
  }
};
Install via CLI
npx skills add https://github.com/ecorismap/ecorismap --skill firebase-patterns
Repository Details
star Stars 8
call_split Forks 1
navigation Branch main
article Path SKILL.md
More from Creator