Documentation
SDKs
Python

Python SDK

Installation

pip install moontraze

Quick Start

from moontraze import MoonSDK, MoonConfig
 
moon = MoonSDK(MoonConfig(
    project_id='YOUR_PROJECT_ID',
    api_key='YOUR_API_KEY',
))
 
# Auth
result = moon.auth.register('user@example.com', 'secret123', 'John')
moon.set_token(result['token'])
 
moon.auth.login('user@example.com', 'secret123')
moon.auth.logout()
moon.auth.change_password('oldSecret', 'newSecret')
 
# Database
moon.db.set('users/user_123', {'name': 'John'})
user = moon.db.get('users/user_123')
moon.db.update_field('users/user_123', 'status', 'verified')
moon.db.delete_field('users/user_123', 'phone')
moon.db.delete('users/user_123')
 
# Database Query
chats = moon.db.query(
    'chats',
    where={
        'OR': [
            {'fromUid': uid},
            {'toUid': uid},
        ],
    },
    order_by={'createdAt': 'desc'},
    limit=20,
)
unread = moon.db.count('chats', where={'toUid': uid, 'read': False})
 
# Storage
with open('photo.jpg', 'rb') as f:
    upload = moon.storage.upload(f, 'photo.jpg')
 
files = moon.storage.list()
moon.storage.create_folder('My Folder')
moon.storage.delete(file_id)
moon.storage.delete_folder(folder_id)
quota = moon.storage.get_quota()
 
# Notifications
moon.notifications.register_token(fcm_token, {'platform': 'android'})
moon.notifications.send_to_project(title='Hello', body='World')
moon.notifications.send_to_user('user_id', title='Hi', body='There')
moon.notifications.send_to_topic('premium', title='Hi', body='There')
history = moon.notifications.get_history(50)

Authentication

The SDK handles JWT automatically.

  • register() and login() set the token internally.
  • Every subsequent request sends the JWT in the Authorization header automatically.
  • logout() clears the token.

You do not need to manage tokens manually.


Database

Use / for nested paths. Each segment is a collection or document ID.

Create — set

moon.db.set('orders/order_1001/items/item_5', {
    'name': 'Wireless Mouse',
    'quantity': 2,
    'price': 799,
})

Creates:

orders/
  order_1001/
    items/
      item_5/
        name: "Wireless Mouse"
        quantity: 2
        price: 799

Important — set replaces the entire document

set() replaces the entire document with the data you provide. If the document already exists, any existing fields that are not included in the new dict will be lost.

Use set() for:

  • Creating a new document
  • Replacing a document completely (when you have the full dict)

Do NOT use set() for:

  • Updating a single field (e.g. last_seen_at, status, avatar_url)
  • Partial updates — use update_field() instead
# BAD — overwrites the whole document
# If `user` dict is incomplete, other fields get dropped
user = moon.db.get('users/user_123')
user['last_seen_at'] = datetime.utcnow().isoformat()
moon.db.set('users/user_123', user)
 
# GOOD — updates only last_seen_at, rest of the document stays intact
moon.db.update_field(
    'users/user_123',
    'last_seen_at',
    datetime.utcnow().isoformat(),
)

Get — get

# Full document
item = moon.db.get('orders/order_1001/items/item_5')
# {'name': 'Wireless Mouse', 'quantity': 2, 'price': 799}
 
# Specific nested field
name = moon.db.get('orders/order_1001/items/item_5/name')
# 'Wireless Mouse'

Update — update_field

# Single field
moon.db.update_field('orders/order_1001/items/item_5', 'quantity', 3)
 
# Deeply nested field
moon.db.update_field('users/user_123', 'address/city/zip', '10001')

update_field updates only the specified field

update_field() updates only the specified field, leaving all other fields in the document untouched. It is the safe way to do partial updates.

Use update_field() for:

  • Updating a single field (e.g. last_seen_at, status, avatar_url)
  • Updating a deeply nested field (e.g. address/city/zip)
  • Any change where you want to keep the rest of the document intact
# Updates only the `last_seen_at` field
moon.db.update_field(
    'users/user_123',
    'last_seen_at',
    datetime.utcnow().isoformat(),
)
 
# Updates a deeply nested field
moon.db.update_field('users/user_123', 'address/city/zip', '10001')

⚠️ Important — update_field only updates existing fields

update_field() updates an existing field — whether it is top-level or deeply nested. But if the field does not already exist in the document, update_field() will not create it — it fails silently.

Example — when the field exists:

