# Firebase Backend v1.5

> **Scope** ‑ This document explains how to integrate the BulletHell Elemental Template v1.4 with Firebase Authentication and Cloud Firestore. It covers both a **Quick Start** for experienced users and a **Step‑by‑Step** walkthrough with screenshots for newcomers.

***

Read the offline backend page first

***

### 1. Quick Start (Experienced Users)

**Unity**

* Unity Version: **2022.3 LTS (or newer)**

**Unity Packages (via Package Manager)**

* Input System
* AI Navigation
* In-App Purchasing
* 2D Sprite
* TextMeshPro (TMP)

**Firebase SDK**

Import the following Firebase SDK packages (same major/minor, recommended):

* **FirebaseAuth 12.10.1**
* **FirebaseFirestore 12.10.1**
* **FirebaseDatabase (Realtime Database) 12.10.1**

Project setup:

* In **Project Settings → Player → Scripting Define Symbols**, add:
  * `FIREBASE`
* Import the **BulletHell Elemental Template**.
* Add scenes to **Build Settings** in this order:
  1. `Login`
  2. `Home`
  3. Other gameplay/maps scenes
* Tags / Layers:
  * Ensure the **Monster** tag exists and is assigned to all enemy prefabs.
* `google-services.json`:
  * Download from Firebase Console.
  * Place in: `Assets/StreamingAssets/` (create the folder if missing).
  * File name must remain **google-services.json**.

***

### 2. Firebase Console Setup

#### 2.1 Authentication

In **Authentication → Sign-in method**:

* Enable:
  * **Email/Password**
  * **Anonymous**

No special action needed here for user IDs; they are handled by the game code using Firebase Auth UID + Firestore metadata.

***

#### 2.2 Firestore Database

Create a **Cloud Firestore** database in **Production mode** (or test mode during development, but with the rules below).

**Required Collections & Documents**

1. **Players**
   * Will be created automatically by the game on first login/registration.
   * No manual documents required.
2. **BattlePass / SeasonInfo**

   Create:

   * Collection: `BattlePass`
   * Document ID: `SeasonInfo`
   * Fields:
     * `Season` — **Number**, e.g. `1`
     * `StartSeason` — **Timestamp**, season start date
     * (Optional) `DurationDays` — **Number**, total days for the season
3. **Admin / Admin**

   Create:

   * Collection: `Admin`
   * Document ID: `Admin`
   * Fields:
     * `gm` — **Array of string**
       * Each entry is a **Firebase Auth UID** of a GM user.
     * `support` — **Array of string**
       * Each entry is a **Firebase Auth UID** of a support user.

   These arrays control who can use GM tools (Events, Mailbox, Global Message, Ban, etc).
4. **Admin / Events**

   Create:

   * Collection: `Admin`
   * Document ID: `Events`
   * You can leave it empty or add fields according to your tooling.
   * Used by the in-game GM tools for map events (must exist for writes under provided rules).
5. **Admin / GlobalMessage**

   Create:

   * Collection: `Admin`
   * Document ID: `GlobalMessage`
   * Used for global announcements.
6. **Admin / Mailbox**

   Create:

   * Collection: `Admin`
   * Document ID: `Mailbox`
   * Can start empty.
   * Used by GM tools to send reward mails.
7. **Friendships**
   * Create a **root-level collection**: `Friendships`.
   * Add **one placeholder document** (for example `placeholder`), so the collection exists.
     * Any shape is fine when created via Console (rules don’t block console writes).
   * Actual friendship documents are created and managed by the game code using the rules below.
8. **Meta / UserCounters**

   Used to assign incremental numeric `userId` values.

   * Create collection: `Meta`
   * Create document: `UserCounters`
   * Add field:
     * `lastUserId` — **Number**
       * Example: `1000` (or `0`, or any starting point you prefer)
   * The game will atomically increment this value when new users are provisioned.

#### 2.3 Firestore Security Rules

Paste the following into **Firestore → Rules**:

