Flutter SDK
Installation
flutter pub add moontraze_flutterQuick Start
import 'package:moontraze_flutter/moontraze_flutter.dart';
final moon = MoonSDK(MoonConfig(
projectId: 'YOUR_PROJECT_ID',
apiKey: 'YOUR_API_KEY',
));
// Auth
final result = await moon.auth.register('user@example.com', 'secret123', 'John');
moon.setToken(result['token']);
await moon.auth.login('user@example.com', 'secret123');
moon.auth.logout();
await moon.auth.changePassword('oldSecret', 'newSecret');
// Database
await moon.db.set('users/user_123', {'name': 'John'});
final user = await moon.db.get('users/user_123');
await moon.db.updateField('users/user_123', 'status', 'verified');
await moon.db.deleteField('users/user_123', 'phone');
await moon.db.delete('users/user_123');
// Database Query
final chats = await moon.db.query('chats',
where: {
'OR': [
{'fromUid': uid},
{'toUid': uid},
],
},
orderBy: {'createdAt': 'desc'},
limit: 20,
);
final unread = await moon.db.count('chats',
where: {'toUid': uid, 'read': false},
);
// Storage
final file = File('photo.jpg');
final upload = await moon.storage.upload(file);
final files = await moon.storage.list();
await moon.storage.createFolder('My Folder');
await moon.storage.delete(fileId);
await moon.storage.deleteFolder(folderId);
final quota = await moon.storage.getQuota();
// Notifications
await moon.notifications.registerToken(fcmToken, {'platform': 'android'});
await moon.notifications.sendToProject(title: 'Hello', body: 'World');
await moon.notifications.sendToUser('user_id', title: 'Hi', body: 'There');
await moon.notifications.sendToTopic('premium', title: 'Hi', body: 'There');
final history = await moon.notifications.getHistory(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.
Storage
Upload
Upload files to your project's storage. Two upload modes:
Folder-based (default)
Upload into a folder. Filename is auto-generated (UUID).
final file = File('photo.jpg');
final upload = await moon.storage.upload(
file,
folderId: '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.
final file = File('avatar.jpg');
final upload = await moon.storage.upload(
file,
customPath: 'users/$uid/profile/avatar.jpg',
);
print(upload['publicUrl']); // https://api.moontraze.com/storage/.../users/.../avatar.jpg
print(upload['path']); // /{projectId}/users/{uid}/profile/avatar.jpgPriority
customPath— path-based (highest priority)folderId— folder-based- Root level — if neither
Upload from bytes
final bytes = await file.readAsBytes();
await moon.storage.uploadBytes(
bytes,
'avatar.jpg',
customPath: 'users/$uid/profile/avatar.jpg',
contentType: 'image/jpeg',
);List Files
final files = await moon.storage.list();
final folderFiles = await moon.storage.list(folderId: 'folder_abc');Delete
await moon.storage.delete(fileId);Folders
final folder = await moon.storage.createFolder('My Folder');
final folders = await moon.storage.listFolders();
await moon.storage.deleteFolder(folderId); // 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)
final result = await moon.storage.getPresignedUrl(fileId);
print(result['url']); // presigned MinIO URL
print(result['expiresIn']); // seconds
// Download as bytes
final bytes = await moon.storage.download(fileId);
// Save to file
await moon.storage.downloadToFile(fileId, '/path/to/save.jpg');Presigned URLs work the same for folderId and customPath uploads —
both are stored in the same table and served identically.
Quota
final quota = await moon.storage.getQuota();
print(quota['usedBytes']);
print(quota['limitBytes']);Deep Nested Paths
Use / to create nested collections and documents. Each segment is a level.
collection/docId/nestedCollection/nestedDocId/fieldExample path: chats/uid1/uid2/msg123
chats— root collectionuid1— document (nested collection parent)uid2— document (nested collection parent)msg123— final document ID
Create
await moon.db.set('chats/uid1/uid2/msg123', {
'text': 'Hello World',
'sender': 'uid1',
});Important —
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 map will be lost.Use
set()for:
- Creating a new document
- Replacing a document completely (when you have the full map)
Do NOT use
set()for:
- Updating a single field (e.g.
lastSeenAt,status,avatarUrl)- Partial updates — use
updateField()instead// BAD — overwrites the whole document // If `user` map is incomplete, other fields get dropped final user = await moon.db.get('users/user_123'); user['lastSeenAt'] = DateTime.now().toIso8601String(); await moon.db.set('users/user_123', user); // GOOD — updates only lastSeenAt, rest of the document stays intact await moon.db.updateField( 'users/user_123', 'lastSeenAt', DateTime.now().toIso8601String(), );
Get
// Full document
final msg = await moon.db.get('chats/uid1/uid2/msg123');
// Specific nested field
final text = await moon.db.get('chats/uid1/uid2/msg123/text');Update
// Single field
await moon.db.updateField('chats/uid1/uid2/msg123', 'text', 'Updated');
// Nested field
await moon.db.updateField('users/user_123', 'address/city/zip', '10002');
updateFieldupdates only the specified field
updateField()updates only the specified field, leaving all other fields in the document untouched. It is the safe way to do partial updates.Use
updateField()for:
- Updating a single field (e.g.
lastSeenAt,status,avatarUrl)- 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 `lastSeenAt` field await moon.db.updateField( 'users/user_123', 'lastSeenAt', DateTime.now().toIso8601String(), ); // Updates a deeply nested field await moon.db.updateField('users/user_123', 'address/city/zip', '10002');
⚠️ Important — updateField only updates existing fields
updateField() updates an existing field — whether it is top-level or deeply nested. But if the field does not already exist in the document, updateField() will not create it — it fails silently.
Example — when the field exists:
// Existing: users/user_123 = { uid, name, address: { city: 'Mumbai' } }
await moon.db.updateField('users/user_123', 'address/city', 'Delhi');
// Works — address/city already existsExample — when the field does not exist:
// Existing: users/user_123 = { uid, name }
await moon.db.updateField('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 = await get(path)
2. merged = { ...existing, ...newFields }
3. await set(path, merged)Add a new nested folder
// Existing: users/user_123 = { uid, name, phone }
final existing = await moon.db.get('users/user_123');
// Create a new nested folder
await 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' } }
final existing = await moon.db.get('users/user_123');
await moon.db.set('users/user_123', {
...existing,
'location': {
...(existing['location'] as Map<String, dynamic>? ?? {}), // ← preserve nested
'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 }
final existing = await moon.db.get('users/user_123');
await 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 | updateField() |
| 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 spread) |
| Replace the entire document | set() without merge |
| Not sure whether the field exists | set() with merge (always safe) |
Rule of thumb
updateField()= 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 a field
await moon.db.deleteField('chats/uid1/uid2/msg123', 'text');
// Delete a document
await moon.db.delete('chats/uid1/uid2/msg123');
// Delete a nested collection (own)
await moon.db.delete('chats/uid1/uid2');★ Query — Filter, Sort, Paginate
Fetch multiple documents from a collection with dynamic filters, sorting, and pagination. No schema required — any field can be queried.
final result = await moon.db.query('chats',
where: {
'OR': [
{'fromUid': uid},
{'toUid': uid},
],
},
orderBy: {'createdAt': 'desc'},
limit: 20,
offset: 0,
);
final List results = result['results']; // matching documents
final int total = result['total']; // total count
final bool hasMore = result['hasMore']; // pagination
final int took = result['took'] ?? 0; // server time (ms)Signature
Future<Map<String, dynamic>> query(
String collection, {
Map<String, dynamic>? where,
Map<String, dynamic>? orderBy,
int limit = 20,
int offset = 0,
})Returns:
{
'results': List<Map<String, dynamic>>, // matching documents
'total': int, // total matching count
'limit': int,
'offset': int,
'hasMore': bool,
'took': int, // server time in ms
}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': null} | IS NULL |
Examples
Simple equality:
final result = await moon.db.query('users',
where: {'role': 'admin'},
);Multiple conditions (AND implicit):
final result = await moon.db.query('orders',
where: {
'status': 'pending',
'amount': {'gt': 5000},
},
);OR conditions:
final result = await moon.db.query('chats',
where: {
'OR': [
{'fromUid': myUid},
{'toUid': myUid},
],
},
);Complex nested:
final result = await moon.db.query('posts',
where: {
'AND': [
{'status': 'published'},
{
'OR': [
{'authorUid': myUid},
{'isPublic': true},
],
},
{'createdAt': {'gte': '2026-01-01'}},
],
},
orderBy: {'createdAt': 'desc'},
limit: 50,
);In array:
final result = await moon.db.query('products',
where: {
'category': {'in': ['electronics', 'gadgets']},
'stock': {'gt': 0},
},
);Text search:
final result = await moon.db.query('users',
where: {
'name': {'contains': 'john'},
},
);Pagination
// Page 1
final page1 = await moon.db.query('posts',
orderBy: {'createdAt': 'desc'},
limit: 20,
offset: 0,
);
// Page 2
final page2 = await moon.db.query('posts',
orderBy: {'createdAt': 'desc'},
limit: 20,
offset: 20,
);
// hasMore batata hai agar aur pages hain
if (page1['hasMore'] == true) {
// load next page
}Sorting
// Single field
final result = await moon.db.query('posts',
orderBy: {'createdAt': 'desc'},
);
// Multiple fields
final result = await moon.db.query('products',
orderBy: {
'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.
final unread = await moon.db.count('chats',
where: {
'toUid': uid,
'read': false,
},
);
print(unread); // 5Signature
Future<int> count(
String collection, {
Map<String, dynamic>? where,
})Returns: int — number of matching documents.
Examples
Total documents in a collection:
final total = await moon.db.count('posts');Conditional count:
final pendingOrders = await moon.db.count('orders',
where: {'status': 'pending'},
);Unread messages in a chat:
final unread = await moon.db.count('chats/conv_123/messages',
where: {
'toUid': myUid,
'read': false,
},
);Full Example — Chat list (paginated)
Future<List<Map<String, dynamic>>> loadMyChats({
required String myUid,
int page = 1,
int pageSize = 20,
}) async {
final result = await moon.db.query('chats',
where: {
'OR': [
{'participant1': myUid},
{'participant2': myUid},
],
},
orderBy: {'lastMessageAt': 'desc'},
limit: pageSize,
offset: (page - 1) * pageSize,
);
return List<Map<String, dynamic>>.from(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 can only be deleted by the project owner from dashboard.
| Path | Who Can Delete |
|---|---|
users/{myUid} | User (own document) |
users/{myUid}/devices | User (own nested) |
users (root) | Owner only — 403 for users |
chats (root) | Owner only — 403 for users |
Why? Root collections contain multiple users' data. Rules engine checks auth.uid and only allows delete on paths the user owns.
Response on denied:
{
"error": "Permission denied: You do not have permission to delete this document",
"status": 403
}