# Existing: users/user_123 = { uid, name, address: { city: 'Mumbai' } }
moon.db.update_field('users/user_123', 'address/city', 'Delhi')
#  Works — address/city already exists

Example — when the field does not exist:

# Existing: users/user_123 = { uid, name }
moon.db.update_field('users/user_123', 'address/city', 'Mumbai')
#  Does NOT work — address field does not exist

★ Creating a new field (top-level or nested) — set with merge

To create a new field — whether it is a top-level field or a deeply nested folder — use set() with merge.

Formula:

1. existing = moon.db.get(path)
2. merged = { **existing, **new_fields }
3. moon.db.set(path, merged)

Add a new nested folder

# Existing: users/user_123 = { uid, name, phone }
existing = moon.db.get('users/user_123')
 
# Create a new nested folder
moon.db.set('users/user_123', {
    **existing,                     # ← preserve existing fields
    'location': {                   # ← new nested folder
        'city': 'Mumbai',
        'country': 'India',
    },
})
# Result: users/user_123 = { uid, name, phone, location: { city, country } }

Add a new field inside an existing nested folder

# Existing: users/user_123 = { uid, location: { city: 'Mumbai' } }
existing = moon.db.get('users/user_123')
 
moon.db.set('users/user_123', {
    **existing,
    'location': {
        **existing.get('location', {}),   # ← preserve existing nested fields
        'country': 'India',               # ← new field
    },
})
# Result: users/user_123 = { uid, location: { city: 'Mumbai', country: 'India' } }

Add a new top-level field

# Existing: users/user_123 = { uid, name }
existing = moon.db.get('users/user_123')
 
moon.db.set('users/user_123', {
    **existing,
    'phone': '+91-9876543210',       # ← new top-level field
})
# Result: users/user_123 = { uid, name, phone }

Decision matrix

SituationUse
Field exists, update its valueupdate_field()
Add a new top-level fieldset() with merge
Add a new nested folderset() with merge
Add a new field inside an existing nested folderset() with merge (nested dict unpacking)
Replace the entire documentset() without merge
Not sure whether the field existsset() with merge (always safe)

Rule of thumb

update_field() = update an existing field set() with merge = create a new field (safe)

If you are not 100% sure whether a field exists, use set() with merge. It is always safe, because **existing preserves all previous fields.

Delete — delete_field / delete

# Delete a field
moon.db.delete_field('orders/order_1001/items/item_5', 'price')
 
# Delete a document
moon.db.delete('orders/order_1001/items/item_5')
 
# Delete a nested collection (own)
moon.db.delete('orders/order_1001/items')

★ Query — Filter, Sort, Paginate

Fetch multiple documents from a collection with dynamic filters, sorting, and pagination. No schema required — any field can be queried.

result = moon.db.query(
    'chats',
    where={
        'OR': [
            {'fromUid': uid},
            {'toUid': uid},
        ],
    },
    order_by={'createdAt': 'desc'},
    limit=20,
    offset=0,
)
 
results = result['results']       # matching documents
total = result['total']           # total count
has_more = result['hasMore']      # pagination
took = result.get('took', 0)      # server time (ms)

Signature

def query(
    collection: str,
    where: Optional[Dict[str, Any]] = None,
    order_by: Optional[Dict[str, str]] = None,
    limit: int = 20,
    offset: int = 0,
) -> Dict[str, Any]

Returns:

{
    'results': List[Dict[str, Any]],  # matching documents
    'total': int,                     # total matching count
    'limit': int,
    'offset': int,
    'hasMore': bool,
    'took': int,                      # server time in ms
}

Note: Use order_by (snake_case) in Python. It is automatically sent as orderBy in the API request.

Filter operators

All operators are optional. Mix and match as needed.

OperatorExampleDescription
Direct equality{'status': 'active'}field = 'active'
eq{'status': {'eq': 'active'}}Same as above
neq / ne{'status': {'neq': 'banned'}}Not equal
gt{'age': {'gt': 18}}Greater than (numeric)
gte{'age': {'gte': 18}}Greater than or equal
lt{'price': {'lt': 1000}}Less than (numeric)
lte{'price': {'lte': 1000}}Less than or equal
contains{'name': {'contains': 'john'}}Case-insensitive substring
startsWith{'name': {'startsWith': 'j'}}Prefix match
endsWith{'email': {'endsWith': '@gmail.com'}}Suffix match
in{'role': {'in': ['admin', 'mod']}}Value in array
nin{'role': {'nin': ['banned']}}Value not in array
exists{'email': {'exists': True}}Field exists / not exists
OR{'OR': [{...}, {...}]}Logical OR
AND{'AND': [{...}, {...}]}Logical AND
NOT{'NOT': {...}}Logical NOT
Nested field{'address.city': 'Mumbai'}Dot notation
Null check{'deletedAt': None}IS NULL

