【第3回】パチ飯マップ — Firebase × サブドメインでエリアを分割する設計

はじめに

前回は、Google Maps + Places API を使って地図タップや検索バーからお店を探す仕組みを紹介しました。

今回は、このアプリが複数のエリアに対応するための設計——サブドメインでエリアを分割し、エリアごとに独立した Firebase データベースを使い分ける仕組みを詳しく解説します。同一コードベースで複数のコミュニティ向けサービスを展開する際の参考になれば幸いです。

なぜサブドメインでエリアを分けるのか

このアプリは最初から「複数エリアへの展開」を前提に設計しています。サブドメイン方式を選んだ理由は次の3点です。

  • コミュニティの独立性:別エリアのユーザーには互いの投稿が見えない。同じコードを使いながら、それぞれが「自分たちのマップ」として使える
  • データの独立性:Firestore の名前付きデータベース機能と組み合わせることで、エリア単位でバックアップ・削除・セキュリティルール設定が完結する
  • URLの自然さarea-a.example.com というURLだけで「どのエリアのマップか」が一目でわかる

以下は3つのエリアそれぞれの実際の画面です。サブドメインが変わるだけで、表示される地図の中心・投稿データ・コミュニティが完全に切り替わります。

鶴見エリアのコミュニティマップ(tsurumi.pachi-meshi.com)
蒲田エリアのコミュニティマップ(kamata.pachi-meshi.com)
秋葉原エリアのコミュニティマップ(akihabara.pachi-meshi.com)

Firebase × サブドメインで実現した3つの技術ポイント

1. window.location からサブドメインを取得してエリアを自動判定する

課題:「どのエリアにいるか」をURLから判定する処理が各コンポーネントに散らばると、エリア追加時の修正漏れが発生しやすい。
解決策:detectArea() 関数をモジュールの初期化時に1度だけ実行し、結果をモジュールスコープの定数として固定する。アプリ全体がこの定数を参照することで、判定ロジックを一箇所に集約できる。

以下は、サブドメインを取得してエリアを特定するコードです。有効なエリア識別子でなければデフォルトのエリアにフォールバックします。

const VALID_AREAS = ['area-a', 'area-b', 'area-c'] as const
type Area = (typeof VALID_AREAS)[number]

function detectArea(): Area {
  const subdomain = window.location.hostname.split('.')[0]
  return (VALID_AREAS as readonly string[]).includes(subdomain)
    ? (subdomain as Area)
    : 'area-a' // localhost / ルートドメインはデフォルト
}

export const areaId = detectArea()

ローカル開発時は localhost がサブドメインに該当しないため、自動的にデフォルトエリアが使われます。Firebase Hosting の複数サイト機能でサブドメインを割り当てた本番環境では、アクセスしたURLに応じて正しいエリアが選ばれます。

2. Firebase Named Database でエリアごとにデータを完全分離する

課題:1つの Firestore データベースにすべてのエリアのデータを混在させると、エリアをまたいだクエリのミスや、セキュリティルールの複雑化が起きやすい。また、特定エリアのデータだけを削除したいときに手間がかかる。
解決策:Firebase Firestore の名前付きデータベース(Named Database)機能を使い、エリアIDをそのままDB名に使って1エリア1DBの構成にする。アプリ起動時に判定した areaIdgetFirestore() の第2引数に渡すだけで、正しいDBに自動接続できる。

以下は、エリアに対応したDBインスタンスを生成するコードです。

import { initializeApp } from 'firebase/app'
import { getFirestore } from 'firebase/firestore'

const app = initializeApp(firebaseConfig)

// アクセスしたサブドメインに対応する DB に自動接続
export const db = getFirestore(app, areaId)

// 統合管理画面用:全エリアの DB 参照を一括保持
export const dbAll: Record<Area, ReturnType<typeof getFirestore>> = {
  'area-a': getFirestore(app, 'area-a'),
  'area-b': getFirestore(app, 'area-b'),
  'area-c': getFirestore(app, 'area-c'),
}

db は各エリアのフロント画面で使う通常のDB参照です。dbAll はルートドメインに配置した統合管理画面専用で、全エリアのデータを横断して閲覧・操作できます。ユーザーには db だけが見えているため、エリアをまたいだデータの混入が構造的に起きません。

3. エリアごとの地図設定を areaConfig.ts で一元管理する

課題:各エリアの地図中心座標やバウンディングボックス(検索対象の矩形範囲)を各コンポーネントにハードコードすると、エリアを追加するたびに複数ファイルを修正する必要が生じる。
解決策:areaConfig.ts に全エリアの設定を集中管理し、areaId で自動解決されるエクスポートを用意する。コンポーネント側は areaCenterareaBounds を読み込むだけでよく、エリアを追加するときは areaConfig.ts を1箇所だけ修正すればよい。

以下は、全エリアの設定をオブジェクトで管理し、現在のエリアに対応する値を自動解決するコードです。

import { areaId } from './firebase'

const AREA_CONFIG = {
  'area-a': {
    center: { lat: 35.510, lng: 139.677 },
    bounds: { north: 35.527, south: 35.493, east: 139.702, west: 139.654 },
  },
  'area-b': {
    center: { lat: 35.560, lng: 139.715 },
    bounds: { north: 35.580, south: 35.540, east: 139.740, west: 139.695 },
  },
  'area-c': {
    center: { lat: 35.701, lng: 139.774 },
    bounds: { north: 35.720, south: 35.685, east: 139.795, west: 139.755 },
  },
} as const

export const areaCenter = AREA_CONFIG[areaId].center
export const areaBounds = AREA_CONFIG[areaId].bounds

areaCenter は地図の初期表示位置として使い、areaBounds は前回紹介した Places Autocomplete のバイアス設定や、エリア外判定のバウンディングボックスとして使います。設定値の形式(緯度・経度の矩形)が全エリアで統一されているため、コンポーネント側は値の種類を意識せずに読み込むだけで済みます。

まとめ:サブドメイン × Named Database でスケールしやすいマルチエリア設計を作る

今回の記事で紹介したポイントをまとめます。

  • detectArea() でサブドメインをモジュール初期化時に1度だけ判定し、定数として固定することで判定ロジックをアプリ全体で一元管理できる
  • Firebase の名前付きデータベースを使ってエリアごとにDBを物理分離すると、セキュリティルール・バックアップ・データ削除がエリア単位で完結する
  • areaConfig.ts に設定を集中管理すれば、エリア追加時の修正箇所が1ファイルに限定される
  • dbAll で全エリアのDB参照を束ねておくことで、統合管理画面から横断アクセスができる

次回は、Cloud Functions + Twitter API で新着投稿を自動ツイートする仕組みを詳しく解説します。Cloud Functions のトリガー設定、Twitter API v2 による投稿、日本語2文字換算の文字数制限対応、リトライロジックなど、実運用で必要になる実装を紹介します。