```javascript
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    /* Helpers */

    function isSignedIn() {
      return request.auth != null;
    }

    function isOwner(uid) {
      return isSignedIn() && request.auth.uid == uid;
    }

    function hasRole(list, uid) {
      return list is list && uid in list;
    }

    /**
     * Returns true if the current user UID is listed as GM or Support
     * inside Admin/Admin document ("gm" or "support" string arrays).
     */
    function isGmOrSupport() {
      return isSignedIn()
        && (
          hasRole(
            get(/databases/$(database)/documents/Admin/Admin).data.gm,
            request.auth.uid
          )
          || hasRole(
            get(/databases/$(database)/documents/Admin/Admin).data.support,
            request.auth.uid
          )
        );
    }
    
    /* PLAYERS */

    match /Players/{uid} {
      allow get, list: if isSignedIn();

      allow create: if isOwner(uid);
      allow delete: if isOwner(uid);

      // Owner can update anything.
      // GM/Support can only change "isBanned".
      allow update: if
        isOwner(uid)
        || (
          isGmOrSupport()
          && resource.data.diff(request.resource.data).changedKeys().hasOnly(['isBanned'])
        );

      match /PurchasedItems/Items/List/{itemId} {
        allow get, list, create, update, delete: if isOwner(uid);
      }

      match /{document=**} {
        allow get, list, create, update, delete: if isOwner(uid);
      }
    }

    /* PLAYER NAMES */

    match /PlayerNames/{nameLower} {
      allow get: if true;
      allow list: if false;

      allow create: if isSignedIn()
        && request.resource.data.keys().hasOnly(['ownerUid', 'createdAt'])
        && request.resource.data.ownerUid == request.auth.uid;

      allow update, delete: if isSignedIn()
        && resource.data.ownerUid == request.auth.uid
        && request.resource.data == resource.data;
    }

    /* CURRENCIES */

    match /Players/{uid}/Currencies/{coinId} {
      allow get: if isOwner(uid);
      allow list: if false;
      allow delete: if false;

      allow create: if isOwner(uid)
        && request.resource.data.keys().hasOnly(['initialAmount', 'amount'])
        && request.resource.data.initialAmount is int
        && request.resource.data.amount is int
        && request.resource.data.initialAmount >= 0
        && request.resource.data.amount >= 0;

      allow update: if isOwner(uid)
        && request.resource.data.keys().hasOnly(['amount'])
        && request.resource.data.amount is int
        && request.resource.data.amount >= 0;
    }

    /* BATTLE PASS META */

    match /BattlePass/{docId} {
      allow get: if isSignedIn() && docId == "SeasonInfo";
      allow list: if false;
      allow create, update, delete: if false;
    }

    /* ADMIN / ROLES */

    match /Admin/Admin {
      allow get: if isSignedIn();
      allow list: if false;
      allow create, update, delete: if false;
    }

    /* ADMIN MAP EVENTS */

    match /Admin/Events {
      allow get: if isSignedIn();
      allow list: if false;
      allow create, update, delete: if isGmOrSupport();
    }

    /* ADMIN MAILBOX */

    match /Admin/Mailbox {
      allow get, list, create, update, delete: if isGmOrSupport();
    }

    /* ADMIN GLOBAL MESSAGE */

    match /Admin/GlobalMessage {
      allow get: if isSignedIn();
      allow list: if false;
      allow create, update, delete: if isGmOrSupport();
    }

    /* OTHER ADMIN DOCS DEFAULT */

    match /Admin/{other} {
      allow get: if isSignedIn();
      allow list, create, update, delete: if false;
    }

    /* FRIENDSHIPS */

    match /Friendships/{fid} {

      function validUsersArray(data) {
        return data.users is list
          && data.users.size() == 2
          && data.users[0] is string
          && data.users[1] is string
          && data.users[0] != data.users[1];
      }

      function sortedIdFor(data) {
        let a = data.users[0];
        let b = data.users[1];
        return (a < b) ? (a + "_" + b) : (b + "_" + a);
      }

      function isParticipant(uid) {
        return uid in resource.data.users;
      }

      function userIndex(uid) {
        return resource.data.users[0] == uid ? 0 :
               (resource.data.users[1] == uid ? 1 : -1);
      }

      function isRequester(uid) {
        return resource.data.requestedBy == uid;
      }

     function friendshipTransitionAllowed() {
        let before = resource.data.state;
        let after = request.resource.data.state;
        let uid = request.auth.uid;
        let idx = userIndex(uid);

        return (
          (before == "PENDING" && after == "ACCEPTED" && !isRequester(uid)) ||
          (before == "PENDING" && after == "PENDING" && isRequester(uid)) ||
          (after == "BLOCKED_0" && idx == 0) ||
          (after == "BLOCKED_1" && idx == 1)
        );
      }

      // Read: only participants.
      allow get, list: if isSignedIn() && isParticipant(request.auth.uid);

      // Create: send friend request (PENDING).
      allow create: if isSignedIn()
        && validUsersArray(request.resource.data)
        && sortedIdFor(request.resource.data) == fid
        && request.auth.uid in request.resource.data.users
        && request.resource.data.requestedBy == request.auth.uid
        && request.resource.data.state == "PENDING"
        && request.resource.data.createdAt is timestamp
        && request.resource.data.updatedAt is timestamp;

      // Update: accept, block, or refresh pending.
      allow update: if isSignedIn()
        && isParticipant(request.auth.uid)
        && validUsersArray(request.resource.data)
        && request.resource.data.users == resource.data.users
        && request.resource.data.requestedBy == resource.data.requestedBy
        && request.resource.data.createdAt == resource.data.createdAt
        && request.resource.data.updatedAt is timestamp
        && friendshipTransitionAllowed();

      // Delete:
      // - requester can cancel pending
      // - other user can reject pending
      // - either can unfriend (ACCEPTED)
      // - blocker can unblock (BLOCKED_x)
      allow delete: if isSignedIn()
        && isParticipant(request.auth.uid)
        && (
          (resource.data.state == "PENDING") ||
          (resource.data.state == "ACCEPTED") ||
          (resource.data.state == "BLOCKED_0"
            && request.auth.uid == resource.data.users[0]) ||
          (resource.data.state == "BLOCKED_1"
            && request.auth.uid == resource.data.users[1])
        );
    }
    match /Meta/UserCounters {
      allow get: if isSignedIn();
      allow create, update: if isSignedIn()
        && request.resource.data.keys().hasOnly(['lastUserId'])
        && request.resource.data.lastUserId is int
        && (
          !resource.exists()
          ||
          (request.resource.data.lastUserId > resource.data.lastUserId)
        );

      allow delete: if false;
    }
  }
}

```