Examples

Simple equality:

result = moon.db.query('users', where={'role': 'admin'})

Multiple conditions (AND implicit):

result = moon.db.query(
    'orders',
    where={
        'status': 'pending',
        'amount': {'gt': 5000},
    },
)

OR conditions:

result = moon.db.query(
    'chats',
    where={
        'OR': [
            {'fromUid': my_uid},
            {'toUid': my_uid},
        ],
    },
)

Complex nested:

result = moon.db.query(
    'posts',
    where={
        'AND': [
            {'status': 'published'},
            {
                'OR': [
                    {'authorUid': my_uid},
                    {'isPublic': True},
                ],
            },
            {'createdAt': {'gte': '2026-01-01'}},
        ],
    },
    order_by={'createdAt': 'desc'},
    limit=50,
)

In array:

result = moon.db.query(
    'products',
    where={
        'category': {'in': ['electronics', 'gadgets']},
        'stock': {'gt': 0},
    },
)

Text search:

result = moon.db.query(
    'users',
    where={'name': {'contains': 'john'}},
)

Pagination

# Page 1
page1 = moon.db.query(
    'posts',
    order_by={'createdAt': 'desc'},
    limit=20,
    offset=0,
)
 
# Page 2
page2 = moon.db.query(
    'posts',
    order_by={'createdAt': 'desc'},
    limit=20,
    offset=20,
)
 
# hasMore batata hai agar aur pages hain
if page1['hasMore']:
    # load next page
    pass

Sorting

# Single field
result = moon.db.query('posts', order_by={'createdAt': 'desc'})
 
# Multiple fields
result = moon.db.query(
    'products',
    order_by={
        'category': 'asc',
        'price': 'desc',
    },
)

Sort directions: 'asc' (or 'ASC'), 'desc' (or 'DESC').


★ Count — Match count without fetching data

Get the number of matching documents without downloading them. Useful for badges, unread counters, dashboard stats.

unread = moon.db.count(
    'chats',
    where={
        'toUid': uid,
        'read': False,
    },
)
 
print(unread)  # 5

Signature

def count(
    collection: str,
    where: Optional[Dict[str, Any]] = None,
) -> int

Returns: int — number of matching documents.

Examples

Total documents in a collection:

total = moon.db.count('posts')

Conditional count:

pending_orders = moon.db.count('orders', where={'status': 'pending'})

Unread messages in a chat:

unread = moon.db.count(
    'chats/conv_123/messages',
    where={
        'toUid': my_uid,
        'read': False,
    },
)

Full Example — Chat list (paginated)

def load_my_chats(my_uid, page=1, page_size=20):
    result = moon.db.query(
        'chats',
        where={
            'OR': [
                {'participant1': my_uid},
                {'participant2': my_uid},
            ],
        },
        order_by={'lastMessageAt': 'desc'},
        limit=page_size,
        offset=(page - 1) * page_size,
    )
    return result['results']

Performance notes

  • Indexed queries — dynamic JSONB GIN indexes make where filters fast, even on millions of documents.
  • Always use limit — never fetch unbounded collections. Default is 20, max is 500.
  • Use count() for badges — don't fetch all documents just to count them.
  • Paginate with offset — for infinite scroll, use limit + offset.

Collection Delete — Owner Only

Root collections (users, orders, posts) can only be deleted by the project owner from dashboard. Users can delete their own document and own nested collections only.

PathWho Can Delete
users/{myUid}User (own)
users/{myUid}/devicesUser (own nested)
users (root)Owner only — returns 403

Storage

Upload, manage, and download files. Two upload modes:

  1. Folder-based — upload into a folder (folder_id)
  2. Path-based — upload to a specific path (custom_path)

Upload

Folder-based (default)

Upload into a folder. Filename is auto-generated (UUID).

with open('photo.jpg', 'rb') as f:
    upload = moon.storage.upload(
        f,
        'photo.jpg',
        folder_id='folder_abc',
    )
 
print(upload['publicUrl'])   # https://api.moontraze.com/storage/.../uuid.jpg
print(upload['path'])        # /{projectId}/{folderName}/uuid.jpg

Path-based (custom path)

Upload to a specific path with a fixed filename. Useful for user-specific storage like users/{uid}/profile/avatar.jpg.

