Python SDK
Installation
pip install moontrazeQuick 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()andlogin()set the token internally.- Every subsequent request sends the JWT in the
Authorizationheader 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: 799Important —
setreplaces 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_fieldupdates 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 existsExample — 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
| Situation | Use |
|---|---|
| Field exists, update its value | update_field() |
| Add a new top-level field | set() with merge |
| Add a new nested folder | set() with merge |
| Add a new field inside an existing nested folder | set() with merge (nested dict unpacking) |
| Replace the entire document | set() without merge |
| Not sure whether the field exists | set() with merge (always safe) |
Rule of thumb
update_field()= update an existing fieldset()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**existingpreserves 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 asorderByin the API request.
Filter operators
All operators are optional. Mix and match as needed.
| Operator | Example | Description |
|---|---|---|
| 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
passSorting
# 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) # 5Signature
def count(
collection: str,
where: Optional[Dict[str, Any]] = None,
) -> intReturns: 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
wherefilters 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, uselimit + 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.
| Path | Who Can Delete |
|---|---|
users/{myUid} | User (own) |
users/{myUid}/devices | User (own nested) |
users (root) | Owner only — returns 403 |
Storage
Upload, manage, and download files. Two upload modes:
- Folder-based — upload into a folder (
folder_id) - 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.jpgPath-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.jpgUpload 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
custom_path— path-based (highest priority)folder_id— folder-based- 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) # recursiveDownload
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
- Create your own Firebase project
- Add your Android app (matching your
applicationId) - Download
google-services.jsonand place it inandroid/app/ - Generate a service account key from Firebase Console
- 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_CONFIGUREDFix: Upload your service account JSON in Moontraze dashboard → Notification Setup.
Data Payload Notes
- All
datavalues 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