|
| 1 | +# Firestore schema (Spendee - inferred on 2025.08) |
| 2 | + |
| 3 | +This document outlines the Firestore hierarchy and the main document/field schema used by the Spendee data export as observed from the client and example documents. |
| 4 | + |
| 5 | +## Top-level hierarchy |
| 6 | + |
| 7 | +- users/{userId} |
| 8 | + - wallets/{walletId} |
| 9 | + - transactions/{transactionId} |
| 10 | + - transactionLabels (collection) |
| 11 | + - categories/{categoryId} |
| 12 | + - labels/{labelId} |
| 13 | + |
| 14 | +Notes: |
| 15 | +- Collections are named exactly as above. Documents often include a `path` field which repeats ids for convenience (e.g. `path.wallet`, `path.transaction`). |
| 16 | +- `transactions` documents reference categories by ID. There is a separate `transactionLabels` subcollection storing labels used for each transaction. |
| 17 | + |
| 18 | +## Document structures and fields (examples / inferred types) |
| 19 | + |
| 20 | +- users/{user} |
| 21 | + - path: { user: string } (document path metadata) |
| 22 | + - firestoreDataExportDone: boolean |
| 23 | + - name: string (user name) |
| 24 | + - email: string |
| 25 | + - legacyId: integer (legacy numeric id) |
| 26 | + - profile: object (profile metadata) |
| 27 | + - birthDate: string (YYYY-MM-DD) |
| 28 | + - gender: nullable string |
| 29 | + - email: string |
| 30 | + - firstname: string |
| 31 | + - lastname: string |
| 32 | + - photo: url string (profile image) |
| 33 | + - subscription: object (optional) |
| 34 | + - introOfferUsedOn: array |
| 35 | + - isNftOwner: nullable boolean |
| 36 | + - productId: string |
| 37 | + - renewalPeriod: string |
| 38 | + - pastType: string |
| 39 | + - expirationDate: nullable Timestamp |
| 40 | + - renewalDate: Timestamp |
| 41 | + - type: string (subscription tier) |
| 42 | + - platform: string |
| 43 | + - referral: object (optional) |
| 44 | + - code: string |
| 45 | + - isRegisteredViaReferral: boolean |
| 46 | + - referredUsersCount: integer |
| 47 | + - timezone: string (IANA tz, e.g. `Europe/Budapest`) |
| 48 | + - homeFeed: object (UI state) |
| 49 | + - lastDisplayedCard: string (card id) |
| 50 | + - device: object |
| 51 | + - languageCode: string (e.g. `hu`) |
| 52 | + - countryCode: string (e.g. `HU`) |
| 53 | + - registeredAt: Timestamp |
| 54 | + - updatedAt: Timestamp |
| 55 | + - globalCurrency: string (e.g. `HUF`) |
| 56 | + - modelVersion: number |
| 57 | + - visibleWallets: array of wallet id strings |
| 58 | + |
| 59 | +Notes: |
| 60 | +- Many of these are optional; `subscription` and `referral` may be absent or contain nullable members. |
| 61 | + |
| 62 | +- users/{user}/wallets/{wallet} |
| 63 | + - path: { wallet: string, user: string } |
| 64 | + - name: string |
| 65 | + - type: string (e.g., "cash", "bank") |
| 66 | + - currency: string (e.g., "HUF", "EUR") |
| 67 | + - status: string (e.g., "active") |
| 68 | + - startingBalance: string (decimal formatted as string; may be negative and high-precision) |
| 69 | + - updatedAt: Timestamp |
| 70 | + - visibleCategories: array of category id strings (optional) |
| 71 | + - modelVersion: number (integer or float) |
| 72 | + - sharedWithUsers: array of user ids (present for shared wallets; may be empty) |
| 73 | + - bank: object (present for `bank` type wallets) |
| 74 | + - nature: string (e.g. `account`) |
| 75 | + - connection: string (connection id) |
| 76 | + - accountNumber: string (IBAN/account number) |
| 77 | + |
| 78 | +Notes and edge cases: |
| 79 | +- `currency` may differ per-wallet (HUF, EUR, etc.). |
| 80 | +- `visibleCategories` lists category IDs that are permitted/visible in that wallet; not every wallet has the same set. |
| 81 | + |
| 82 | + |
| 83 | +- users/{user}/categories/{category} |
| 84 | + - id: string (same as document id) -- some exports show this in list responses |
| 85 | + - name: string (e.g., "Other", "Étkezés") |
| 86 | + - type: string ("expense" or "income") |
| 87 | + |
| 88 | +- users/{user}/labels/{label} |
| 89 | + - text or name: string (label text). In list output `text` appears but client maps it to `name`. |
| 90 | + - (document id) is the label id |
| 91 | + |
| 92 | +- users/{user}/wallets/{wallet}/transactions/{transaction} |
| 93 | + - id: string (in many responses added by the client from `path.transaction`) |
| 94 | + - path: { user: string, wallet: string, transaction: string } |
| 95 | + - author: string (user id who created the transaction) |
| 96 | + - note: string (transaction description) |
| 97 | + - madeAt: Timestamp / DatetimeWithNanoseconds |
| 98 | + - madeAtTimezone: string (e.g., "Europe/Budapest") |
| 99 | + - madeAtTimezoneOffset: integer (seconds) |
| 100 | + - usdValue: { amount: string, exchangeRate: string } (strings in the Firestore export) |
| 101 | + - amount: number or string (local currency amount, sign indicates direction) |
| 102 | + - type: string (e.g., "regular") |
| 103 | + - isPending: boolean |
| 104 | + - category: categoryId (UUID string) |
| 105 | + - updatedAt: Timestamp |
| 106 | + - modelVersion: integer (1 currently) |
| 107 | + |
| 108 | + Example (extracted): |
| 109 | + { |
| 110 | + 'author': 'af150597-5b96-4f8b-9eb3-0b2e0aa72a81', |
| 111 | + 'note': '20250705 00074524 1007 MAMMUT BUDAPEST 4.500,00 HUF ...', |
| 112 | + 'madeAt': DatetimeWithNanoseconds(2025, 7, 7, 10, 0, tzinfo=datetime.timezone.utc), |
| 113 | + 'madeAtTimezoneOffset': 7200, |
| 114 | + 'usdValue': {'exchangeRate': '340.45914320', 'amount': '-13.21744500'}, |
| 115 | + 'type': 'regular', |
| 116 | + 'modelVersion': 1, |
| 117 | + 'updatedAt': DatetimeWithNanoseconds(...), |
| 118 | + 'madeAtTimezone': 'Europe/Budapest', |
| 119 | + 'category': '2a4df530-a79f-49f4-8623-60000d434daa', |
| 120 | + 'path': {'wallet': 'b368c5c2-68fe-4f98-9d4f-08e0cdca57a7', 'transaction': 'd2b4caa7-12eb-4c04-a744-d2bf7e02bdd2', 'user': 'af150597-5b96-4f8b-9eb3-0b2e0aa72a81'}, |
| 121 | + 'isPending': False, |
| 122 | + 'amount': '-4500' |
| 123 | + } |
| 124 | + |
| 125 | + |
| 126 | +## transactionLabels subcollection |
| 127 | +- users/{user}/wallets/{wallet}/transactions/{transaction}/transactionLabels |
| 128 | + - documents are small and often only contain a `label` field that references the label ID |
| 129 | + - list example (client side): `[x.get('label') for x in collection.get()]` -> `['01e9e6be-34ea-...']` |
| 130 | + - The client resolves IDs to human names using the labels collection |
| 131 | + |
| 132 | +## Cross-references and client-side maps |
| 133 | +- Client keeps mappings for convenience: |
| 134 | + - wallet_name_map: { wallet_name: wallet_id } |
| 135 | + - category_name_map: { category_id: category_name } |
| 136 | + - category_type_map: { category_id: 'income'|'expense' } |
| 137 | + - label_name_map: { label_id: label_text } |
| 138 | + |
| 139 | +## How to dump arbitrary documents (helper) |
| 140 | +- A helper `_get_raw_document(self, path: str, as_json: bool = False)` is useful to inspect any document by absolute path (for example `users/{userId}`, `users/{userId}/wallets/{walletId}`). |
| 141 | +- Example use (the project `run.py` now calls several examples): |
| 142 | + - `spendee._get_raw_document(f"users/{spendee.user_id}", as_json=True)` |
| 143 | + - `spendee._get_raw_document(f"users/{spendee.user_id}/wallets/{walletId}", as_json=True)` |
| 144 | + |
| 145 | +Provide outputs of these helper calls and I will update this schema with exact fields and types where necessary. |
| 146 | + |
| 147 | +## Next steps / tips |
| 148 | +- If you paste the JSON output of these helper calls here, I'll extend this file with precise types and optional fields, and add a complete JSON Schema if you want one. |
| 149 | + |
| 150 | +--- |
| 151 | + "documentChange": { |
| 152 | + "document": { |
| 153 | + "name": "projects/spendee-app/databases/(default)/documents/users/af150597-5b96-4f8b-9eb3-0b2e0aa72a81/deletedDocuments/75b406c0-a897-44f3-bce9-5191321d0507", |
| 154 | + "fields": { |
| 155 | + "modelVersion": { |
| 156 | + "integerValue": "1" |
| 157 | + }, |
| 158 | + "path": { |
| 159 | + "mapValue": { |
| 160 | + "fields": { |
| 161 | + "deletedDocument": { |
| 162 | + "stringValue": "75b406c0-a897-44f3-bce9-5191321d0507" |
| 163 | + }, |
| 164 | + "user": { |
| 165 | + "stringValue": "af150597-5b96-4f8b-9eb3-0b2e0aa72a81" |
| 166 | + } |
| 167 | + } |
| 168 | + } |
| 169 | + }, |
| 170 | + "createdAt": { |
| 171 | + "timestampValue": "2025-08-22T13:07:07.586Z" |
| 172 | + }, |
| 173 | + "deletedDocumentPath": { |
| 174 | + "stringValue": "users/af150597-5b96-4f8b-9eb3-0b2e0aa72a81/wallets/b368c5c2-68fe-4f98-9d4f-08e0cdca57a7/transactions/fbe90b02-f382-4607-808c-d37bcd17ae76/transactionLabels/0e6ec129-dc67-4fba-92db-953c4677cf76" |
| 175 | + } |
| 176 | + }, |
| 177 | + "createTime": "2025-08-22T13:07:07.615660Z", |
| 178 | + "updateTime": "2025-08-22T13:07:07.615660Z" |
| 179 | + }, |
| 180 | + "targetIds": [ |
| 181 | + 44 |
| 182 | + ] |
| 183 | + } |
| 184 | +} |
0 commit comments