with open('avatar.jpg', 'rb') as f:
    upload = moon.storage.upload(
        f,
        'avatar.jpg',
        custom_path=f'users/{uid}/profile/avatar.jpg',
    )
 
print(upload['publicUrl'])   # https://api.moontraze.com/storage/.../users/.../avatar.jpg
print(upload['path'])        # /{projectId}/users/{uid}/profile/avatar.jpg

Upload from path (helper)

# Folder-based
upload = moon.storage.upload_from_path(
    '/path/to/photo.jpg',
    folder_id='folder_abc',
)
 
# Path-based
upload = moon.storage.upload_from_path(
    '/path/to/avatar.jpg',
    custom_path=f'users/{uid}/profile/avatar.jpg',
)

Priority

  1. custom_path — path-based (highest priority)
  2. folder_id — folder-based
  3. Root level — if neither

List Files

files = moon.storage.list()
folder_files = moon.storage.list(folder_id='folder_abc')

Delete

moon.storage.delete(file_id)

Folders

folder = moon.storage.create_folder('My Folder')
folders = moon.storage.list_folders()
moon.storage.delete_folder(folder_id)   # recursive

Download

Downloads use presigned URLs — the client fetches directly from MinIO/S3, keeping your server free from bandwidth load.

# Get presigned URL (10 min expiry)
result = moon.storage.get_presigned_url(file_id)
print(result['url'])          # presigned MinIO URL
print(result['expiresIn'])    # seconds
 
# Download file to path
moon.storage.download(file_id, '/path/to/save.jpg')
 
# Get direct download URL
url = moon.storage.get_download_url(file_id)

Presigned URLs work the same for folder_id and custom_path uploads — both are stored in the same table and served identically.

Quota

quota = moon.storage.get_quota()
print(quota['usedBytes'])
print(quota['limitBytes'])

Notifications

Push notifications are delivered via your own Firebase project (BYOF). One-time setup from the Moontraze dashboard is required before sending.

One-time Setup

  1. Create your own Firebase project
  2. Add your Android app (matching your applicationId)
  3. Download google-services.json and place it in android/app/
  4. Generate a service account key from Firebase Console
  5. Upload the service account JSON in Moontraze dashboard → Notification Setup

See the Push Notifications Guide for details.

Register FCM Token

moon.notifications.register_token(fcm_token, {'platform': 'android'})

Unregister FCM Token

moon.notifications.unregister_token(fcm_token)

Send to All Users in Project

moon.notifications.send_to_project(
    title='New Feature Released',
    body='Check out what we shipped!',
    data={'type': 'announcement'},
)

Send to Specific User

moon.notifications.send_to_user(
    'user_id',
    title='Order Shipped',
    body='Your order is on the way',
    data={
        'type': 'order',
        'orderId': '1234',
        'deepLink': 'myapp://order/1234',
    },
)

Send to Topic

Devices must subscribe to the topic first:

moon.notifications.send_to_topic(
    'premium_users',
    title='Premium Feature',
    body='Exclusive for premium users',
)

Schedule for Later

from datetime import datetime, timezone
 
moon.notifications.schedule(
    title='Reminder',
    body="Don't forget!",
    send_at=datetime(2026, 12, 31, 10, 0, 0, tzinfo=timezone.utc),
    user_id='user_abc',      # optional — omit for project-wide
    data={'type': 'reminder'},
)

Get History

history = moon.notifications.get_history(50)
 
for item in history:
    print(f"{item['title']} — {item['status']}")
    print(f"Success: {item['successCount']}, Failed: {item['failureCount']}")

Delete History

# Delete a single notification
moon.notifications.delete_history('notification_id')
 
# Clear all history for the current project
moon.notifications.clear_history()

Preferences

prefs = moon.notifications.get_preferences()
 
moon.notifications.update_preferences({
    'pushEnabled': True,
    'emailEnabled': False,
    'marketingEnabled': False,
    'securityEnabled': True,
    'quietHoursEnabled': True,
    'quietHoursStart': '22:00',
    'quietHoursEnd': '08:00',
    'timezone': 'Asia/Kolkata',
})

Error: Firebase Not Configured

If a send method is called without Firebase credentials uploaded:

MoonException: Firebase not configured for this project.
Code: FIREBASE_NOT_CONFIGURED

Fix: Upload your service account JSON in Moontraze dashboard → Notification Setup.

Data Payload Notes

  • All data values are auto-converted to strings (FCM requirement)
  • Total payload (title + body + data) must stay under 4 KB
  • Common keys: type, deepLink, imageUrl, plus any app-specific IDs

Registry