### 3. Realtime Database Setup

#### 3.1 Create Realtime Database

In **Firebase Console → Realtime Database**:

1. Create a new database in the same project.
2. Choose a location (any supported region).
3. You can start in locked mode and then paste the rules below.

#### 3.2 Base Structure

The game will create nodes automatically when presence & invites are used, but you may create them upfront (empty) for clarity:

* Root-level:
  * `status` (empty object)
  * `invites` (empty object)
  * `chat` is created automatically when global chat is used.

Example initial JSON:

```json
{
  "status": {},
  "invites": {}
}
```

#### 3.3 Realtime Database Rules

Paste the following into **Realtime Database → Rules**:

```javascript
{
  "rules": {
    "status": {
      "$uid": {
        ".read": "auth != null && auth.uid == $uid",
        ".write": "auth != null && auth.uid == $uid"
      }
    },
    "invites": {
      "$targetUid": {
        ".read": "auth != null && auth.uid == $targetUid",
        "$inviteId": {
          ".write": "auth != null && newData.child('fromUid').val() === auth.uid"
        }
      }
    },
    "chat": {
      ".read": "auth != null",
      ".write": "false",
      ".indexOn": ["timestamp"],
      "$messageId": {
        ".write": "auth != null && !data.exists()",
        ".validate": "newData.hasChildren(['playerId', 'playerName', 'iconId', 'frameId', 'message', 'timestamp', 'isHighlighted'])",
        "playerId": {
          ".validate": "newData.isString() && newData.val() === auth.uid"
        },
        "playerName": {
          ".validate": "newData.isString() && newData.val().length > 0 && newData.val().length <= 50"
        },
        "iconId": {
          ".validate": "newData.isString() && newData.val().length > 0"
        },
        "frameId": {
          ".validate": "newData.isString() && newData.val().length > 0"
        },
        "message": {
          ".validate": "newData.isString() && newData.val().length > 0 && newData.val().length <= 500"
        },
        "timestamp": {
          ".validate": "newData.isNumber() && newData.val() <= now"
        },
        "isHighlighted": {
          ".validate": "newData.isBoolean()"
        }
      }
    }
  }
}
```

