Documentation
SDKs
Flutter

Flutter SDK

Installation

flutter pub add moontraze_flutter

Quick 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() 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.


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.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.

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.jpg

Priority

  1. customPath — path-based (highest priority)
  2. folderId — folder-based
  3. 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);   // 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)
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/field

Example path: chats/uid1/uid2/msg123

  • chats — root collection
  • uid1 — 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 — 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 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');

updateField updates 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 exists

Example — 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

SituationUse
Field exists, update its valueupdateField()
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 spread)
Replace the entire documentset() without merge
Not sure whether the field existsset() with merge (always safe)

Rule of thumb

updateField() = 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 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.

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': 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); // 5

Signature

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 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 can only be deleted by the project owner from dashboard.

PathWho Can Delete
users/{myUid}User (own document)
users/{myUid}/devicesUser (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
}

Registry