# 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="/files/dYGNAiOMelYPoEqOAcfJ" 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="/files/3TkZ4yWOgROs35M6W05H" 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="/files/zWNE1k52I65WcfegUhAU" 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="/files/aa64vClHNZkKyDWpufd1" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/jeS53iMF9O7Qo7wlPeFK" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/MohTP07irrqEZwiQ9Egs" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/076RfN9ma7VdbYtYUrDQ" 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="/files/zIfZp7m4me5tm4cmCwwc" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/qjnDbhmNpNXxanfmLE7U" 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="/files/EyrAmBHIrjnbkWvexhJ3" 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>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://bizachi-dev.gitbook.io/bullethell-elemental-template/getting-started/quickstart.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