### **For developers who have not yet set up a project with Firebase or who have any questions, below is a detailed step-by-step guide with images:**

#### 2. Step‑by‑Step Guide (With Images)

**2.1 Create a URP Project**

1. Open **Unity Hub → New Project**.
2. Select **Universal 3D (URP)** template.
3. Name your project and click **Create**.

*Optional* – HDRP/Built‑in pipelines work, but demo textures may need conversion.

<figure><img src="https://215355839-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fc8AVbPtcUOjcVrQx9Eyu%2Fuploads%2Fr1MdhmJQQGCTAywH7BBT%2F0.png?alt=media&#x26;token=6ff9476b-8df2-41e3-9780-b87b40b4e977" alt=""><figcaption></figcaption></figure>

**2.2 Install Required Unity Packages**

*Window → Package Manager → Unity Registry*. Add *Input System*, *AI Navigation*, *In‑App Purchasing*, *2D Sprite*, *TextMeshPro*.

<figure><img src="https://215355839-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fc8AVbPtcUOjcVrQx9Eyu%2Fuploads%2FZ22fAlXiXThldRiHKlnO%2F20.png?alt=media&#x26;token=b99175ac-da6a-466e-8c3f-0c296040423f" alt=""><figcaption></figcaption></figure>

**2.3 Import Firebase SDK**

