React Native SDK
Installation
npm install @moontraze/react-nativeQuick Start
import MoonSDK from '@moontraze/react-native';
const moon = new MoonSDK({
projectId: 'YOUR_PROJECT_ID',
apiKey: 'YOUR_API_KEY',
});
// Auth
const 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' });
const 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
const chats = await moon.db.query('chats', {
where: {
OR: [
{ fromUid: uid },
{ toUid: uid },
],
},
orderBy: { createdAt: 'desc' },
limit: 20,
});
const unread = await moon.db.count('chats', {
where: { toUid: uid, read: false },
});
// Storage
await moon.storage.upload({
uri: 'file:///path/to/photo.jpg',
name: 'photo.jpg',
type: 'image/jpeg',
});
const files = await moon.storage.list();
await moon.storage.createFolder('My Folder');
await moon.storage.delete(fileId);
await moon.storage.deleteFolder(folderId);
const 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({ topic: 'premium', title: 'Hi', body: 'There' });
const 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.
Database
Use / for nested paths. Each segment is a collection or document ID.
Create — set
await moon.db.set('courses/js_101/lessons/les_3', {
title: 'Async/Await',
duration: 25,
videoUrl: 'https://cdn.example.com/les3.mp4',
});Creates:
courses/
js_101/
lessons/
les_3/
title: "Async/Await"
duration: 25
videoUrl: "..."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 object will be lost.Use
set()for:
- Creating a new document
- Replacing a document completely (when you have the full object)
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` object is incomplete, other fields get dropped const user = await moon.db.get('users/user_123'); user.lastSeenAt = new Date().toISOString(); 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', new Date().toISOString(), );
Get — get
// Full document
const lesson = await moon.db.get('courses/js_101/lessons/les_3');
// { title: 'Async/Await', duration: 25, videoUrl: '...' }
// Specific nested field
const title = await moon.db.get('courses/js_101/lessons/les_3/title');
// 'Async/Await'Update — updateField
// Single field
await moon.db.updateField('courses/js_101/lessons/les_3', 'duration', 30);
// Deeply nested field
await moon.db.updateField('users/user_123', 'address/city/zip', '10001');
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', new Date().toISOString(), ); // Updates a deeply nested field await moon.db.updateField('users/user_123', 'address/city/zip', '10001');
⚠️ 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 }
const 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' } }
const existing = await moon.db.get('users/user_123');
await moon.db.set('users/user_123', {
...existing,
location: {
...(existing.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 }
const 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 — deleteField / delete
// Delete a field
await moon.db.deleteField('courses/js_101/lessons/les_3', 'videoUrl');
// Delete a document
await moon.db.delete('courses/js_101/lessons/les_3');
// Delete a nested collection (own)
await moon.db.delete('courses/js_101/lessons');★ Query — Filter, Sort, Paginate
Fetch multiple documents from a collection with dynamic filters, sorting, and pagination. No schema required — any field can be queried.
const result = await moon.db.query('chats', {
where: {
OR: [
{ fromUid: uid },
{ toUid: uid },
],
},
orderBy: { createdAt: 'desc' },
limit: 20,
offset: 0,
});
const results = result.results; // matching documents
const total = result.total; // total count
const hasMore = result.hasMore; // pagination
const took = result.took; // server time (ms)Signature
query(collection: string, options?: {
where?: Record<string, any>;
orderBy?: Record<string, 'asc' | 'desc' | 'ASC' | 'DESC'>;
limit?: number; // default 20, max 500
offset?: number; // default 0
}): Promise<{
results: any[];
total: number;
limit: number;
offset: number;
hasMore: boolean;
took?: number;
}>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:
const result = await moon.db.query('users', {
where: { role: 'admin' },
});Multiple conditions (AND implicit):
const result = await moon.db.query('orders', {
where: {
status: 'pending',
amount: { gt: 5000 },
},
});OR conditions:
const result = await moon.db.query('chats', {
where: {
OR: [
{ fromUid: myUid },
{ toUid: myUid },
],
},
});Complex nested:
const 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:
const result = await moon.db.query('products', {
where: {
category: { in: ['electronics', 'gadgets'] },
stock: { gt: 0 },
},
});Text search:
const result = await moon.db.query('users', {
where: {
name: { contains: 'john' },
},
});Pagination
// Page 1
const page1 = await moon.db.query('posts', {
orderBy: { createdAt: 'desc' },
limit: 20,
offset: 0,
});
// Page 2
const page2 = await moon.db.query('posts', {
orderBy: { createdAt: 'desc' },
limit: 20,
offset: 20,
});
// hasMore batata hai agar aur pages hain
if (page1.hasMore) {
// load next page
}Sorting
// Single field
const result = await moon.db.query('posts', {
orderBy: { createdAt: 'desc' },
});
// Multiple fields
const 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.
const unread = await moon.db.count('chats', {
where: {
toUid: uid,
read: false,
},
});
console.log(unread); // 5Signature
count(collection: string, options?: {
where?: Record<string, any>;
}): Promise<number>Returns: number — number of matching documents.
Examples
Total documents in a collection:
const total = await moon.db.count('posts');Conditional count:
const pendingOrders = await moon.db.count('orders', {
where: { status: 'pending' },
});Unread messages in a chat:
const unread = await moon.db.count('chats/conv_123/messages', {
where: {
toUid: myUid,
read: false,
},
});Full Example — Chat list (paginated)
async function loadMyChats({
myUid,
page = 1,
pageSize = 20,
}: {
myUid: string;
page?: number;
pageSize?: number;
}) {
const result = await moon.db.query('chats', {
where: {
OR: [
{ participant1: myUid },
{ participant2: myUid },
],
},
orderBy: { lastMessageAt: 'desc' },
limit: pageSize,
offset: (page - 1) * pageSize,
});
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, courses, 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 (
folderId) - Path-based — upload to a specific path (
customPath)
Upload
Folder-based (default)
Upload into a folder. Filename is auto-generated (UUID).
await moon.storage.upload(
{
uri: 'file:///path/to/photo.jpg',
name: 'photo.jpg',
type: 'image/jpeg',
},
{ folderId: 'folder_abc' },
);
// Result:
// {
// id: 'file_abc',
// name: 'photo.jpg',
// publicUrl: 'https://api.moontraze.com/storage/.../uuid.jpg',
// 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.
await moon.storage.upload(
{
uri: 'file:///path/to/avatar.jpg',
name: 'avatar.jpg',
type: 'image/jpeg',
},
{ customPath: `users/${uid}/profile/avatar.jpg` },
);
// Result:
// {
// id: 'file_abc',
// name: 'avatar.jpg',
// publicUrl: 'https://api.moontraze.com/storage/.../users/.../avatar.jpg',
// path: '/{projectId}/users/{uid}/profile/avatar.jpg',
// }Priority
customPath— path-based (highest priority)folderId— folder-based- Root level — if neither
Backward compatible
Old style still works:
// String folderId
await moon.storage.upload(file, 'folder_abc');List Files
const files = await moon.storage.list();
const folderFiles = await moon.storage.list('folder_abc');Delete
await moon.storage.delete(fileId);Folders
const folder = await moon.storage.createFolder('My Folder');
const 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)
const result = await moon.storage.getPresignedUrl(fileId);
console.log(result.url); // presigned MinIO URL
console.log(result.expiresIn); // seconds
// Get direct URLs
const downloadUrl = await moon.storage.getDownloadUrl(fileId);
const previewUrl = await moon.storage.getPreviewUrl(fileId);Presigned URLs work the same for folderId and customPath uploads —
both are stored in the same table and served identically.
Quota
const quota = await moon.storage.getQuota();
console.log(quota.usedBytes);
console.log(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
await moon.notifications.registerToken(fcmToken, { platform: 'android' });Unregister FCM Token
await moon.notifications.unregisterToken(fcmToken);Send to All Users in Project
await moon.notifications.sendToProject({
title: 'New Feature Released',
body: 'Check out what we shipped!',
data: { type: 'announcement' },
});Send to Specific User
await moon.notifications.sendToUser('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:
await messaging().subscribeToTopic('premium_users');
await moon.notifications.sendToTopic({
topic: 'premium_users',
title: 'Premium Feature',
body: 'Exclusive for premium users',
});Schedule for Later
await moon.notifications.schedule(
{
title: 'Reminder',
body: "Don't forget!",
userId: 'user_abc', // optional — omit for project-wide
data: { type: 'reminder' },
},
new Date('2026-12-31T10:00:00Z'),
);Get History
const history = await moon.notifications.getHistory(50);
history.forEach((item) => {
console.log(`${item.title} — ${item.status}`);
console.log(`Success: ${item.successCount}, Failed: ${item.failureCount}`);
});Delete History
// Delete a single notification
await moon.notifications.deleteHistory('notification_id');
// Clear all history for the current project
await moon.notifications.clearHistory();Preferences
const prefs = await moon.notifications.getPreferences();
await moon.notifications.updatePreferences({
pushEnabled: true,
emailEnabled: false,
marketingEnabled: false,
securityEnabled: true,
quietHoursEnabled: true,
quietHoursStart: '22:00',
quietHoursEnd: '08:00',
timezone: 'Asia/Kolkata',
});Handling Incoming Notifications
The SDK does not manage firebase_messaging stream listeners — you wire them yourself:
// Foreground
messaging().onMessage(async (remoteMessage) => {
console.log('Foreground:', remoteMessage.notification?.title);
});
// Tap when app was in background
messaging().onNotificationOpenedApp((remoteMessage) => {
const data = remoteMessage.data;
if (data.type === 'order') {
navigation.navigate('Order', { orderId: data.orderId });
}
});
// Background handler (must be registered outside component)
messaging().setBackgroundMessageHandler(async (remoteMessage) => {
console.log('Background message:', remoteMessage);
});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