1. Download lastest `FirebaseAuth.unitypackage` , `FirebaseFirestore.unitypackage`  and `FirebaseDatabase.unitypackage`  from the [Firebase Unity SDK Archive](https://developers.google.com/unity/archive).
2. Double‑click each package to import.
3. When prompted, allow **ExternalDependancyManager** to resolve libraries.
4. add FIREBASE in Scripting Define Symbols in Build Settings > Player

<figure><img src="https://215355839-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fc8AVbPtcUOjcVrQx9Eyu%2Fuploads%2F0HOZ4kkmWovLP0YkguoR%2F8.png?alt=media&#x26;token=2f97a044-f3a5-4c31-8f69-29397f80449f" alt=""><figcaption></figcaption></figure>

**2.4 Import the Template**

Drag the *BulletHell Elemental Template* `.unitypackage` into the Editor or install via **Package Manager → My Assets**.

**2.5 Configure Project Settings**

* **Player Settings → Identification → Package Name** must match the *Android/iOS* package names you register in Firebase.
* Switch Platform to PC, **Android** or **iOS** before building (Editor → File → Build Settings).

**2.6 Firebase Console Configuration**

1. **Create Project** → add Android and/or iOS app with the correct package name.
2. Download **google‑services.json** (Android) or **GoogleService‑Info.plist** (iOS).
3. Place *json* file in **Assets/StreamingAssets/**. *If Unity cannot locate the file, verify the folder name and that the filename has no suffix (e.g., (1)).*

<figure><img src="https://215355839-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fc8AVbPtcUOjcVrQx9Eyu%2Fuploads%2FW5wJm4637a1ZDvIe7crU%2FAddTags.png?alt=media&#x26;token=4cded9d3-5b7c-4251-bac3-c25b2f2a5a07" alt=""><figcaption></figcaption></figure>

<figure><img src="https://215355839-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fc8AVbPtcUOjcVrQx9Eyu%2Fuploads%2FOwVME9RRvyQPHZNG5qhz%2FAddMaps.png?alt=media&#x26;token=359b874c-0ccf-4fd0-abfc-8ddc7767d929" alt=""><figcaption></figcaption></figure>

<figure><img src="https://215355839-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fc8AVbPtcUOjcVrQx9Eyu%2Fuploads%2FsThaxv1NJXPe363JWqOV%2FApp.png?alt=media&#x26;token=5e59cc19-4d56-4b7c-954e-db21f6a0d2d0" alt=""><figcaption></figcaption></figure>

<figure><img src="https://215355839-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fc8AVbPtcUOjcVrQx9Eyu%2Fuploads%2FVkhvH1OFITHnCuIOxa2O%2F17.png?alt=media&#x26;token=e6697665-a408-49f5-a0bc-91eaad880dec" alt=""><figcaption></figcaption></figure>

1. **Firestore Database → Start in production mode**.
2. See the step-by-step instructions and rules at the beginning regarding which fields to create in Firestore.
3. Apply the security rules (see Quick Start above).

**2.8 Prepare Cloud Firestore**

* **Authentication → Sign‑in method**: enable **Email/Password** and **Anonymous**.

**2.7 Enable Authentication**

<figure><img src="https://215355839-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fc8AVbPtcUOjcVrQx9Eyu%2Fuploads%2FpWXKMFiejX5czMulgq3i%2FAuth.png?alt=media&#x26;token=a5a6fa05-717e-4c57-80a8-b728247bd091" alt=""><figcaption></figcaption></figure>

<figure><img src="https://215355839-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fc8AVbPtcUOjcVrQx9Eyu%2Fuploads%2FfMpPoGe3sfKlCuNqm60N%2FpreviewFirestore.png?alt=media&#x26;token=55663b68-a271-47b8-a281-b64bcaf587e1" alt=""><figcaption></figcaption></figure>

### 3. Realtime Database Setup

#### 3.1 Create Realtime Database

In **Firebase Console → Realtime Database**:

1. Create a new database in the same project.
2. Choose a location (any supported region).
3. You can start in locked mode and then paste the rules below.

#### 3.2 Base Structure

The game will create nodes automatically when presence & invites are used, but you may create them upfront (empty) for clarity:

* Root-level:
  * `status` (empty object)
  * `invites` (empty object)
  * `chat` is created automatically when global chat is used.

Example initial JSON (optional):

```json
{
  "status": {},
  "invites": {}
}
```

<figure><img src="https://215355839-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fc8AVbPtcUOjcVrQx9Eyu%2Fuploads%2FX6DhNMg0Xr6VKI3UJraY%2FpreviewRealtime.png?alt=media&#x26;token=bf4e05fd-69ee-4985-bd44-ccd80dcfa92d" alt=""><figcaption></figcaption></figure>

**2.9 Verify Tags, Layers & Scenes**

If you imported the template into an existing project:

* Add **Monster** tag in **Edit → Project Settings → Tags & Layers**.
* Assign it to enemy prefabs in `BulletHellTemplate/Resources/Monsters/`.
* Ensure scene order in **Build Settings**: `Login`, `Home`, *others*.

**2.10 Test in Editor**

1. Open `Login` scene.
2. Press **Play** – the template auto‑creates a local account, syncs with Firebase, and loads initial data.
3. Check **Console** for Firebase initialization logs.

#### 3. Common Issues & Fixes

| Issue                                            | Cause                  | Fix                                                                                                            |
| ------------------------------------------------ | ---------------------- | -------------------------------------------------------------------------------------------------------------- |
| `google‑services.json` not found                 | File in wrong location | Place inside **Assets/StreamingAssets/**. Refresh project.                                                     |
| "Unknown error" on account creation              | Weak password          | Use at least 8 chars, 1 number, 1 special char.                                                                |
| Android/iOS build fails on "Validate References" | Unused platform DLLs   | *Edit → Project Settings → Player → Other Settings*, disable **Validate References** for unsupported platform. |

***

#### 4. Next Steps

* Customize Battle Pass seasons by updating `Season` and `StartSeason` fields.
* Harden security with **App Check** or move save logic to **Cloud Functions** (requires paid Firebase plan).
* Join our community for support and feature requests.

**Discord** – <https://discord.com/invite/EGGj77g3eQ\\>
**Email**    – <rafbizachi5@gmail.com>
